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
|
|
@ -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).
|
> **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
|
## Commands
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ Full flag reference for each command (modes, `--gates`, `--profile`, breaking ch
|
||||||
| git-historian | opus | Recent changes, ownership, hot files |
|
| git-historian | opus | Recent changes, ownership, hot files |
|
||||||
| research-scout | opus | External docs for unfamiliar tech (conditional, planning only) |
|
| research-scout | opus | External docs for unfamiliar tech (conditional, planning only) |
|
||||||
| convention-scanner | opus | Coding conventions: naming, style, error handling, test patterns |
|
| 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) |
|
| brief-conformance-reviewer | opus | Brief conformance review (SC + Non-Goal traceability) |
|
||||||
| code-correctness-reviewer | opus | Code correctness review (7 dimensions) |
|
| code-correctness-reviewer | opus | Code correctness review (7 dimensions) |
|
||||||
| review-coordinator | opus | Judge Agent — dedup + reasonableness filter + verdict |
|
| review-coordinator | opus | Judge Agent — dedup + reasonableness filter + verdict |
|
||||||
|
|
|
||||||
|
|
@ -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:
|
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:
|
> **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`.
|
> - **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).
|
> - **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).
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,12 @@ missing, vague, or contradictory.
|
||||||
|
|
||||||
## Input
|
## Input
|
||||||
|
|
||||||
You receive the path to a brief file (trekbrief v2.0 format, produced by
|
You receive the path to a brief file (trekbrief format, produced by
|
||||||
`/trekbrief`). Read it and evaluate its quality across five dimensions.
|
`/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):
|
A brief has these sections (see template for full structure):
|
||||||
- `## Intent` — why the work matters (load-bearing)
|
- `## 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_topics` count in frontmatter does not match section count
|
||||||
- `research_status: complete` but research files are missing on disk
|
- `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
|
## Rating
|
||||||
|
|
||||||
Rate each dimension on two parallel scales:
|
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"} |
|
| Testability | {Pass/Weak/Fail} | {brief summary or "None"} |
|
||||||
| Scope clarity | {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"} |
|
| Research Plan | {Pass/Weak/Fail} | {brief summary or "None"} |
|
||||||
|
| Memory alignment | {Pass/Weak/Fail} | {brief summary, "None", or "N/A — no memory"} |
|
||||||
|
|
||||||
### Findings
|
### Findings
|
||||||
|
|
||||||
|
|
@ -224,6 +254,12 @@ information that would strengthen the brief. List only if actionable.}
|
||||||
{ "topic": "{topic title}", "issue": "{what is missing or wrong}" }
|
{ "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"
|
"verdict": "PROCEED | PROCEED_WITH_RISKS | REVISE"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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
|
Use `AskUserQuestion` with three options. If "pick new slug", ask for a
|
||||||
new slug and restart Phase 2.
|
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: <value>` 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 — Completeness loop
|
||||||
|
|
||||||
Phase 3 is a **section-driven completeness loop**. Instead of a numbered
|
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`
|
- **Frontmatter:** populate `task`, `slug`, `project_dir`, `research_topics`
|
||||||
(count of topics), `research_status: pending`, `auto_research: false`
|
(count of topics), `research_status: pending`, `auto_research: false`
|
||||||
(will update in Phase 5 if user opts in), `interview_turns` (total
|
(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: <state.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: <state.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.
|
- **Intent:** expand the user's motivation into 3–5 sentences. Load-bearing.
|
||||||
- **Goal:** concrete end state.
|
- **Goal:** concrete end state.
|
||||||
- **Non-Goals:** from state, or "- None explicitly stated" bullet if empty.
|
- **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**
|
**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:
|
Launch the `brief-reviewer` agent (foreground, blocking) with the prompt:
|
||||||
|
|
||||||
> "Review this task brief for quality: `{PROJECT_DIR}/brief.md.draft`.
|
> "Review this task brief for quality: `{PROJECT_DIR}/brief.md.draft`.
|
||||||
> Check completeness, consistency, testability, scope clarity, and
|
> Check completeness, consistency, testability, scope clarity,
|
||||||
> research-plan validity. Report findings, verdict, and the required
|
> research-plan validity, and memory alignment. Report findings, verdict, and
|
||||||
> machine-readable JSON block."
|
> 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**
|
**Step 4d — Parse JSON scores**
|
||||||
|
|
||||||
|
|
@ -460,12 +513,13 @@ Extract per-dimension scores:
|
||||||
|
|
||||||
```
|
```
|
||||||
review = {
|
review = {
|
||||||
completeness: { score, gaps },
|
completeness: { score, gaps },
|
||||||
consistency: { score, issues },
|
consistency: { score, issues },
|
||||||
testability: { score, weak_criteria },
|
testability: { score, weak_criteria },
|
||||||
scope_clarity: { score, unclear_sections },
|
scope_clarity: { score, unclear_sections },
|
||||||
research_plan: { score, invalid_topics },
|
research_plan: { score, invalid_topics },
|
||||||
verdict: "PROCEED | PROCEED_WITH_RISKS | REVISE"
|
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`
|
- `testability.score ≥ 4`
|
||||||
- `scope_clarity.score ≥ 4`
|
- `scope_clarity.score ≥ 4`
|
||||||
- `research_plan.score == 5`
|
- `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
|
(Research Plan requires a perfect score because its format is checked
|
||||||
mechanically: ends in `?`, `Required for plan steps` filled, scope is
|
mechanically: ends in `?`, `Required for plan steps` filled, scope is
|
||||||
|
|
@ -499,9 +557,11 @@ stumble.)
|
||||||
|
|
||||||
**If gate fails AND iteration count < 3:**
|
**If gate fails AND iteration count < 3:**
|
||||||
1. Identify the weakest dimension (lowest score; tie broken by priority:
|
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
|
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:
|
Example generators:
|
||||||
- `completeness.gaps: ["Non-Goals empty, unclear if deliberate"]`
|
- `completeness.gaps: ["Non-Goals empty, unclear if deliberate"]`
|
||||||
→ "You did not specify anything out-of-scope. Is that deliberate, or
|
→ "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?
|
→ "For research topic 'JWT': which plan steps depend on the answer?
|
||||||
Give one or two concrete kinds of step (e.g., 'library selection',
|
Give one or two concrete kinds of step (e.g., 'library selection',
|
||||||
'threat model', 'migration strategy')."
|
'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.
|
3. Ask via `AskUserQuestion`. Record the answer into Phase 3 state.
|
||||||
4. Return to Step 4a with incremented iteration count. The reviewer sees
|
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
|
an updated draft, so you MUST re-read the brief and regenerate the
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ Each artifact carries an explicit version field. Schema bumps are coordinated:
|
||||||
|
|
||||||
| Artifact | Field | Current |
|
| 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 |
|
| `research/*.md` | (implicit; tracked via `type: trekresearch-brief`) | unversioned |
|
||||||
| `plan.md` | `plan_version` (frontmatter) | `1.7` |
|
| `plan.md` | `plan_version` (frontmatter) | `1.7` |
|
||||||
| `progress.json` | `schema_version` (top-level) | `"1"` |
|
| `progress.json` | `schema_version` (top-level) | `"1"` |
|
||||||
|
|
@ -43,7 +43,7 @@ Every validator exposes a CLI: `node lib/validators/<name>.mjs --json <path>` re
|
||||||
|
|
||||||
## Handover 1 — `brief.md` → research/ (PUBLIC CONTRACT)
|
## 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).
|
**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/<name>.mjs --json <path>` re
|
||||||
| Field | Type | Required | Allowed values | Notes |
|
| Field | Type | Required | Allowed values | Notes |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `type` | string | yes | `trekbrief` | Hard-coded discriminator |
|
| `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 | |
|
| `created` | date | yes | YYYY-MM-DD | |
|
||||||
| `task` | string | yes | one-line description | |
|
| `task` | string | yes | one-line description | |
|
||||||
| `slug` | string | yes | URL-safe slug | Used in project_dir |
|
| `slug` | string | yes | URL-safe slug | Used in project_dir |
|
||||||
|
|
@ -71,8 +71,10 @@ Every validator exposes a CLI: `node lib/validators/<name>.mjs --json <path>` re
|
||||||
| `brief_quality` | string | optional | `complete \| partial` | Set when iteration cap is hit |
|
| `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` | 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`. |
|
| `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):
|
**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`
|
- `## Intent`
|
||||||
- `## Goal`
|
- `## Goal`
|
||||||
- `## Success Criteria`
|
- `## Success Criteria`
|
||||||
|
|
@ -89,11 +91,14 @@ Optional but standard sections: `## Non-Goals`, `## Constraints`, `## Preference
|
||||||
| Status enum | every read | `research_status ∈ allowed values` |
|
| Status enum | every read | `research_status ∈ allowed values` |
|
||||||
| **State machine** | every read | `research_topics > 0 && research_status === "skipped"` requires `brief_quality === "partial"` |
|
| **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. |
|
| **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.
|
**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:**
|
**Failure modes:**
|
||||||
- `BRIEF_NOT_FOUND` → consumer halts with a usage message
|
- `BRIEF_NOT_FOUND` → consumer halts with a usage message
|
||||||
|
|
|
||||||
|
|
@ -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 BRIEF_BODY_SECTIONS = ['Intent', 'Goal', 'Success Criteria'];
|
||||||
export const PHASE_SIGNAL_PHASES = Object.freeze(['research', 'plan', 'execute', 'review']);
|
export const PHASE_SIGNAL_PHASES = Object.freeze(['research', 'plan', 'execute', 'review']);
|
||||||
export const EFFORT_LEVELS = Object.freeze(['low', 'standard', 'high']);
|
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) {
|
function getRequiredFields(type) {
|
||||||
return type === 'trekreview' ? REVIEW_AS_BRIEF_REQUIRED_FRONTMATTER : BRIEF_REQUIRED_FRONTMATTER;
|
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
|
// 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
|
// 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).
|
// 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') {
|
if (typeof fm.brief_version === 'string' || typeof fm.brief_version === 'number') {
|
||||||
const vm = String(fm.brief_version).match(/^(\d+)\.(\d+)$/);
|
const vm = String(fm.brief_version).match(/^(\d+)\.(\d+)$/);
|
||||||
if (vm) {
|
if (vm) {
|
||||||
const major = Number(vm[1]);
|
const major = Number(vm[1]);
|
||||||
const minor = Number(vm[2]);
|
const minor = Number(vm[2]);
|
||||||
const atLeast21 = major > 2 || (major === 2 && minor >= 1);
|
const atLeast21 = major > 2 || (major === 2 && minor >= 1);
|
||||||
|
const atLeast22 = major > 2 || (major === 2 && minor >= 2);
|
||||||
if (atLeast21 && !hasSignals && !hasPartial && fm.type !== 'trekreview') {
|
if (atLeast21 && !hasSignals && !hasPartial && fm.type !== 'trekreview') {
|
||||||
errors.push(issue(
|
errors.push(issue(
|
||||||
'BRIEF_V51_MISSING_SIGNALS',
|
'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.',
|
'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`,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
type: trekbrief
|
type: trekbrief
|
||||||
brief_version: "2.1"
|
brief_version: "2.2"
|
||||||
created: {YYYY-MM-DD}
|
created: {YYYY-MM-DD}
|
||||||
task: "{one-line task description}"
|
task: "{one-line task description}"
|
||||||
slug: {slug}
|
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
|
auto_research: false # true if user opted into Claude-managed research
|
||||||
interview_turns: {N}
|
interview_turns: {N}
|
||||||
source: {interview | manual}
|
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).
|
# v5.1 — per-phase effort + model signal (Phase 3.5).
|
||||||
# `effort` ∈ {low, standard, high}. Omit `model:` for `standard` so composition
|
# `effort` ∈ {low, standard, high}. Omit `model:` for `standard` so composition
|
||||||
# falls through to profile resolver. Force-stop alternative is the commented
|
# 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
|
> reads it to produce the implementation plan. Every decision in the plan must
|
||||||
> trace back to content in this brief.
|
> 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
|
## Intent
|
||||||
|
|
||||||
*Why are we doing this? What is the motivation, user need, or strategic context?
|
*Why are we doing this? What is the motivation, user need, or strategic context?
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
||||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
const ROOT = join(HERE, '..', '..');
|
const ROOT = join(HERE, '..', '..');
|
||||||
const COMMAND_FILE = join(ROOT, 'commands', 'trekbrief.md');
|
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);
|
const FIXTURE = (name) => join(ROOT, 'tests', 'fixtures', name);
|
||||||
|
|
||||||
function read() {
|
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: <state\.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', () => {
|
test('trekbrief — SC1: phase_signals_partial: true does NOT trigger the gate', () => {
|
||||||
const partial = `---
|
const partial = `---
|
||||||
type: trekbrief
|
type: trekbrief
|
||||||
|
|
|
||||||
|
|
@ -559,10 +559,10 @@ test('operational files no longer reference trekrevise (v5.0.0 removal)', () =>
|
||||||
|
|
||||||
// --- v5.1 — phase_signals + brief_version 2.1 ---
|
// --- 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');
|
const t = read('templates/trekbrief-template.md');
|
||||||
assert.match(t, /^brief_version: "2\.1"$/m,
|
assert.match(t, /^brief_version: "2\.2"$/m,
|
||||||
'trekbrief-template.md must declare brief_version: "2.1" (quoted) — unquoted parses as Number and bypasses sequencing gate');
|
'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', () => {
|
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');
|
'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');
|
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||||||
assert.ok(!t.includes('`"2.0"` (current)'),
|
assert.ok(!t.includes('`"2.0"` (current)'),
|
||||||
'Handover 1 schema table must not still mark brief_version "2.0" as current (stale)');
|
'Handover 1 schema table must not still mark brief_version "2.0" as current (stale)');
|
||||||
assert.ok(t.includes('`"2.1"` (current)'),
|
assert.ok(!t.includes('`"2.1"` (current)'),
|
||||||
'Handover 1 schema table must mark brief_version "2.1" as 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');
|
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||||||
assert.ok(!t.includes('v5.4 may promote'),
|
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)');
|
'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'),
|
assert.ok(t.includes('froze `2.1` as the public-contract baseline'),
|
||||||
'the Versioning note must record that v5.4 freezes the schema 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)');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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.equal(r.valid, true, JSON.stringify(r.errors));
|
||||||
assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
|
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)));
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue