From 736ae55d66ed7a65da4a8cc91639b834e89747c1 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Thu, 18 Jun 2026 13:09:50 +0200 Subject: [PATCH] =?UTF-8?q?feat(voyage):=20S6=20=E2=80=94=20v5.5=20brief?= =?UTF-8?q?=20framing=20enforcement=20(brief=5Fversion=202.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB --- CLAUDE.md | 4 +- README.md | 6 ++ agents/brief-reviewer.md | 40 +++++++- commands/trekbrief.md | 88 +++++++++++++++--- docs/HANDOVER-CONTRACTS.md | 15 ++- lib/validators/brief-validator.mjs | 51 ++++++++++ templates/trekbrief-template.md | 17 +++- tests/commands/trekbrief.test.mjs | 58 ++++++++++++ tests/lib/doc-consistency.test.mjs | 53 +++++++++-- tests/validators/brief-validator.test.mjs | 108 ++++++++++++++++++++++ 10 files changed, 409 insertions(+), 31 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5ce6e0c..be3a9b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execut > **Trinity context (2026-05-13, informational).** Voyage is Tier 1 (per-task) of a three-tier architecture in active design under the author's private marketplace: Tier 2 `app-creator` (per-app — "what does the app need, what's the next brief?") produces briefs Voyage consumes; Tier 3 `app-factory` (per-portfolio — "which app needs me now?") aggregates state across multiple app-creator instances. Both are pre-implementation and will ship to Forgejo when ready. **Asymmetry is a hard invariant:** Voyage stays unaware of Tier 2/3. Handover 1 (brief format) is the only integration point — any compatible producer can feed Voyage, app-creator is not privileged. Brief-schema changes are therefore breaking changes for downstream consumers, formalized as a public contract in v5.4 — see `docs/HANDOVER-CONTRACTS.md` §Handover 1 (PUBLIC CONTRACT). -> **Cross-cutting invariant: brief framing must match operator intent (2026-05-15).** Etablert etter residiv. Briefen er pipelinens source of truth; operatørens intent lever i hodet + i memory-filer (`feedback_*`, `project_*`); pipelinen tvinger ikke alignment. Høyere reasoning-kraft polerer feil premiss istedenfor å utfordre det. **Tre lag av forsvar (input-siden), alle BLOCKER ved brudd når v5.5 shipper:** (1) eksplisitt `framing: preserve|refine|replace|new-direction` i brief-frontmatter, `AskUserQuestion`-validert før brief-prosa skrives; (2) memory-alignment check som ny dimensjon i `brief-reviewer` — sammenlikner brief-prosa mot relevante memory-filer og rapporterer eksplisitte motsigelser; (3) obligatorisk `## TL;DR`-seksjon (≤ 5 linjer) øverst i `brief.md`. Implementeres i v5.5 (tracket i `STATE.md` § NESTE STEG når aktivt). Inntil shipping: operatør må manuelt sjekke at briefens framingord ikke motsier intent, særlig etter avvist iterasjon hvor "delta fra forrige" er en farlig default-ankring. +> **Cross-cutting invariant: brief framing must match operator intent (2026-05-15).** Etablert etter residiv. Briefen er pipelinens source of truth; operatørens intent lever i hodet + i memory-filer (`feedback_*`, `project_*`); pipelinen tvinger ikke alignment. Høyere reasoning-kraft polerer feil premiss istedenfor å utfordre det. **Tre lag av forsvar (input-siden), implementert i S6 som `brief_version 2.2`-gate (v5.5), alle BLOCKER ved brudd for briefer som deklarerer ≥ 2.2:** (1) eksplisitt `framing: preserve|refine|replace|new-direction` i brief-frontmatter, `AskUserQuestion`-validert i `/trekbrief` Phase 2.5 før brief-prosa skrives (ikke-skippbar, også i `--quick`); enum-feil → `BRIEF_INVALID_FRAMING` (alle versjoner), fravær ved ≥ 2.2 → `BRIEF_MISSING_FRAMING`; (2) memory-alignment som dimensjon 6 i `brief-reviewer` — sammenlikner brief-prosa + `framing` mot relevante memory-filer, rapporterer kun eksplisitte motsigelser (degraderer til score 5 N/A uten memory-kontekst), wired til Phase 4e-gate (`memory_alignment.score ≥ 4`); (3) obligatorisk `## TL;DR`-seksjon (≤ 5 linjer, soft-cap → `BRIEF_TLDR_TOO_LONG`) øverst i `brief.md`. Eksisterende 2.0/2.1-briefer forblir gyldige (forward+backward-compat, speiler `phase_signals ≥ 2.1`-presedensen); `trekreview`-briefer er unntatt. **Schema-akse bumpet (2.1→2.2); plugin-versjon-badge + CHANGELOG bumpes ved den koordinerte releasen (S10).** Kontrakt-evolusjon dokumentert i `docs/HANDOVER-CONTRACTS.md` §Handover 1. Den gamle manuelle stopgap-sjekken er dermed retired for ≥ 2.2-briefer. ## Commands @@ -39,7 +39,7 @@ Full flag reference for each command (modes, `--gates`, `--profile`, breaking ch | git-historian | opus | Recent changes, ownership, hot files | | research-scout | opus | External docs for unfamiliar tech (conditional, planning only) | | convention-scanner | opus | Coding conventions: naming, style, error handling, test patterns | -| brief-reviewer | opus | Task brief quality (5 dimensions: completeness, consistency, testability, scope clarity, research plan validity) | +| brief-reviewer | opus | Task brief quality (6 dimensions: completeness, consistency, testability, scope clarity, research plan validity, memory alignment) | | brief-conformance-reviewer | opus | Brief conformance review (SC + Non-Goal traceability) | | code-correctness-reviewer | opus | Code correctness review (7 dimensions) | | review-coordinator | opus | Judge Agent — dedup + reasonableness filter + verdict | diff --git a/README.md b/README.md index c9a4ab0..08f9f2d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ A [Claude Code](https://docs.anthropic.com/en/docs/claude-code) plugin for deep implementation planning, multi-source research, autonomous execution, independent post-hoc review, and zero-friction multi-session resumption. Six commands, one pipeline: +> **What's new — brief framing enforcement (`brief_version 2.2`)** — `/trekbrief` now opens with a **framing declaration** (Phase 2.5): every brief records `framing: preserve | refine | replace | new-direction` *before* any prose is written, so the plan can't quietly polish a wrong premise. Three version-gated layers ship at `brief_version ≥ 2.2`: +> - **Framing field** — a required `framing` enum in frontmatter; enum-checked on any version, and a missing value at `≥ 2.2` is a blocker. Collected in Phase 2.5 before prose, non-skippable even in `--quick`. +> - **Memory alignment** — a new `brief-reviewer` dimension compares the brief's Intent/Goal + framing against operator memory and flags *explicit* contradictions (a no-op that scores N/A when no memory is supplied). +> - **Obligatory `## TL;DR`** — a ≤ 5-line, framing-anchored summary at the top of every brief, so a wrong premise is caught at a glance. +> Existing `2.0`/`2.1` briefs stay valid (forward + backward compatible), mirroring the `phase_signals ≥ 2.1` gate. This is a **schema-axis** change — the plugin version badge bumps at the next coordinated release; see [docs/HANDOVER-CONTRACTS.md](docs/HANDOVER-CONTRACTS.md) §Handover 1 for the contract evolution. +> > **What's new in v5.1.1** — Remediation patch closing 11 of 12 findings from the v5.1.0 review (SC8 dogfood gate scheduled for sesjon 8). Lukker: > - **Bug fixes (load-bearing):** YAML-number bypass in `brief-validator` (#8) + doc-consistency pin lock-in (#11) so the gate fires for both quoted and unquoted `brief_version`. > - **Wiring:** `phase-signal-resolver` helper wired into all 4 downstream commands (#9) with TDD pair `resolvePhaseModel` + profile-resolver non-interference test (#4 SC5); `brief-validator` gate required uniformly in `/trekresearch` + `/trekexecute` (#12). diff --git a/agents/brief-reviewer.md b/agents/brief-reviewer.md index bd1f3b1..2d988c6 100644 --- a/agents/brief-reviewer.md +++ b/agents/brief-reviewer.md @@ -35,8 +35,12 @@ missing, vague, or contradictory. ## Input -You receive the path to a brief file (trekbrief v2.0 format, produced by -`/trekbrief`). Read it and evaluate its quality across five dimensions. +You receive the path to a brief file (trekbrief format, produced by +`/trekbrief`). Read it and evaluate its quality across six dimensions. + +The caller may also supply **operator memory context** (paths or excerpts of +`feedback_*` / `project_*` facts) in your prompt for the memory-alignment +dimension. If none is supplied, that dimension scores 5 (N/A) — see dimension 6. A brief has these sections (see template for full structure): - `## Intent` — why the work matters (load-bearing) @@ -148,6 +152,31 @@ Flag as **research-plan invalid** if: - `research_topics` count in frontmatter does not match section count - `research_status: complete` but research files are missing on disk +### 6. Memory alignment (NEW in v5.5) + +The brief is the pipeline's source of truth, but the operator's *real* intent +also lives in memory facts (`feedback_*`, `project_*`). When the two diverge, +downstream reasoning power polishes a wrong premise instead of challenging it. +This dimension is the second layer of the framing-alignment defense. + +The caller MAY supply operator memory excerpts or file paths in your prompt. + +- **If memory context IS supplied:** compare the brief — especially `## Intent`, + `## Goal`, and the frontmatter `framing:` value — against those facts. Report + **EXPLICIT contradictions only** (a brief claim that a memory fact directly + negates), never vibes or soft mismatches. +- **If NO memory context is supplied:** score `5`, set `contradictions: []`, and + note "no memory context supplied". Do not speculate or invent contradictions. + +Flag as **memory-misaligned** if: +- The frontmatter `framing:` value contradicts memory (e.g. `framing: preserve` + but memory records the prior direction was explicitly abandoned) +- `## Intent` or `## Goal` asserts a premise a memory fact directly negates +- A Constraint or Preference contradicts a recorded operator preference + +For each contradiction, capture: the brief claim (quoted), the memory fact +(quoted), and the source file. These feed `/trekbrief` Phase 4e follow-ups. + ## Rating Rate each dimension on two parallel scales: @@ -193,6 +222,7 @@ find it by reading the last `json` code fence. | Testability | {Pass/Weak/Fail} | {brief summary or "None"} | | Scope clarity | {Pass/Weak/Fail} | {brief summary or "None"} | | Research Plan | {Pass/Weak/Fail} | {brief summary or "None"} | +| Memory alignment | {Pass/Weak/Fail} | {brief summary, "None", or "N/A — no memory"} | ### Findings @@ -224,6 +254,12 @@ information that would strengthen the brief. List only if actionable.} { "topic": "{topic title}", "issue": "{what is missing or wrong}" } ] }, + "memory_alignment": { + "score": 1-5, + "contradictions": [ + { "brief_claim": "{quoted brief text}", "memory_fact": "{quoted memory fact}", "file": "{source file}" } + ] + }, "verdict": "PROCEED | PROCEED_WITH_RISKS | REVISE" } ``` diff --git a/commands/trekbrief.md b/commands/trekbrief.md index 7abeb6a..c9f02d8 100644 --- a/commands/trekbrief.md +++ b/commands/trekbrief.md @@ -99,6 +99,41 @@ If the directory already exists and is non-empty, warn and ask: Use `AskUserQuestion` with three options. If "pick new slug", ask for a new slug and restart Phase 2. +## Phase 2.5 — Framing declaration (v5.5) + +Before gathering ANY brief content, the operator MUST declare how this brief +relates to prior operator intent. This is the **first layer of the +framing-alignment defense** (CLAUDE.md cross-cutting invariant): the premise is +declared explicitly *before* the interview can drift, and long before `/trekplan` +can polish a wrong premise with reasoning power. + +**This runs BEFORE any brief prose is drafted.** The committed value is written to +brief frontmatter as `framing: ` in Step 4a and is REQUIRED for +`brief_version: "2.2"` (the validator emits `BRIEF_MISSING_FRAMING` otherwise). + +Ask via `AskUserQuestion` — one question, four canonical options: + +| Option | Maps to `framing:` | Meaning | +|--------|--------------------|---------| +| **Preserve** | `preserve` | Same intent as before; this brief continues a prior direction unchanged. | +| **Refine** | `refine` | Same core intent, sharpened or narrowed scope. | +| **Replace** | `replace` | Supersedes a prior brief's approach; intent re-stated from scratch. | +| **New direction** | `new-direction` | Net-new intent; no prior brief to anchor against. | + +Commit the answer to `state.framing` immediately. + +**No safe default.** `framing` cannot be guessed — the danger the invariant guards +against is exactly "delta from last is a dangerous default anchor after a rejected +iteration." Therefore **this question is asked even in `--quick` mode** and is the +one dialog that has no skip path. If the operator force-stops here, re-surface the +four options once more; the brief cannot be written at `brief_version: "2.2"` +without a committed framing value. + +Report: +``` +Framing: {preserve | refine | replace | new-direction} +``` + ## Phase 3 — Completeness loop Phase 3 is a **section-driven completeness loop**. Instead of a numbered @@ -424,7 +459,15 @@ Build the brief text from Phase 3 state by filling the template: - **Frontmatter:** populate `task`, `slug`, `project_dir`, `research_topics` (count of topics), `research_status: pending`, `auto_research: false` (will update in Phase 5 if user opts in), `interview_turns` (total - questions asked across Phase 3 + Phase 4), `source: interview`. + questions asked across Phase 3 + Phase 4), `source: interview`. Set + `brief_version: "2.2"` and `framing: ` (committed in Phase + 2.5 — never omit; the validator blocks a 2.2 brief without it). +- **TL;DR (v5.5, required at 2.2):** write a `## TL;DR` section (≤ 5 content + lines) at the very top of the body, before `## Intent`. It is the + framing-anchored one-glance summary — what the brief asks for and how it + relates to prior intent given `framing: `. Drafting it FIRST + forces the wrong-premise check before the rest of the prose is written. Keep + it to ≤ 5 lines (the validator warns with `BRIEF_TLDR_TOO_LONG` above that). - **Intent:** expand the user's motivation into 3–5 sentences. Load-bearing. - **Goal:** concrete end state. - **Non-Goals:** from state, or "- None explicitly stated" bullet if empty. @@ -446,12 +489,22 @@ final file is only written after the gate passes). **Step 4c — Launch brief-reviewer** +**Gather memory context first (v5.5, layer 2 of the framing defense).** If the +operator's environment exposes memory facts (e.g. an auto-memory `MEMORY.md` plus +`feedback_*` / `project_*` topic files), collect the paths or excerpts of those +relevant to this task. This is best-effort and environment-dependent: if no memory +is available, pass nothing — the reviewer scores the memory-alignment dimension +`5` (N/A) when no context is supplied. + Launch the `brief-reviewer` agent (foreground, blocking) with the prompt: > "Review this task brief for quality: `{PROJECT_DIR}/brief.md.draft`. -> Check completeness, consistency, testability, scope clarity, and -> research-plan validity. Report findings, verdict, and the required -> machine-readable JSON block." +> Check completeness, consistency, testability, scope clarity, +> research-plan validity, and memory alignment. Report findings, verdict, and +> the required machine-readable JSON block. +> Operator memory context (compare the brief's Intent/Goal and its declared +> `framing:` value against these for EXPLICIT contradictions only): +> {memory paths or excerpts, or "none supplied"}." **Step 4d — Parse JSON scores** @@ -460,12 +513,13 @@ Extract per-dimension scores: ``` review = { - completeness: { score, gaps }, - consistency: { score, issues }, - testability: { score, weak_criteria }, - scope_clarity: { score, unclear_sections }, - research_plan: { score, invalid_topics }, - verdict: "PROCEED | PROCEED_WITH_RISKS | REVISE" + completeness: { score, gaps }, + consistency: { score, issues }, + testability: { score, weak_criteria }, + scope_clarity: { score, unclear_sections }, + research_plan: { score, invalid_topics }, + memory_alignment:{ score, contradictions }, # v5.5 — layer 2 + verdict: "PROCEED | PROCEED_WITH_RISKS | REVISE" } ``` @@ -484,6 +538,10 @@ The gate **passes** when all of the following are true: - `testability.score ≥ 4` - `scope_clarity.score ≥ 4` - `research_plan.score == 5` +- `memory_alignment.score ≥ 4` (v5.5 — a score ≤ 3 means the reviewer found an + EXPLICIT contradiction between the brief and operator memory; this is a + framing-alignment blocker, not a wording nit. A `5` is also returned when no + memory context was supplied, so this never blocks environments without memory.) (Research Plan requires a perfect score because its format is checked mechanically: ends in `?`, `Required for plan steps` filled, scope is @@ -499,9 +557,11 @@ stumble.) **If gate fails AND iteration count < 3:** 1. Identify the weakest dimension (lowest score; tie broken by priority: - research_plan > testability > completeness > consistency > scope_clarity). + research_plan > memory_alignment > testability > completeness > + consistency > scope_clarity). 2. Generate a targeted follow-up question from the dimension's detail - field (gaps / issues / weak_criteria / unclear_sections / invalid_topics). + field (gaps / issues / weak_criteria / unclear_sections / invalid_topics / + contradictions). Example generators: - `completeness.gaps: ["Non-Goals empty, unclear if deliberate"]` → "You did not specify anything out-of-scope. Is that deliberate, or @@ -514,6 +574,10 @@ stumble.) → "For research topic 'JWT': which plan steps depend on the answer? Give one or two concrete kinds of step (e.g., 'library selection', 'threat model', 'migration strategy')." + - `memory_alignment.contradictions: [{"brief_claim":"continue the REST approach","memory_fact":"team decided to move to GraphQL","file":"project_api.md"}]` + → "Your brief's framing says 'preserve', but memory records the team + moved off REST to GraphQL. Is this brief intentionally reviving REST, + or should the framing be 'replace' / 'new-direction'?" 3. Ask via `AskUserQuestion`. Record the answer into Phase 3 state. 4. Return to Step 4a with incremented iteration count. The reviewer sees an updated draft, so you MUST re-read the brief and regenerate the diff --git a/docs/HANDOVER-CONTRACTS.md b/docs/HANDOVER-CONTRACTS.md index 4e2b47b..55e4350 100644 --- a/docs/HANDOVER-CONTRACTS.md +++ b/docs/HANDOVER-CONTRACTS.md @@ -10,7 +10,7 @@ Each artifact carries an explicit version field. Schema bumps are coordinated: | Artifact | Field | Current | |---|---|---| -| `brief.md` | `brief_version` (frontmatter) | `2.1` | +| `brief.md` | `brief_version` (frontmatter) | `2.2` | | `research/*.md` | (implicit; tracked via `type: trekresearch-brief`) | unversioned | | `plan.md` | `plan_version` (frontmatter) | `1.7` | | `progress.json` | `schema_version` (top-level) | `"1"` | @@ -43,7 +43,7 @@ Every validator exposes a CLI: `node lib/validators/.mjs --json ` re ## Handover 1 — `brief.md` → research/ (PUBLIC CONTRACT) -**Stability tier: PUBLIC CONTRACT.** Handover 1 is the *only* public integration boundary of the pipeline: `brief.md` is what an upstream producer hands to Voyage, and Voyage consumes it without any knowledge of who produced it. This asymmetry is a hard invariant (see `CLAUDE.md` §Trinity context) — the interactive `/trekbrief` interview is just one producer; a `manual` brief, or an external per-app / per-portfolio producer, is equally valid as long as the artifact conforms to the schema below. No producer is privileged. The consequence: changing this schema — renaming or removing a field, narrowing an enum, or promoting an optional field to required — is a **breaking change for every downstream consumer** and MUST follow the [breaking-change protocol](#breaking-change-protocol) (version bump + N-1 compatibility window). Additive *optional* fields are non-breaking by design: the validator tolerates unknown frontmatter keys silently (forward-compat), so a newer brief still validates against an older consumer. As of the v5.4 contract formalization, `brief_version` **2.1** is the frozen public-contract baseline. +**Stability tier: PUBLIC CONTRACT.** Handover 1 is the *only* public integration boundary of the pipeline: `brief.md` is what an upstream producer hands to Voyage, and Voyage consumes it without any knowledge of who produced it. This asymmetry is a hard invariant (see `CLAUDE.md` §Trinity context) — the interactive `/trekbrief` interview is just one producer; a `manual` brief, or an external per-app / per-portfolio producer, is equally valid as long as the artifact conforms to the schema below. No producer is privileged. The consequence: changing this schema — renaming or removing a field, narrowing an enum, or promoting an optional field to required — is a **breaking change for every downstream consumer** and MUST follow the [breaking-change protocol](#breaking-change-protocol) (version bump + N-1 compatibility window). Additive *optional* fields are non-breaking by design: the validator tolerates unknown frontmatter keys silently (forward-compat), so a newer brief still validates against an older consumer. The v5.4 contract formalization froze `brief_version` **2.1** as the public-contract baseline; **v5.5 evolves it to `2.2`** under the breaking-change protocol — adding the required `framing` field + `## TL;DR` section gated at ≥ 2.2 (existing 2.0/2.1 briefs stay valid). See the Versioning paragraph below for the 2.2 details. **Producer:** `/trekbrief` Phase 4g (after `brief-reviewer` stop-gate passes or iteration cap is hit). @@ -58,7 +58,7 @@ Every validator exposes a CLI: `node lib/validators/.mjs --json ` re | Field | Type | Required | Allowed values | Notes | |---|---|---|---|---| | `type` | string | yes | `trekbrief` | Hard-coded discriminator | -| `brief_version` | string | yes | `"2.1"` (current) | Bump on schema change | +| `brief_version` | string | yes | `"2.2"` (current) | Bump on schema change | | `created` | date | yes | YYYY-MM-DD | | | `task` | string | yes | one-line description | | | `slug` | string | yes | URL-safe slug | Used in project_dir | @@ -71,8 +71,10 @@ Every validator exposes a CLI: `node lib/validators/.mjs --json ` re | `brief_quality` | string | optional | `complete \| partial` | Set when iteration cap is hit | | `phase_signals` | list | optional (v5.1+) | list of `{phase, effort?, model?}` entries | Per-phase effort + model commitment from Phase 3.5. Mutually exclusive with `phase_signals_partial`. | | `phase_signals_partial` | bool | optional (v5.1+) | `true` | Force-stop record from Phase 3.5. Mutually exclusive with `phase_signals`. | +| `framing` | string | **required at ≥ 2.2** (v5.5) | `preserve \| refine \| replace \| new-direction` | How this brief relates to prior operator intent. Enum-checked on any version when present; missing → `BRIEF_MISSING_FRAMING` at `brief_version ≥ 2.2`. Layer 1 of the framing-alignment defense. | **Body invariants:** required sections (validator runs in strict mode at write-time, soft mode at read-time): +- `## TL;DR` — **required at `brief_version ≥ 2.2`** (v5.5); ≤ 5 content lines (soft cap → `BRIEF_TLDR_TOO_LONG` warning). Layer 3 of the framing-alignment defense. - `## Intent` - `## Goal` - `## Success Criteria` @@ -89,11 +91,14 @@ Optional but standard sections: `## Non-Goals`, `## Constraints`, `## Preference | Status enum | every read | `research_status ∈ allowed values` | | **State machine** | every read | `research_topics > 0 && research_status === "skipped"` requires `brief_quality === "partial"` | | **v5.1 sequencing gate** | every read | `brief_version ≥ 2.1` requires `phase_signals` (list) OR `phase_signals_partial: true` — error `BRIEF_V51_MISSING_SIGNALS` on miss. Validator-only enforcement; commands surface, don't re-enforce. | -| Body sections | strict only | All `BRIEF_BODY_SECTIONS` present | +| **v5.5 framing gate** | every read | `framing` enum-checked on any version when present (`BRIEF_INVALID_FRAMING`); at `brief_version ≥ 2.2` a missing `framing` → `BRIEF_MISSING_FRAMING` and a missing `## TL;DR` → `BRIEF_MISSING_SECTION` (strict) / warning (soft). `trekreview` briefs are exempt. | +| Body sections | strict only | All `BRIEF_BODY_SECTIONS` present (`## TL;DR` added at ≥ 2.2) | **State machine** detail: a brief that says it has research topics but skipped them must explicitly admit it (via `brief_quality: partial`). This is the most common failure mode the validator catches. -**Versioning:** current is `2.1` (v5.1 — adds optional `phase_signals` + `phase_signals_partial`). The forward-compat policy in `brief-validator.mjs` header still applies: unknown frontmatter keys flow through silently, so a `2.1` brief still validates against pre-v5.1 consumers. The version bump exists because v2.1 activates the **version-conditional sequencing gate** (above) — the only check in the validator that triggers on `brief_version` rather than field-presence. There are no live `1.x` briefs; remove legacy paths in next major. The v5.4 contract formalization **freezes this schema as the public-contract baseline** (see the PUBLIC CONTRACT callout under the Handover 1 heading): per the S3 effort-axis decision, `phase_signals` stays **optional**, and the v2.1 sequencing gate (`phase_signals` **or** `phase_signals_partial`) is the stability mechanism. Promoting `phase_signals` to required would be a future breaking change (→ `3.0`) under the protocol above — explicitly *not* part of v5.4. +**Versioning:** current is `2.1` (v5.1 — adds optional `phase_signals` + `phase_signals_partial`). The forward-compat policy in `brief-validator.mjs` header still applies: unknown frontmatter keys flow through silently, so a `2.1` brief still validates against pre-v5.1 consumers. The version bump exists because v2.1 activates the **version-conditional sequencing gate** (above) — the only check in the validator that triggers on `brief_version` rather than field-presence. There are no live `1.x` briefs; remove legacy paths in next major. The v5.4 contract formalization **froze `2.1` as the public-contract baseline** (see the PUBLIC CONTRACT callout under the Handover 1 heading): per the S3 effort-axis decision, `phase_signals` stays **optional**, and the v2.1 sequencing gate (`phase_signals` **or** `phase_signals_partial`) is the stability mechanism. Promoting `phase_signals` to required would be a future breaking change under the protocol above — explicitly *not* part of v5.4. + +**v5.5 → `2.2` (framing enforcement).** `2.2` adds two **required-at-2.2** elements — the `framing` enum field and the `## TL;DR` body section — gated identically to the v2.1 mechanism: the new requirements fire only on `brief_version ≥ 2.2`, so every existing `2.0` / `2.1` brief still validates (forward- and backward-compatible). Because adding required elements is a breaking change for any producer that declares `2.2`, this is a controlled version bump under the breaking-change protocol: downstream producers (e.g. a Tier-2 per-app producer) that emit `brief_version: "2.2"` MUST also emit `framing` + `## TL;DR`. The framing enum is additionally enforced on *any* version when the field is present (`BRIEF_INVALID_FRAMING`). The three framing-alignment layers (framing field, `brief-reviewer` memory-alignment dimension, obligatory TL;DR) implement the `CLAUDE.md` cross-cutting invariant. Note: the *plugin* version bump + CHANGELOG entry for this schema change land at the coordinated release (matrix §S10), separate from this schema axis. **Failure modes:** - `BRIEF_NOT_FOUND` → consumer halts with a usage message diff --git a/lib/validators/brief-validator.mjs b/lib/validators/brief-validator.mjs index de22278..118be50 100644 --- a/lib/validators/brief-validator.mjs +++ b/lib/validators/brief-validator.mjs @@ -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`, + )); + } + } + } } } diff --git a/templates/trekbrief-template.md b/templates/trekbrief-template.md index e0c2232..242e954 100644 --- a/templates/trekbrief-template.md +++ b/templates/trekbrief-template.md @@ -1,6 +1,6 @@ --- type: trekbrief -brief_version: "2.1" +brief_version: "2.2" created: {YYYY-MM-DD} task: "{one-line task description}" slug: {slug} @@ -10,6 +10,12 @@ research_status: pending # pending | in_progress | complete | skipped auto_research: false # true if user opted into Claude-managed research interview_turns: {N} source: {interview | manual} +# v5.5 — framing: how this brief relates to prior operator intent. REQUIRED at +# brief_version ≥ 2.2. One of: preserve | refine | replace | new-direction. +# AskUserQuestion-validated in /trekbrief Phase 2.5 BEFORE any brief prose is +# written — the first layer of the framing-alignment defense (guards against the +# plan polishing a wrong premise after a rejected iteration). +framing: {preserve | refine | replace | new-direction} # v5.1 — per-phase effort + model signal (Phase 3.5). # `effort` ∈ {low, standard, high}. Omit `model:` for `standard` so composition # falls through to profile resolver. Force-stop alternative is the commented @@ -33,6 +39,15 @@ phase_signals: > reads it to produce the implementation plan. Every decision in the plan must > trace back to content in this brief. +## TL;DR + +*≤ 5 lines. The framing-anchored one-glance summary: what this brief asks for and +how it relates to prior operator intent (framing: {preserve | refine | replace | +new-direction}). Written FIRST so a reader catches a wrong premise before reading +the full brief. Required at brief_version ≥ 2.2.* + +{≤5-line summary.} + ## Intent *Why are we doing this? What is the motivation, user need, or strategic context? diff --git a/tests/commands/trekbrief.test.mjs b/tests/commands/trekbrief.test.mjs index 0788f67..f06be06 100644 --- a/tests/commands/trekbrief.test.mjs +++ b/tests/commands/trekbrief.test.mjs @@ -21,6 +21,8 @@ import { parseDocument } from '../../lib/util/frontmatter.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..'); const COMMAND_FILE = join(ROOT, 'commands', 'trekbrief.md'); +const REVIEWER_FILE = join(ROOT, 'agents', 'brief-reviewer.md'); +const TEMPLATE_FILE = join(ROOT, 'templates', 'trekbrief-template.md'); const FIXTURE = (name) => join(ROOT, 'tests', 'fixtures', name); function read() { @@ -97,6 +99,62 @@ test('trekbrief — SC1: missing phase_signals + brief_version 2.1 triggers BRIE ); }); +// --- v5.5 — framing enforcement + TL;DR + memory-alignment prose-pins --- + +test('trekbrief — v5.5 Phase 2.5 framing declaration heading present', () => { + const text = read(); + assert.match(text, /^## Phase 2\.5 — Framing declaration/m, + 'Phase 2.5 framing-declaration heading missing from commands/trekbrief.md'); +}); + +test('trekbrief — v5.5 Phase 2.5 references all four framing values', () => { + const text = read(); + const start = text.indexOf('## Phase 2.5'); + const section = text.slice(start, text.indexOf('## Phase 3', start)); + for (const v of ['preserve', 'refine', 'replace', 'new-direction']) { + assert.ok(section.includes(v), `Phase 2.5 missing framing value "${v}"`); + } +}); + +test('trekbrief — v5.5 Phase 2.5 runs before any brief prose (precedes Phase 3)', () => { + const text = read(); + assert.ok(text.indexOf('## Phase 2.5') < text.indexOf('## Phase 3'), + 'Phase 2.5 must come before the completeness loop (before prose)'); + assert.ok(text.includes('even in `--quick` mode'), + 'framing must be non-skippable even in --quick mode'); +}); + +test('trekbrief — v5.5 Step 4a writes framing + brief_version 2.2 + generates TL;DR', () => { + const text = read(); + assert.ok(/brief_version: "2\.2"/.test(text), 'Step 4a must set brief_version 2.2'); + assert.ok(/framing: /.test(text), 'Step 4a must write the committed framing value'); + assert.ok(/## TL;DR/.test(text), 'Step 4a must generate the TL;DR section'); +}); + +test('trekbrief — v5.5 Phase 4e gate includes memory_alignment', () => { + const text = read(); + assert.ok(/memory_alignment\.score ≥ 4/.test(text), + 'Phase 4e gate must require memory_alignment.score ≥ 4'); +}); + +test('trekbrief — v5.5 brief-reviewer declares the memory-alignment dimension', () => { + const reviewer = readFileSync(REVIEWER_FILE, 'utf8'); + assert.match(reviewer, /### 6\. Memory alignment/, + 'brief-reviewer.md missing dimension 6 (memory alignment)'); + assert.ok(reviewer.includes('"memory_alignment"'), + 'brief-reviewer.md JSON schema missing memory_alignment key'); + assert.ok(/no memory context (is )?supplied/i.test(reviewer), + 'brief-reviewer must define the no-memory-context N/A fallback'); +}); + +test('trekbrief — v5.5 template carries framing field, 2.2, and TL;DR section', () => { + const tpl = readFileSync(TEMPLATE_FILE, 'utf8'); + assert.ok(/brief_version: "2\.2"/.test(tpl), 'template must declare brief_version 2.2'); + assert.match(tpl, /^framing: \{preserve \| refine \| replace \| new-direction\}/m, + 'template frontmatter must include the framing field'); + assert.match(tpl, /^## TL;DR$/m, 'template must include the ## TL;DR section'); +}); + test('trekbrief — SC1: phase_signals_partial: true does NOT trigger the gate', () => { const partial = `--- type: trekbrief diff --git a/tests/lib/doc-consistency.test.mjs b/tests/lib/doc-consistency.test.mjs index 224cbd6..f23bc76 100644 --- a/tests/lib/doc-consistency.test.mjs +++ b/tests/lib/doc-consistency.test.mjs @@ -559,10 +559,10 @@ test('operational files no longer reference trekrevise (v5.0.0 removal)', () => // --- v5.1 — phase_signals + brief_version 2.1 --- -test('v5.1 — templates/trekbrief-template.md declares brief_version: "2.1" (quoted)', () => { +test('v5.5 — templates/trekbrief-template.md declares brief_version: "2.2" (quoted)', () => { const t = read('templates/trekbrief-template.md'); - assert.match(t, /^brief_version: "2\.1"$/m, - 'trekbrief-template.md must declare brief_version: "2.1" (quoted) — unquoted parses as Number and bypasses sequencing gate'); + assert.match(t, /^brief_version: "2\.2"$/m, + 'trekbrief-template.md must declare brief_version: "2.2" (quoted) — unquoted parses as Number and bypasses sequencing gate; v5.5 bumped 2.1→2.2 for framing enforcement'); }); test('v5.1 — templates/trekbrief-template.md contains phase_signals: block', () => { @@ -641,18 +641,53 @@ test('v5.4 — Handover 1 declares the PUBLIC CONTRACT callout + breaking-for-do 'the callout must state that a brief-schema change is a breaking change for every downstream consumer'); }); -test('v5.4 — brief_version 2.1 is the frozen public-contract baseline (no stale 2.0)', () => { +test('v5.5 — brief_version 2.2 is current in the public-contract schema table (no stale 2.0/2.1)', () => { const t = read('docs/HANDOVER-CONTRACTS.md'); assert.ok(!t.includes('`"2.0"` (current)'), 'Handover 1 schema table must not still mark brief_version "2.0" as current (stale)'); - assert.ok(t.includes('`"2.1"` (current)'), - 'Handover 1 schema table must mark brief_version "2.1" as current'); + assert.ok(!t.includes('`"2.1"` (current)'), + 'Handover 1 schema table must not still mark brief_version "2.1" as current (stale — v5.5 bumped to 2.2)'); + assert.ok(t.includes('`"2.2"` (current)'), + 'Handover 1 schema table must mark brief_version "2.2" as current'); }); -test('v5.4 — phase_signals stays optional: v5.4 freezes, does not promote to required', () => { +test('v5.4 — phase_signals stays optional: not promoted to required', () => { const t = read('docs/HANDOVER-CONTRACTS.md'); assert.ok(!t.includes('v5.4 may promote'), 'the speculative "v5.4 may promote phase_signals to required" line must be resolved (S3 froze the shape)'); - assert.ok(t.includes('freezes this schema as the public-contract baseline'), - 'the Versioning note must record that v5.4 freezes the schema as the public-contract baseline'); + assert.ok(t.includes('froze `2.1` as the public-contract baseline'), + 'the Versioning note must record that v5.4 froze 2.1 as the public-contract baseline'); +}); + +// --- v5.5 — framing enforcement: brief_version 2.2 (Handover 1 contract evolution) --- +// S6: the framing-alignment invariant (CLAUDE.md) ships as a controlled brief_version +// bump 2.1→2.2 adding two required-at-2.2 elements (framing field + ## TL;DR section), +// version-gated so 2.0/2.1 briefs stay valid. These pin the contract-doc evolution. + +test('v5.5 — Handover 1 schema table adds the framing field row', () => { + const t = read('docs/HANDOVER-CONTRACTS.md'); + assert.ok(t.includes('| `framing` |'), + 'HANDOVER-CONTRACTS must add a framing row to the Handover 1 schema table'); + assert.ok(t.includes('preserve \\| refine \\| replace \\| new-direction'), + 'the framing row must list the four canonical enum values'); +}); + +test('v5.5 — Handover 1 documents the framing gate + required-at-2.2 TL;DR section', () => { + const t = read('docs/HANDOVER-CONTRACTS.md'); + assert.ok(t.includes('v5.5 framing gate'), + 'validation-strategy table must add a v5.5 framing gate row'); + assert.ok(/##\s*TL;DR.*required at `brief_version ≥ 2\.2`/.test(t) || t.includes('`## TL;DR` — **required at `brief_version ≥ 2.2`**'), + 'body-invariants must mark ## TL;DR as required at brief_version ≥ 2.2'); +}); + +test('v5.5 — Versioning note records the 2.2 framing-enforcement evolution', () => { + const t = read('docs/HANDOVER-CONTRACTS.md'); + assert.ok(t.includes('v5.5 → `2.2` (framing enforcement)'), + 'the Versioning note must document the v5.5 → 2.2 framing-enforcement evolution'); +}); + +test('v5.5 — voyage README.md mentions framing enforcement / brief_version 2.2', () => { + const t = read('README.md'); + assert.ok(t.includes('framing') && t.includes('brief_version 2.2'), + 'voyage README.md must carry a "What\'s new" note for framing enforcement (brief_version 2.2)'); }); diff --git a/tests/validators/brief-validator.test.mjs b/tests/validators/brief-validator.test.mjs index 69e250f..2732cd1 100644 --- a/tests/validators/brief-validator.test.mjs +++ b/tests/validators/brief-validator.test.mjs @@ -250,3 +250,111 @@ test('validateBrief — v5.1.1: UNQUOTED brief_version 2.1 WITH phase_signals is assert.equal(r.valid, true, JSON.stringify(r.errors)); assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS')); }); + +// --- v5.5 — framing enforcement + obligatory TL;DR (gated at brief_version ≥ 2.2) --- +// Operator decision (S6, option A1): framing + TL;DR are hard BLOCKERs for briefs +// declaring brief_version ≥ 2.2; existing 2.0/2.1 briefs stay valid (forward-compat, +// mirroring the phase_signals ≥ 2.1 precedent). The framing ENUM check fires on any +// version when the field is present but malformed. + +const GOOD_BRIEF_22 = `--- +type: trekbrief +brief_version: "2.2" +created: 2026-06-18 +task: "Add JWT auth to API" +slug: jwt-auth +project_dir: .claude/projects/2026-06-18-jwt-auth/ +research_topics: 0 +research_status: complete +auto_research: false +interview_turns: 5 +source: interview +framing: new-direction +phase_signals_partial: true +--- + +# Task: JWT auth + +## TL;DR + +Net-new JWT auth; no prior brief to anchor against. + +## Intent + +Why this matters. + +## Goal + +What success looks like. + +## Success Criteria + +- All tests pass. +`; + +test('validateBrief — v5.5 well-formed 2.2 brief (framing + TL;DR) accepted', () => { + const r = validateBriefContent(GOOD_BRIEF_22, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('validateBrief — v5.5 framing enum: invalid value rejected on any version', () => { + const t = GOOD_BRIEF.replace('source: interview\n', 'source: interview\nframing: sideways\n'); + const r = validateBriefContent(t, { strict: true }); + assert.equal(r.valid, false); + assert.ok(r.errors.find(e => e.code === 'BRIEF_INVALID_FRAMING')); +}); + +test('validateBrief — v5.5 framing enum: all four canonical values accepted', () => { + for (const v of ['preserve', 'refine', 'replace', 'new-direction']) { + const t = GOOD_BRIEF_22.replace('framing: new-direction', `framing: ${v}`); + const r = validateBriefContent(t, { strict: true }); + assert.equal(r.valid, true, `framing=${v}: ${JSON.stringify(r.errors)}`); + } +}); + +test('validateBrief — v5.5 brief_version 2.2 missing framing rejected (BRIEF_MISSING_FRAMING)', () => { + const t = GOOD_BRIEF_22.replace('framing: new-direction\n', ''); + const r = validateBriefContent(t, { strict: true }); + assert.equal(r.valid, false); + assert.ok(r.errors.find(e => e.code === 'BRIEF_MISSING_FRAMING')); +}); + +test('validateBrief — v5.5 brief_version 2.2 missing ## TL;DR rejected (strict)', () => { + const t = GOOD_BRIEF_22.replace(/## TL;DR\n\n[^\n]*\n\n/, ''); + const r = validateBriefContent(t, { strict: true }); + assert.equal(r.valid, false); + assert.ok(r.errors.find(e => e.code === 'BRIEF_MISSING_SECTION' && /TL;DR/.test(e.message))); +}); + +test('validateBrief — v5.5 brief_version 2.2 missing ## TL;DR demoted to warning (soft)', () => { + const t = GOOD_BRIEF_22.replace(/## TL;DR\n\n[^\n]*\n\n/, ''); + const r = validateBriefContent(t, { strict: false }); + assert.ok(r.warnings.find(w => w.code === 'BRIEF_MISSING_SECTION' && /TL;DR/.test(w.message))); +}); + +test('validateBrief — v5.5 TL;DR exceeding 5 lines emits BRIEF_TLDR_TOO_LONG warning', () => { + const longTldr = ['l1', 'l2', 'l3', 'l4', 'l5', 'l6'].join('\n'); + const t = GOOD_BRIEF_22.replace('Net-new JWT auth; no prior brief to anchor against.', longTldr); + const r = validateBriefContent(t, { strict: true }); + assert.ok( + r.warnings.find(w => w.code === 'BRIEF_TLDR_TOO_LONG'), + `expected TL;DR-too-long warning; warnings=${JSON.stringify(r.warnings)}`, + ); +}); + +test('validateBrief — v5.5 backward-compat: 2.1 brief without framing/TL;DR stays valid', () => { + const t = GOOD_BRIEF + .replace('brief_version: "2.0"', 'brief_version: "2.1"') + .replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n'); + const r = validateBriefContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); + assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_FRAMING')); + assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_SECTION' && /TL;DR/.test(e.message))); +}); + +test('validateBrief — v5.5 trekreview brief not subject to framing/TL;DR gate', () => { + const r = validateBriefContent(REVIEW_AS_BRIEF, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); + assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_FRAMING')); + assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_SECTION' && /TL;DR/.test(e.message))); +});