feat(voyage): S18 — framing-hardening (min-version gate + memory status + pre-2.2 doc + flagship hedge)

Closes devils-advocate audit #4–#6 (MAJOR×MED). Four additive, non-breaking
changes to the v5.5 framing-alignment machinery:

1. --min-brief-version <ver> gate (audit #4). Opt-in version floor on
   /trekplan + /trekresearch, forwarded to brief-validator as --min-version.
   New opts.minBriefVersion warns BRIEF_VERSION_BELOW_MINIMUM (never blocks)
   when a brief declares a version below the floor; trekreview exempt; absent
   opt = no check. CLI shim parses --min-version and skips its value token in
   filePath detection.

2. memory_alignment.status field (audit #5). brief-reviewer now emits
   status: verified | n_a | contradictions so a score-5 N/A (no memory) is
   distinguishable from a score-5 verified-aligned brief — the score≥4 gate
   passes in both, status reveals whether the wrong-premise defense ran.

3. Document pre-2.2 = zero framing enforcement (audit #4). HANDOVER-CONTRACTS
   §Handover 1 now states the producer-elective hole + two remedies. Also
   fixes a stale "current is 2.1" line (current is 2.2).

4. Soften flagship overselling (audit #6). CLAUDE.md Context-Engineering
   principle now hedges that main-context relief is asserted-by-design, not
   measured (T1 PoC found Δ≈0); README carried no false claim to fix.

TDD: 8 new tests written failing-first (5 validator, 1 trekbrief status pin,
2 doc-consistency cross-file pins). Suite 691→699 (697 pass / 2 skip / 0 fail).
No flagship prose-pin added (deliberate, per S19 anti-bloat).

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:
Kjell Tore Guttormsen 2026-06-19 13:33:00 +02:00
commit 987e847ea1
11 changed files with 145 additions and 13 deletions

View file

@ -13,8 +13,9 @@ The coordinated release held since the framing work. Supersedes the unreleased i
Three version-gated layers ship at `brief_version ≥ 2.2`, implementing the `CLAUDE.md` cross-cutting invariant "brief framing must match operator intent":
- **`framing` enum** (`preserve | refine | replace | new-direction`) — required in frontmatter, collected in `/trekbrief` Phase 2.5 *before* any prose is written, non-skippable even in `--quick`. Enum-checked on any version (`BRIEF_INVALID_FRAMING`); missing at ≥ 2.2 → `BRIEF_MISSING_FRAMING`.
- **Memory-alignment dimension** — new `brief-reviewer` dimension 6 compares brief Intent/Goal + framing against operator memory, flagging *explicit* contradictions only (scores N/A when no memory is supplied); wired to the Phase 4e gate (`memory_alignment.score ≥ 4`).
- **Memory-alignment dimension** — new `brief-reviewer` dimension 6 compares brief Intent/Goal + framing against operator memory, flagging *explicit* contradictions only (scores N/A when no memory is supplied); wired to the Phase 4e gate (`memory_alignment.score ≥ 4`). Emits a `status` field (`verified | n_a | contradictions`) so a score-5 N/A (no memory available) is distinguishable from a score-5 verified-aligned brief — the gate passes in both cases, and `status` is what tells the operator whether the wrong-premise defense actually ran.
- **Obligatory `## TL;DR`** — ≤ 5 content lines at the top of every 2.2 brief (soft cap → `BRIEF_TLDR_TOO_LONG`).
- **Opt-in version floor**`--min-brief-version <ver>` on `/trekplan` + `/trekresearch` (forwarded to the validator as `--min-version`) raises a `BRIEF_VERSION_BELOW_MINIMUM` *warning* — never a block — when a brief declares a version below the floor. Closes the producer-elective hole where a `2.0`/`2.1` brief silently sidesteps framing enforcement; `docs/HANDOVER-CONTRACTS.md` §Handover 1 now documents pre-2.2 = zero framing enforcement and the two remedies (require `2.2` upstream, or pass the floor flag).
Existing `2.0`/`2.1` briefs stay valid (forward + backward compatible), mirroring the `phase_signals ≥ 2.1` precedent. `trekreview` briefs are exempt.

View file

@ -2,7 +2,7 @@
Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execute, review, continue. Deep implementation planning and research with specialized agent swarms, external research, adversarial review, session decomposition, disciplined execution, and headless support.
**Design principle: Context Engineering** — build the right context by orchestrating specialized agents. Each step in the pipeline (brief → research → plan → execute) produces a structured artifact that the next step consumes.
**Design principle: Context Engineering** — build the right context by orchestrating specialized agents. Each step in the pipeline (brief → research → plan → execute) produces a structured artifact that the next step consumes. (The claim that fanning out to a swarm *relieves* the main context is asserted-by-design, not measured end-to-end: the one delegation actually measured — the Phase-7 synthesis PoC — found Δ main-context ≈ 0, see `docs/T1-synthesis-poc-results.md`. The structured-artifact handoffs are the load-bearing benefit; main-context relief is not yet demonstrated.)
> **v3.0.0 — architect step extracted from this plugin.** The plan command still auto-discovers `architecture/overview.md` if present, so any compatible producer (architect plugin no longer publicly distributed; the architecture/overview.md slot remains available for any compatible producer) plugs into the same slot. See [CHANGELOG.md](CHANGELOG.md) for migration history.

View file

@ -164,9 +164,17 @@ 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.
negates), never vibes or soft mismatches. With no contradiction found, set
`status: "verified"`; with one or more, set `status: "contradictions"`.
- **If NO memory context is supplied:** score `5`, set `contradictions: []`,
`status: "n_a"`, and note "no memory context supplied". Do not speculate or
invent contradictions.
The `status` field is load-bearing: a `score: 5` alone is ambiguous because it is
emitted both when memory was checked and aligned (`verified`) and when no memory
was available to check (`n_a`). Downstream gates that read `memory_alignment.score
≥ 4` therefore pass in *both* cases — `status` is what tells the operator whether
the wrong-premise defense actually ran or was simply absent.
Flag as **memory-misaligned** if:
- The frontmatter `framing:` value contradicts memory (e.g. `framing: preserve`
@ -256,6 +264,7 @@ information that would strengthen the brief. List only if actionable.}
},
"memory_alignment": {
"score": 1-5,
"status": "verified | n_a | contradictions",
"contradictions": [
{ "brief_claim": "{quoted brief text}", "memory_fact": "{quoted memory fact}", "file": "{source file}" }
]

View file

@ -63,8 +63,11 @@ Parse `$ARGUMENTS` for mode flags. Order of precedence:
- Set **project_dir = {dir}**, **brief_path = {dir}/brief.md**.
- **Validate inputs** (soft mode — warnings do not block, errors do):
```bash
# Brief schema sanity check (frontmatter + state machine, soft on body sections)
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md"
# Brief schema sanity check (frontmatter + state machine, soft on body sections).
# When --min-brief-version was passed, append --min-version {min_brief_version}
# so the validator emits BRIEF_VERSION_BELOW_MINIMUM (warn, never block) for an
# older brief that sidesteps framing enforcement.
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json [--min-version {min_brief_version}] "{dir}/brief.md"
# v5.1.1 — resolve per-phase brief-signal for plan phase. Result is
# captured as phase_signal_result and used at Agent-spawn sites below
@ -120,7 +123,15 @@ Parse `$ARGUMENTS` for mode flags. Order of precedence:
autonomy-gate state machine via the CLI shim:
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`.
9. If neither `--brief` nor `--project` is present after flag parsing,
9. **`--min-brief-version <ver>`** — (optional) version floor. Set
**min_brief_version = {ver}** (e.g. `2.2`). Forwarded to the brief-validator
as `--min-version {ver}`; when the brief declares an older version it emits a
`BRIEF_VERSION_BELOW_MINIMUM` **warning** (never blocks) — framing enforcement
only fires at `≥ 2.2`, so an older brief sidesteps the framing-alignment
defense silently. Absent → no version check. See `docs/HANDOVER-CONTRACTS.md`
§Handover 1 for the pre-2.2 enforcement hole this guards.
10. If neither `--brief` nor `--project` is present after flag parsing,
output usage and stop:
```
@ -140,6 +151,7 @@ Modes:
--research Add up to 3 extra research briefs as planning context
--fg No-op alias (foreground is the only mode as of v2.4.0)
--quick Skip exploration agent swarm; plan directly
--min-brief-version <ver> Warn (never block) if the brief is older than <ver> (e.g. 2.2)
--export Generate shareable output from an existing plan (no new planning)
--decompose Split an existing plan into self-contained headless sessions

View file

@ -60,7 +60,9 @@ Supported flags:
at Agent-spawn sites in Phase 4 to inject the brief-resolved model:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md"
# When --min-brief-version was passed, append --min-version {min_brief_version}
# so an older brief raises BRIEF_VERSION_BELOW_MINIMUM (warn, never block).
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json [--min-version {min_brief_version}] "{dir}/brief.md"
node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/phase-signal-resolver.mjs --brief "{dir}/brief.md" --phase research --json
```
@ -71,6 +73,13 @@ Supported flags:
state machine via the CLI shim:
`node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --state X --event Y --gates {true|false}`.
7. `--min-brief-version <ver>` — (optional) version floor for an attached
`--project` brief. Set **min_brief_version = {ver}** (e.g. `2.2`). Forwarded
to the brief-validator below as `--min-version {ver}`; an older brief emits a
`BRIEF_VERSION_BELOW_MINIMUM` **warning** (never blocks) because framing
enforcement only fires at `≥ 2.2`. Absent → no version check. See
`docs/HANDOVER-CONTRACTS.md` §Handover 1 for the pre-2.2 enforcement hole.
Flags can be combined:
- `--local` — local-only research
- `--external --quick` — external-only, lightweight

View file

@ -96,10 +96,12 @@ Optional but standard sections: `## Non-Goals`, `## Constraints`, `## Preference
**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.5.0 contract formalization **established `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 the v5.5.0 formalization.
**Versioning:** the `2.1` schema axis (v5.1 — adds optional `phase_signals` + `phase_signals_partial`; current is `2.2`, see the next paragraph). 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.5.0 contract formalization **established `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 the v5.5.0 formalization.
**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.
**Pre-2.2 briefs receive zero framing enforcement (producer-elective defense).** The framing-alignment defense is gated at `brief_version ≥ 2.2`, so a brief declaring `2.0` or `2.1` gets **none** of it: `framing` is optional and a missing value is not an error, the `## TL;DR` section is not required, and the `brief-reviewer` memory-alignment dimension scores N/A. The backward-compatibility this buys is deliberate, but it also means the framing defense is **producer-elective**: any producer can sidestep the entire defense — deliberately or by shipping a legacy template — simply by declaring `brief_version: "2.1"`, and the pipeline raises no signal by default. This is a documented property of the contract, not a bug. Two remedies close it for a given consumer: (1) require upstream producers to emit `brief_version: "2.2"` (which forces `framing` + `## TL;DR`); or (2) pass the opt-in **`--min-brief-version <ver>`** flag to `/trekplan` or `/trekresearch` (forwarded to the validator as `--min-version`), which raises a `BRIEF_VERSION_BELOW_MINIMUM` **warning** — never a block — when a brief declares a version below the floor. The flag defaults to off, so the N-1 compatibility window for `2.0`/`2.1` briefs is preserved unless a consumer explicitly opts into the stricter floor.
**Failure modes:**
- `BRIEF_NOT_FOUND` → consumer halts with a usage message
- `FM_MISSING` → file has no frontmatter; halt

View file

@ -24,6 +24,7 @@ Always interactive. Phase 3 is a section-driven completeness loop (no hard cap o
| `--external` | Only external research agents (skip codebase analysis) |
| `--fg` | No-op alias (foreground is default since v2.4.0) |
| `--gates {open\|closed\|adaptive}` | (v3.4.0) Autonomy-checkpoint policy. Default `adaptive` |
| `--min-brief-version <ver>` | (S18) Warn — never block — if an attached `--project` brief declares a version below `<ver>` (e.g. `2.2`), i.e. sidesteps framing enforcement |
| `--profile <name>` | (v4.1.0) Model profile for the research phase. |
Flags combine: `--project <dir> --local`, `--external --quick`.
@ -37,6 +38,7 @@ Flags combine: `--project <dir> --local`, `--external --quick`.
| `--research <brief> [brief2]` | Enrich with extra research briefs beyond what is in `{project_dir}/research/` |
| `--fg` | No-op alias (foreground is default since v2.4.0) |
| `--quick` | Plan directly (no agent swarm) |
| `--min-brief-version <ver>` | (S18) Warn — never block — if the brief declares a version below `<ver>` (e.g. `2.2`), i.e. sidesteps framing enforcement |
| `--export <pr\|issue\|markdown\|headless> <plan>` | Generate shareable output from existing plan |
| `--decompose <plan>` | Split plan into self-contained headless sessions |
| `--gates {open\|closed\|adaptive}` | (v3.4.0) Autonomy-checkpoint policy. Default `adaptive` |

View file

@ -155,6 +155,32 @@ export function validateBriefContent(text, opts = {}) {
}
}
// S18 — opt-in version floor. When the caller passes minBriefVersion (commands
// forward --min-brief-version), WARN (never block) if the brief declares a
// version below it: framing enforcement only fires at ≥ 2.2, so an older brief
// sidesteps the framing-alignment defense silently. Absent opt → no check.
// trekreview briefs have no framing and are exempt.
if (opts.minBriefVersion && fm.type !== 'trekreview') {
const mm = String(opts.minBriefVersion).match(/^(\d+)\.(\d+)$/);
if (mm) {
const minMajor = Number(mm[1]);
const minMinor = Number(mm[2]);
const bm = fm.brief_version === undefined
? null
: String(fm.brief_version).match(/^(\d+)\.(\d+)$/);
const below = bm
? (Number(bm[1]) < minMajor || (Number(bm[1]) === minMajor && Number(bm[2]) < minMinor))
: true; // absent or unparseable version is below any floor
if (below) {
warnings.push(issue(
'BRIEF_VERSION_BELOW_MINIMUM',
`brief_version ${fm.brief_version ?? '(absent)'} is below the requested minimum ${opts.minBriefVersion} — framing enforcement (≥ 2.2) is bypassed`,
'Re-run /trekbrief to produce a brief_version 2.2 brief, or drop --min-brief-version to accept the older brief as-is.',
));
}
}
}
if (fm.type !== undefined && !BRIEF_TYPE_VALUES.includes(fm.type)) {
errors.push(issue(
'BRIEF_WRONG_TYPE',
@ -223,12 +249,15 @@ export function validateBrief(filePath, opts = {}) {
if (import.meta.url === `file://${process.argv[1]}`) {
const args = process.argv.slice(2);
const strict = !args.includes('--soft');
const filePath = args.find(a => !a.startsWith('--'));
const minIdx = args.indexOf('--min-version');
const minBriefVersion = minIdx >= 0 ? args[minIdx + 1] : undefined;
// filePath is the first positional, skipping the --min-version value token.
const filePath = args.find((a, i) => !a.startsWith('--') && i !== minIdx + 1);
if (!filePath) {
process.stderr.write('Usage: brief-validator.mjs [--soft] <brief.md>\n');
process.stderr.write('Usage: brief-validator.mjs [--soft] [--min-version <x.y>] <brief.md>\n');
process.exit(2);
}
const r = validateBrief(filePath, { strict });
const r = validateBrief(filePath, { strict, minBriefVersion });
if (args.includes('--json')) {
process.stdout.write(JSON.stringify({ valid: r.valid, errors: r.errors, warnings: r.warnings }, null, 2) + '\n');
} else {

View file

@ -147,6 +147,12 @@ test('trekbrief — v5.5 brief-reviewer declares the memory-alignment dimension'
'brief-reviewer must define the no-memory-context N/A fallback');
});
test('trekbrief — S18 brief-reviewer memory_alignment carries a status field', () => {
const reviewer = readFileSync(REVIEWER_FILE, 'utf8');
assert.ok(reviewer.includes('"status": "verified | n_a | contradictions"'),
'memory_alignment must emit a status enum (verified | n_a | contradictions) so a score-5 N/A (no memory) is distinguishable from a score-5 verified-aligned brief');
});
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');

View file

@ -869,3 +869,30 @@ test('S17: NW2 bake-off verdict is not labeled unqualified POSITIVE (raw data un
'T2-bakeoff-results.md must disclose the raw per-run data is un-archived / un-reproducible.',
);
});
// ── S18: framing-hardening cross-file invariants ───────────────────────────
test('S18: --min-brief-version is documented at the trekplan + trekresearch boundary', () => {
// The opt-in version floor must be discoverable wherever flags live: both
// consuming commands and the central flag reference.
for (const f of ['commands/trekplan.md', 'commands/trekresearch.md', 'docs/command-modes.md']) {
assert.ok(
read(f).includes('--min-brief-version'),
`${f} must document the --min-brief-version flag (S18 framing-hardening)`,
);
}
});
test('S18: HANDOVER-CONTRACTS documents the pre-2.2 zero-framing-enforcement hole', () => {
// The framing defense is producer-elective: a brief declaring ≤ 2.1 sidesteps
// it entirely. Handover 1 (PUBLIC CONTRACT) must disclose this and name the remedy.
const h = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(
/BRIEF_VERSION_BELOW_MINIMUM/.test(h),
'HANDOVER-CONTRACTS.md must name the BRIEF_VERSION_BELOW_MINIMUM gate as the pre-2.2 remedy',
);
assert.ok(
/zero framing enforcement|no framing enforcement|sidestep/i.test(h),
'HANDOVER-CONTRACTS.md must state that pre-2.2 briefs receive zero framing enforcement',
);
});

View file

@ -358,3 +358,38 @@ test('validateBrief — v5.5 trekreview brief not subject to framing/TL;DR gate'
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)));
});
// S18 — opt-in --min-brief-version gate. Warns (never blocks) when a brief
// declares a version below the caller's floor, sidestepping framing enforcement.
const BRIEF_21 = GOOD_BRIEF
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
.replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n');
test('validateBrief — S18 min-version: older brief below floor warns but does not block', () => {
const r = validateBriefContent(BRIEF_21, { strict: true, minBriefVersion: '2.2' });
assert.equal(r.valid, true, `must warn, not block: ${JSON.stringify(r.errors)}`);
assert.ok(
r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM'),
`expected BRIEF_VERSION_BELOW_MINIMUM; warnings=${JSON.stringify(r.warnings)}`,
);
});
test('validateBrief — S18 min-version: absent opt means no version-floor check', () => {
const r = validateBriefContent(BRIEF_21, { strict: true });
assert.ok(!r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM'));
});
test('validateBrief — S18 min-version: brief meeting the floor does not warn', () => {
const r = validateBriefContent(GOOD_BRIEF_22, { strict: true, minBriefVersion: '2.2' });
assert.ok(!r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM'));
});
test('validateBrief — S18 min-version: 2.0 brief below 2.2 floor warns', () => {
const r = validateBriefContent(GOOD_BRIEF, { minBriefVersion: '2.2' });
assert.ok(r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM'));
});
test('validateBrief — S18 min-version: trekreview brief is exempt (no framing to bypass)', () => {
const r = validateBriefContent(REVIEW_AS_BRIEF, { minBriefVersion: '2.2' });
assert.ok(!r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM'));
});