feat(profiles): add built-in fable profile (all six phases on fable)
This commit is contained in:
parent
8b7a849a76
commit
84fbee2313
5 changed files with 51 additions and 10 deletions
|
|
@ -32,7 +32,7 @@ const OPTIONAL_KEYS = [
|
||||||
const OPTIONAL_BOOLEAN_KEYS = new Set(OPTIONAL_KEYS);
|
const OPTIONAL_BOOLEAN_KEYS = new Set(OPTIONAL_KEYS);
|
||||||
|
|
||||||
// Optional string-typed manifest keys (v4.1 Step 3 — additive forward-compat).
|
// Optional string-typed manifest keys (v4.1 Step 3 — additive forward-compat).
|
||||||
// `profile_used`: name of the model profile (economy|balanced|premium|<custom>) the
|
// `profile_used`: name of the model profile (economy|balanced|premium|fable|<custom>) the
|
||||||
// step was executed under. Absence is fine (v4.0 manifests have no
|
// step was executed under. Absence is fine (v4.0 manifests have no
|
||||||
// profile concept); presence MUST be a string.
|
// profile concept); presence MUST be a string.
|
||||||
// Unlike OPTIONAL_BOOLEAN_KEYS, absence is NOT defaulted — the field is simply
|
// Unlike OPTIONAL_BOOLEAN_KEYS, absence is NOT defaulted — the field is simply
|
||||||
|
|
|
||||||
21
lib/profiles/fable.yaml
Normal file
21
lib/profiles/fable.yaml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
profile_version: "1.0"
|
||||||
|
name: fable
|
||||||
|
phase_models:
|
||||||
|
- phase: brief
|
||||||
|
model: fable
|
||||||
|
- phase: research
|
||||||
|
model: fable
|
||||||
|
- phase: plan
|
||||||
|
model: fable
|
||||||
|
- phase: execute
|
||||||
|
model: fable
|
||||||
|
- phase: review
|
||||||
|
model: fable
|
||||||
|
- phase: continue
|
||||||
|
model: fable
|
||||||
|
parallel_agents_min: 6
|
||||||
|
parallel_agents_max: 8
|
||||||
|
external_research_enabled: true
|
||||||
|
brief_reviewer_iter_cap: 3
|
||||||
|
---
|
||||||
|
|
@ -67,6 +67,11 @@ export function resolvePhaseSignalFromFile(briefPath, phase) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CLI shim — mirrors lib/validators/brief-validator.mjs:168 pattern.
|
// CLI shim — mirrors lib/validators/brief-validator.mjs:168 pattern.
|
||||||
|
// Footgun guard (v5.9): this shim's `model` output is brief-signal-only — it
|
||||||
|
// never consults the profile layer. For command wiring, the composed resolver
|
||||||
|
// CLI (`resolver.mjs --resolve-phase-model`, brief > profile > default) is the
|
||||||
|
// single resolution source for {effort, model}. Do not re-wire commands/*.md
|
||||||
|
// Bash blocks back to this shim.
|
||||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const getArg = (name) => {
|
const getArg = (name) => {
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ import { resolvePhaseSignal } from './phase-signal-resolver.mjs';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const BUILTIN_PROFILES_DIR = __dirname; // lib/profiles/
|
const BUILTIN_PROFILES_DIR = __dirname; // lib/profiles/
|
||||||
const BUILTIN_NAMES = new Set(['economy', 'balanced', 'premium']);
|
const BUILTIN_NAMES = new Set(['economy', 'balanced', 'premium', 'fable']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the path to a profile file.
|
* Resolve the path to a profile file.
|
||||||
|
|
@ -221,7 +221,13 @@ export function validateProfileFile(path, opts = {}) {
|
||||||
* @param {string|null} briefPath Absolute or repo-relative path to brief.md, or null
|
* @param {string|null} briefPath Absolute or repo-relative path to brief.md, or null
|
||||||
* @param {string[]|object} argv Full process.argv array OR parsed flags object
|
* @param {string[]|object} argv Full process.argv array OR parsed flags object
|
||||||
* @param {object} [env] Environment-variable record (defaults to process.env)
|
* @param {object} [env] Environment-variable record (defaults to process.env)
|
||||||
* @returns {{model: string, source: 'brief-signal'|'flag'|'env'|'default'}}
|
* @returns {{effort?: string, model: string, source: 'brief-signal'|'flag'|'env'|'default'}}
|
||||||
|
*
|
||||||
|
* `effort` (v5.9 ADDITIVE) is the brief signal's effort passed through when
|
||||||
|
* present — so commands consume ONE coherent {effort, model, source} result
|
||||||
|
* instead of two split CLI calls. Absent when the brief carries no valid
|
||||||
|
* effort signal for the phase (commands default to 'standard' per the
|
||||||
|
* composition rule).
|
||||||
*
|
*
|
||||||
* Error handling contract:
|
* Error handling contract:
|
||||||
* - Never throws. Any failure (ENOENT on briefPath, malformed YAML, missing
|
* - Never throws. Any failure (ENOENT on briefPath, malformed YAML, missing
|
||||||
|
|
@ -234,7 +240,10 @@ export function validateProfileFile(path, opts = {}) {
|
||||||
* directly; commands must inject {resolved model} at Agent-tool spawn sites.
|
* directly; commands must inject {resolved model} at Agent-tool spawn sites.
|
||||||
*/
|
*/
|
||||||
export function resolvePhaseModel(phase, briefPath, argv, env = process.env) {
|
export function resolvePhaseModel(phase, briefPath, argv, env = process.env) {
|
||||||
// Step 1: brief-signal lookup
|
// Step 1: brief-signal lookup. `effort` is captured independently of `model`
|
||||||
|
// so a signal like {effort: high} (no model) still passes effort through
|
||||||
|
// while the model falls to the profile layer.
|
||||||
|
let effort;
|
||||||
if (typeof briefPath === 'string' && briefPath.length > 0 && existsSync(briefPath)) {
|
if (typeof briefPath === 'string' && briefPath.length > 0 && existsSync(briefPath)) {
|
||||||
let fm = null;
|
let fm = null;
|
||||||
try {
|
try {
|
||||||
|
|
@ -246,8 +255,11 @@ export function resolvePhaseModel(phase, briefPath, argv, env = process.env) {
|
||||||
}
|
}
|
||||||
if (fm) {
|
if (fm) {
|
||||||
const signal = resolvePhaseSignal(fm, phase);
|
const signal = resolvePhaseSignal(fm, phase);
|
||||||
|
if (signal && typeof signal.effort === 'string') effort = signal.effort;
|
||||||
if (signal && typeof signal.model === 'string' && signal.model.length > 0) {
|
if (signal && typeof signal.model === 'string' && signal.model.length > 0) {
|
||||||
return { model: signal.model, source: 'brief-signal' };
|
return effort !== undefined
|
||||||
|
? { effort, model: signal.model, source: 'brief-signal' }
|
||||||
|
: { model: signal.model, source: 'brief-signal' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +290,9 @@ export function resolvePhaseModel(phase, briefPath, argv, env = process.env) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const model = phaseModels[phase] || 'opus';
|
const model = phaseModels[phase] || 'opus';
|
||||||
return { model, source: profile_source };
|
return effort !== undefined
|
||||||
|
? { effort, model, source: profile_source }
|
||||||
|
: { model, source: profile_source };
|
||||||
}
|
}
|
||||||
|
|
||||||
// CLI shim — invoked by commands/trek*.md via Bash.
|
// CLI shim — invoked by commands/trek*.md via Bash.
|
||||||
|
|
@ -300,7 +314,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
if (args.includes('--json')) {
|
if (args.includes('--json')) {
|
||||||
process.stdout.write(JSON.stringify(r) + '\n');
|
process.stdout.write(JSON.stringify(r) + '\n');
|
||||||
} else {
|
} else {
|
||||||
process.stdout.write(`model=${r.model} source=${r.source}\n`);
|
const effort = 'effort' in r ? ` effort=${r.effort}` : '';
|
||||||
|
process.stdout.write(`model=${r.model} source=${r.source}${effort}\n`);
|
||||||
}
|
}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// tests/validators/profile-validator.test.mjs
|
// tests/validators/profile-validator.test.mjs
|
||||||
// SC #1, #2, #3: profile-validator validates lib/profiles/{economy,balanced,premium}.yaml
|
// SC #1, #2, #3: profile-validator validates lib/profiles/{economy,balanced,premium,fable}.yaml
|
||||||
// (innebygde profiler) plus rejects invalid models and invalid enum types.
|
// (innebygde profiler) plus rejects invalid models and invalid enum types.
|
||||||
|
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
|
|
@ -18,9 +18,9 @@ import {
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const REPO_ROOT = join(__dirname, '..', '..');
|
const REPO_ROOT = join(__dirname, '..', '..');
|
||||||
|
|
||||||
// SC #1: alle 3 innebygde profiler grønne
|
// SC #1: alle 4 innebygde profiler grønne
|
||||||
|
|
||||||
for (const profileName of ['economy', 'balanced', 'premium']) {
|
for (const profileName of ['economy', 'balanced', 'premium', 'fable']) {
|
||||||
test(`SC #1: lib/profiles/${profileName}.yaml validates clean`, () => {
|
test(`SC #1: lib/profiles/${profileName}.yaml validates clean`, () => {
|
||||||
const r = validateProfile(join(REPO_ROOT, 'lib', 'profiles', `${profileName}.yaml`));
|
const r = validateProfile(join(REPO_ROOT, 'lib', 'profiles', `${profileName}.yaml`));
|
||||||
assert.equal(r.valid, true,
|
assert.equal(r.valid, true,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue