chore(cleanup): R-b R3 — profiles, phase signals and the effort dialog removed, one fixed path per command

The four profiles, the resolver, the profile validator and docs/profiles.md go;
/trekbrief Phase 3.5 and the brief's phase_signals checks go (an older brief's
key is ignored); MANIFEST_PROFILE_DRIFT and profile_used go. Every Profile,
Composition rule, Sequencing gate and High-effort section leaves the commands.
Decision 209: /trekresearch runs the full swarm with contrarian-researcher as
wave 2 every run; /trekreview and the coordinator run all four passes with no
high-effort normalization. Voyage agents are spawned with no model (their own
model: opus applies); the two built-in spawns pass model "opus". --profile
stays in the parser as a retired flag that swallows its value. §Model & effort
moves to docs/architecture.md; the sequencing-gate eval case goes.

Files removed: 44 (43 from the R3 list + tests/commands/trekreview.test.mjs,
whose every test pinned removed text). Files edited: 43.
Tests: 2467 (2466 pass / 0 fail / 1 skipped) before, 2309 (2309 pass / 0 fail /
0 skipped) after. Durable check: all three groups green, none pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-07 21:12:57 +01:00
commit 2a35d067d9
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
87 changed files with 225 additions and 4967 deletions

View file

@ -9,7 +9,6 @@
import { readFileSync, existsSync } from 'node:fs';
import { parseDocument } from '../util/frontmatter.mjs';
import { issue, ok, fail } from '../util/result.mjs';
import { BASE_ALLOWED_MODELS } from './profile-validator.mjs';
import { parseSuccessCriteria, runnableCheck, firstCommand } from '../verification/criteria-runner.mjs';
import { isMain } from '../util/is-main.mjs';
@ -18,8 +17,6 @@ export const REVIEW_AS_BRIEF_REQUIRED_FRONTMATTER = ['type', 'task', 'slug', 'pr
export const BRIEF_TYPE_VALUES = Object.freeze(['trekbrief', 'trekreview']);
export const BRIEF_RESEARCH_STATUS_VALUES = ['pending', 'in_progress', 'complete', 'skipped'];
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']);
@ -77,55 +74,6 @@ export function validateBriefContent(text, opts = {}) {
}
}
// v5.1 — phase_signals (additive optional field) + version-conditional sequencing gate.
// Composition rule documented in each downstream command's "Composition rule (v5.1)" section.
const hasSignals = 'phase_signals' in fm;
const hasPartial = 'phase_signals_partial' in fm;
if (hasSignals && hasPartial) {
errors.push(issue(
'BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE',
'phase_signals and phase_signals_partial are mutually exclusive — set exactly one',
'Either commit per-phase signals OR record phase_signals_partial: true (force-stop).',
));
}
if (hasSignals) {
if (!Array.isArray(fm.phase_signals)) {
errors.push(issue(
'BRIEF_INVALID_PHASE_SIGNALS',
'phase_signals must be a list of {phase, effort?, model?} entries',
));
} else {
for (const entry of fm.phase_signals) {
if (!entry || typeof entry !== 'object' || !('phase' in entry)) {
errors.push(issue('BRIEF_INVALID_PHASE_SIGNALS', `phase_signals entry must include a "phase" key`));
continue;
}
if (!PHASE_SIGNAL_PHASES.includes(entry.phase)) {
errors.push(issue(
'BRIEF_INVALID_PHASE_SIGNAL_PHASE',
`phase_signals.phase "${entry.phase}" not in [${PHASE_SIGNAL_PHASES.join(', ')}]`,
));
}
if ('effort' in entry && !EFFORT_LEVELS.includes(entry.effort)) {
errors.push(issue(
'BRIEF_INVALID_EFFORT',
`phase_signals.effort "${entry.effort}" not in [${EFFORT_LEVELS.join(', ')}]`,
));
}
if ('model' in entry && !BASE_ALLOWED_MODELS.includes(entry.model)) {
errors.push(issue(
'BRIEF_INVALID_MODEL',
`phase_signals.model "${entry.model}" not in [${BASE_ALLOWED_MODELS.join(', ')}]`,
));
}
}
}
}
// Sequencing gate: brief_version ≥ 2.1 requires phase_signals OR phase_signals_partial.
// Coerce to String so the gate fires regardless of whether YAML parsed the value as
// 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)) {
@ -156,15 +104,7 @@ export function validateBriefContent(text, opts = {}) {
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',
'brief_version ≥ 2.1 requires phase_signals (or phase_signals_partial: true)',
'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)) {

View file

@ -58,7 +58,6 @@ import {
testFirstFileEntries, testFirstFiles,
} from '../parsers/plan-schema.mjs';
import { validateAllManifests } from '../parsers/manifest-yaml.mjs';
import { parseDocument } from '../util/frontmatter.mjs';
import { issue, fail } from '../util/result.mjs';
import { parsePlanVerification, firstCommand, runnableCheck, refusal } from '../verification/criteria-runner.mjs';
import { isMain } from '../util/is-main.mjs';
@ -426,29 +425,6 @@ export function validatePlanContent(rawText, opts = {}) {
else warnings.push(...v18);
}
// v4.1 SC #20 — MANIFEST_PROFILE_DRIFT detection. Strict-mode only.
// If plan frontmatter declares `profile: <name>` and any step manifest
// declares `profile_used: <other>`, emit a warning (NOT an error) so
// operators see drift but parsing remains forward-compat.
if (strict) {
const planFm = parseDocument(text).parsed?.frontmatter;
const planProfile =
planFm && typeof planFm.profile === 'string' ? planFm.profile : null;
if (planProfile) {
for (const m of manRes.parsed) {
const stepProfile = m.manifest && m.manifest.profile_used;
if (typeof stepProfile === 'string' && stepProfile !== planProfile) {
warnings.push(issue(
'MANIFEST_PROFILE_DRIFT',
`step ${m.n}: profile_used = ${stepProfile}, plan profile = ${planProfile}`,
'A step manifest declares a different profile than the plan frontmatter; ' +
'verify whether this is intentional (manual override) or accidental drift.',
));
}
}
}
}
return {
valid: errors.length === 0,
errors,

View file

@ -1,195 +0,0 @@
// lib/validators/profile-validator.mjs
// Validate model-profile YAML files (lib/profiles/*.yaml + custom voyage-profiles/*.yaml).
//
// Profile schema (v4.1, profile_version 1.0):
// name : string
// phase_models : list-of-dicts [{phase: string, model: string}, ...]
// 6 entries required (brief, research, plan, execute, review, continue)
// parallel_agents_min : number (≥ 1)
// parallel_agents_max : number (≥ parallel_agents_min)
// external_research_enabled : boolean
// brief_reviewer_iter_cap : number (≥ 1)
// experimental : boolean (OPTIONAL) — true marks a tier whose
// gating constants are not yet empirically
// calibrated (e.g. economy's parked-synthetic
// cross-tier Jaccard floor). Absent ⇒ stable.
//
// Issue codes:
// PROFILE_MISSING_FIELD — required top-level frontmatter field absent
// PROFILE_INVALID_MODEL — phase_models[N].model not in allowlist
// PROFILE_INVALID_ENUM — wrong-type value (e.g. external_research_enabled is string)
// PROFILE_READ_ERROR — file unreadable or parse-error
// PROFILE_NOT_FOUND — file does not exist
//
// Allowed model values: ['sonnet', 'opus', 'fable']. Haiku is allowed only when
// VOYAGE_ALLOW_HAIKU=1 (the model-choice rule: Haiku is never a default, only
// an explicit opt-in for special use).
import { readFileSync, existsSync } from 'node:fs';
import { parseDocument } from '../util/frontmatter.mjs';
import { issue, ok, fail } from '../util/result.mjs';
import { isMain } from '../util/is-main.mjs';
export const PROFILE_REQUIRED_FIELDS = Object.freeze([
'name',
'phase_models',
'parallel_agents_min',
'parallel_agents_max',
'external_research_enabled',
'brief_reviewer_iter_cap',
]);
export const PROFILE_REQUIRED_PHASES = Object.freeze([
'brief', 'research', 'plan', 'execute', 'review', 'continue',
]);
export const BASE_ALLOWED_MODELS = Object.freeze(['sonnet', 'opus', 'fable']);
function getAllowedModels(env = process.env) {
if (env.VOYAGE_ALLOW_HAIKU === '1') {
return [...BASE_ALLOWED_MODELS, 'haiku'];
}
return BASE_ALLOWED_MODELS;
}
export function validateProfileContent(text, opts = {}) {
const env = opts.env || process.env;
const allowedModels = getAllowedModels(env);
const doc = parseDocument(text);
if (!doc.valid) {
return fail(issue('PROFILE_READ_ERROR', `Frontmatter parse error: ${doc.errors[0]?.message || 'unknown'}`));
}
const fm = doc.parsed.frontmatter || {};
const errors = [];
const warnings = [];
// Required field presence
for (const k of PROFILE_REQUIRED_FIELDS) {
if (!(k in fm)) {
errors.push(issue('PROFILE_MISSING_FIELD', `Required profile field missing: ${k}`));
}
}
// Type checks for scalar fields
if ('name' in fm && typeof fm.name !== 'string') {
errors.push(issue('PROFILE_INVALID_ENUM', `name must be string (got ${typeof fm.name})`));
}
if ('parallel_agents_min' in fm && typeof fm.parallel_agents_min !== 'number') {
errors.push(issue('PROFILE_INVALID_ENUM',
`parallel_agents_min must be number (got ${typeof fm.parallel_agents_min})`));
}
if ('parallel_agents_max' in fm && typeof fm.parallel_agents_max !== 'number') {
errors.push(issue('PROFILE_INVALID_ENUM',
`parallel_agents_max must be number (got ${typeof fm.parallel_agents_max})`));
}
if (typeof fm.parallel_agents_min === 'number' && typeof fm.parallel_agents_max === 'number') {
if (fm.parallel_agents_max < fm.parallel_agents_min) {
errors.push(issue('PROFILE_INVALID_ENUM',
`parallel_agents_max (${fm.parallel_agents_max}) < parallel_agents_min (${fm.parallel_agents_min})`));
}
}
if ('external_research_enabled' in fm && typeof fm.external_research_enabled !== 'boolean') {
errors.push(issue('PROFILE_INVALID_ENUM',
`external_research_enabled must be boolean (got ${typeof fm.external_research_enabled})`));
}
if ('brief_reviewer_iter_cap' in fm && typeof fm.brief_reviewer_iter_cap !== 'number') {
errors.push(issue('PROFILE_INVALID_ENUM',
`brief_reviewer_iter_cap must be number (got ${typeof fm.brief_reviewer_iter_cap})`));
}
// Optional field: `experimental` (boolean). Absent ⇒ tier is stable.
if ('experimental' in fm && typeof fm.experimental !== 'boolean') {
errors.push(issue('PROFILE_INVALID_ENUM',
`experimental must be boolean (got ${typeof fm.experimental})`));
}
// phase_models validation
if ('phase_models' in fm) {
if (!Array.isArray(fm.phase_models)) {
errors.push(issue('PROFILE_INVALID_ENUM', `phase_models must be a list-of-dicts (got ${typeof fm.phase_models})`));
} else {
const seenPhases = new Set();
for (let i = 0; i < fm.phase_models.length; i++) {
const entry = fm.phase_models[i];
if (!entry || typeof entry !== 'object') {
errors.push(issue('PROFILE_INVALID_ENUM',
`phase_models[${i}] must be a dict {phase, model} (got ${typeof entry})`,
undefined,
`phase_models[${i}]`));
continue;
}
if (typeof entry.phase !== 'string') {
errors.push(issue('PROFILE_INVALID_ENUM',
`phase_models[${i}].phase must be string`,
undefined,
`phase_models[${i}].phase`));
} else {
seenPhases.add(entry.phase);
if (!PROFILE_REQUIRED_PHASES.includes(entry.phase)) {
errors.push(issue('PROFILE_INVALID_ENUM',
`phase_models[${i}].phase "${entry.phase}" not in [${PROFILE_REQUIRED_PHASES.join(', ')}]`,
undefined,
`phase_models[${i}].phase`));
}
}
if (typeof entry.model !== 'string') {
errors.push(issue('PROFILE_INVALID_MODEL',
`phase_models[${i}].model must be string`,
undefined,
`phase_models[${i}].model`));
} else if (!allowedModels.includes(entry.model)) {
errors.push(issue('PROFILE_INVALID_MODEL',
`phase_models[${i}].model "${entry.model}" not in [${allowedModels.join(', ')}]` +
(entry.model === 'haiku' ? ' (set VOYAGE_ALLOW_HAIKU=1 to allow)' : ''),
undefined,
`phase_models[${i}].model`));
}
}
// All 6 required phases must be present
for (const required of PROFILE_REQUIRED_PHASES) {
if (!seenPhases.has(required)) {
errors.push(issue('PROFILE_MISSING_FIELD',
`phase_models missing required phase entry: ${required}`));
}
}
}
}
return { valid: errors.length === 0, errors, warnings, parsed: { frontmatter: fm } };
}
export function validateProfile(filePath, opts = {}) {
if (!existsSync(filePath)) {
return fail(issue('PROFILE_NOT_FOUND', `Profile file not found: ${filePath}`));
}
let text;
try { text = readFileSync(filePath, 'utf-8'); }
catch (e) { return fail(issue('PROFILE_READ_ERROR', `Cannot read ${filePath}: ${e.message}`)); }
const r = validateProfileContent(text, opts);
return { ...r, parsed: { ...(r.parsed || {}), filePath } };
}
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const filePath = args.find(a => !a.startsWith('--'));
if (!filePath) {
process.stderr.write('Usage: profile-validator.mjs [--json] <profile.yaml>\n');
process.exit(2);
}
const r = validateProfile(filePath);
if (args.includes('--json')) {
process.stdout.write(JSON.stringify({ valid: r.valid, errors: r.errors, warnings: r.warnings }, null, 2) + '\n');
} else {
process.stdout.write(`profile-validator: ${r.valid ? 'PASS' : 'FAIL'} ${filePath}\n`);
for (const e of r.errors) process.stderr.write(` ERROR [${e.code}] ${e.message}${e.location ? ' at ' + e.location : ''}\n`);
for (const w of r.warnings) process.stderr.write(` WARN [${w.code}] ${w.message}\n`);
}
process.exit(r.valid ? 0 : 1);
}