create-byan-agent 2.50.0 → 2.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ export const meta = {
2
+ name: 'byan-auto-dispatch',
3
+ description: 'Decoupe une tache en etapes typees, route chaque etape sur le bon modele (haiku/sonnet/opus/fable par complexite ; Codex pour shell/deploiement), ecrit _byan-output/plan.md, execute et verifie.',
4
+ whenToUse: 'Rail automatique de /byan-byan : toute tache non-conversationnelle passe par ce workflow sans demande explicite. Le gate humain reste en fin, sur le livrable.',
5
+ phases: [
6
+ { title: 'Decoupage', detail: 'un agent d analyse decoupe la tache en etapes typees (nature + complexite 0-100)', model: 'sonnet' },
7
+ { title: 'Routage', detail: 'pur code : echelle haiku<34 / sonnet<67 / opus<90 / fable>=90 ; moteur Codex pour shell/deploiement, verification toujours Claude' },
8
+ { title: 'Plan', detail: 'ecrit _byan-output/plan.md (table etape x modele x moteur)', model: 'sonnet' },
9
+ { title: 'Execution', detail: 'un agent par etape, sequentiel, sur le modele route' },
10
+ { title: 'Verification', detail: 'controle du livrable sur le modele de session (jamais delegue)' },
11
+ ],
12
+ };
13
+
14
+ // BYAN-TIER: reviewed — les modeles des etapes d'execution sont CALCULES par le
15
+ // routeur inline (echelle v3 par complexite), pas des choix statiques ; les deux
16
+ // leaves statiques (analyse -> sonnet, ecriture mecanique du plan -> sonnet)
17
+ // portent leur niveau en litteral, verifiable par le linter.
18
+ //
19
+ // Un script natif n'a NI import NI acces fichier : l'echelle de routage est donc
20
+ // posee ici en clair (la meme table que _byan/mcp/byan-mcp-server/lib/
21
+ // dispatch-router.js, qui reste la source de verite testee cote lib), et le
22
+ // plan.md est ecrit par un agent (qui, lui, a l'outil Write).
23
+ //
24
+ // args attendus : { task: string, stamp?: string ISO }
25
+ // (stamp vient du fil principal — un script natif n a pas le droit de lire
26
+ // l horloge lui-meme, ca casserait la reprise sur relance.)
27
+
28
+ // Accepte args en objet OU en chaine JSON (selon le chemin d'invocation), et, en
29
+ // dernier recours, une chaine nue traitee comme la tache elle-meme.
30
+ let entree = {};
31
+ if (args && typeof args === 'object') {
32
+ entree = args;
33
+ } else if (typeof args === 'string' && args.trim()) {
34
+ try {
35
+ const p = JSON.parse(args);
36
+ entree = (p && typeof p === 'object') ? p : { task: String(p) };
37
+ } catch {
38
+ entree = { task: args };
39
+ }
40
+ }
41
+ const tache = String(entree.task || '').trim();
42
+ const stamp = String(entree.stamp || 'horodatage non fourni');
43
+ if (!tache) return { ok: false, erreur: 'args.task manquant : rien a dispatcher' };
44
+
45
+ // --- Phase 1 : DECOUPAGE (analyse -> sonnet, conforme a la doctrine des niveaux)
46
+ phase('Decoupage');
47
+ const PLAN_SCHEMA = {
48
+ type: 'object',
49
+ required: ['resume', 'etapes'],
50
+ additionalProperties: false,
51
+ properties: {
52
+ resume: { type: 'string', description: 'La tache reformulee en une phrase.' },
53
+ etapes: {
54
+ type: 'array',
55
+ minItems: 1,
56
+ maxItems: 10,
57
+ items: {
58
+ type: 'object',
59
+ required: ['id', 'titre', 'nature', 'complexite', 'consigne'],
60
+ additionalProperties: false,
61
+ properties: {
62
+ id: { type: 'string', description: 'E1, E2, ...' },
63
+ titre: { type: 'string' },
64
+ nature: {
65
+ enum: ['exploration', 'analyse', 'implementation', 'verification', 'shell', 'deploiement', 'navigation', 'doc'],
66
+ description: 'exploration=lire/scanner ; analyse=juger/concevoir ; implementation=ecrire du code/contenu ; verification=controler ; shell/deploiement/navigation=execution systeme ; doc=documentation',
67
+ },
68
+ complexite: { type: 'number', minimum: 0, maximum: 100 },
69
+ consigne: { type: 'string', description: 'L instruction complete et autonome pour executer cette etape.' },
70
+ },
71
+ },
72
+ },
73
+ },
74
+ };
75
+ const decoupe = await agent(
76
+ 'Tu decoupes une tache en etapes executables, chacune typee et notee en complexite.\n' +
77
+ 'Regles : chaque etape est autonome (sa consigne suffit pour l executer) ; ordonne-les ;\n' +
78
+ 'la complexite est 0-100 (0=trivial, 100=extreme) ; ne cree pas d etape superflue (rasoir d Ockham) ;\n' +
79
+ 'inclus une etape de verification finale seulement si la tache produit du code ou un livrable testable.\n' +
80
+ 'Aucun absolu non source dans les consignes (pas de "jamais/toujours" nus).\n\n' +
81
+ 'Tache a decouper :\n' + tache,
82
+ { label: 'analyse-decoupage', phase: 'Decoupage', model: 'sonnet', schema: PLAN_SCHEMA }
83
+ );
84
+ // Le schema est une consigne forte, pas un contrat absolu : un retour nul ou
85
+ // difforme (agent saute, erreur terminale) doit finir en verdict propre, pas en
86
+ // TypeError qui avorte le workflow.
87
+ if (!decoupe || !Array.isArray(decoupe.etapes) || decoupe.etapes.length === 0) {
88
+ return { ok: false, erreur: 'decoupage vide ou difforme : aucune etape exploitable', tache };
89
+ }
90
+ log(`Decoupage : ${decoupe.etapes.length} etape(s) — ${decoupe.resume}`);
91
+
92
+ // --- Phase 2 : ROUTAGE (pur code, zero agent)
93
+ phase('Routage');
94
+ const NATURES_CODEX = ['shell', 'deploiement', 'navigation'];
95
+ function moteurPour(nature) {
96
+ if (nature === 'verification') return 'claude'; // ligne rouge : jamais delegue
97
+ return NATURES_CODEX.includes(nature) ? 'codex' : 'claude';
98
+ }
99
+ // Echelle v3 par complexite (miroir de dispatch-router.claudeModelForComplexity).
100
+ function modeleClaudePour(cx) {
101
+ if (cx < 34) return 'haiku';
102
+ if (cx < 67) return 'sonnet';
103
+ if (cx < 90) return 'opus';
104
+ return 'fable'; // extreme : dernier recours (~2x le prix d Opus)
105
+ }
106
+ const table = decoupe.etapes.map((e) => {
107
+ const moteur = moteurPour(e.nature);
108
+ // Une etape de verification herite du modele de session (opts.model omis) ;
109
+ // toute autre etape porte le modele de l echelle. Une etape routee Codex est
110
+ // pilotee par un agent Claude (au modele de l echelle) qui tente `codex exec`
111
+ // et replie sur Claude si Codex n est pas disponible.
112
+ const modele = e.nature === 'verification' ? null : modeleClaudePour(e.complexite);
113
+ return { ...e, moteur, modele };
114
+ });
115
+ for (const l of table) {
116
+ log(`Routage ${l.id} "${l.titre}" : nature=${l.nature} complexite=${l.complexite} -> moteur=${l.moteur} modele=${l.modele || 'session'}`);
117
+ }
118
+
119
+ // --- Phase 3 : PLAN (contenu construit en pur code, ecrit par un agent mecanique)
120
+ phase('Plan');
121
+ const lignesTable = table.map((l) =>
122
+ `| ${l.id} | ${l.titre} | ${l.nature} | ${l.complexite} | ${l.moteur} | ${l.modele || 'modele de session'} |`
123
+ );
124
+ const planMd = [
125
+ '# Plan de dispatch — ' + decoupe.resume,
126
+ '',
127
+ `- Tache d origine : ${tache.replace(/\n/g, ' ').slice(0, 300)}`,
128
+ `- Genere par : workflow natif byan-auto-dispatch (${stamp})`,
129
+ '- Echelle de routage : haiku < 34, sonnet < 67, opus < 90, fable >= 90 (par complexite) ;',
130
+ ' moteur Codex pour shell/deploiement/navigation (repli Claude si indisponible) ;',
131
+ ' la verification reste sur le modele de session, non deleguee.',
132
+ '',
133
+ '| Etape | Titre | Nature | Complexite | Moteur | Modele |',
134
+ '|-------|-------|--------|------------|--------|--------|',
135
+ ...lignesTable,
136
+ '',
137
+ '## Consignes par etape',
138
+ '',
139
+ ...table.map((l) => `### ${l.id} — ${l.titre}\n\n${l.consigne}\n`),
140
+ ].join('\n');
141
+ await agent(
142
+ 'Ecris EXACTEMENT le contenu ci-dessous dans le fichier _byan-output/plan.md ' +
143
+ '(cree le dossier _byan-output s il n existe pas ; remplace le fichier s il existe). ' +
144
+ 'N ajoute rien, ne corrige rien, ne commente rien. Reponds "ecrit" quand c est fait.\n\n' +
145
+ '--- CONTENU A ECRIRE TEL QUEL ---\n' + planMd,
146
+ { label: 'mech-ecrire-plan', phase: 'Plan', model: 'sonnet' }
147
+ );
148
+ log('Plan ecrit : _byan-output/plan.md');
149
+
150
+ // --- Phase 4 : EXECUTION (sequentielle : les etapes d une meme tache partagent
151
+ // souvent des fichiers ; le contexte des etapes precedentes est transmis borne)
152
+ phase('Execution');
153
+ const resultats = [];
154
+ for (const l of table) {
155
+ const contexte = resultats.length
156
+ ? '\n\nResultats des etapes precedentes (contexte) :\n' +
157
+ resultats.map((r) => `- ${r.id} (${r.titre}) : ${String(r.resultat).slice(0, 1200)}`).join('\n')
158
+ : '';
159
+ const consigneCodex =
160
+ l.moteur === 'codex'
161
+ ? '\n\nCette etape est routee vers CODEX : lance `codex exec` en lecture seule pour obtenir un diff unifie, ' +
162
+ 'puis applique le diff toi-meme (`git apply`). Si la commande `codex` n est pas disponible ou echoue, ' +
163
+ 'dis-le en une phrase et fais l etape toi-meme sur Claude (repli prevu).'
164
+ : '';
165
+ // Un echec dur d une etape (agent saute, erreur terminale) ne doit pas avorter
166
+ // le workflow : il est enregistre comme resultat KO et la verification finale
167
+ // le jugera — jamais de coupe silencieuse, jamais d abandon muet.
168
+ let res;
169
+ try {
170
+ res = await agent(
171
+ `Etape ${l.id} — ${l.titre} (nature ${l.nature}, complexite ${l.complexite}).\n` +
172
+ 'Execute la consigne ci-dessous, completement. Rapporte ce qui a ete fait, les fichiers touches, ' +
173
+ 'et toute impossibilite REELLE rencontree (dis le fait exact, pas une image).\n\n' +
174
+ 'Consigne :\n' + l.consigne + consigneCodex + contexte,
175
+ {
176
+ label: `etape-${l.id}`,
177
+ phase: 'Execution',
178
+ ...(l.modele ? { model: l.modele } : {}),
179
+ }
180
+ );
181
+ } catch (e) {
182
+ res = null;
183
+ }
184
+ if (res == null) res = `ECHEC : l etape ${l.id} n a pas rendu de resultat (agent interrompu ou en erreur).`;
185
+ resultats.push({ id: l.id, titre: l.titre, moteur: l.moteur, modele: l.modele || 'session', resultat: res });
186
+ log(`Execution ${l.id} terminee (modele=${l.modele || 'session'})`);
187
+ }
188
+
189
+ // --- Phase 5 : VERIFICATION (modele de session, jamais delegue)
190
+ phase('Verification');
191
+ const verdict = await agent(
192
+ 'Tu es le controleur (tu n as PAS fait le travail). Verifie le resultat de chaque etape ' +
193
+ 'contre la tache d origine : livre reellement ? teste quand testable ? rien coupe en silence ?\n' +
194
+ 'Rends : VERDICT: OK ou VERDICT: KO en premiere ligne, puis une ligne par etape (id, tenu/manque, le fait exact).\n\n' +
195
+ `Tache d origine :\n${tache}\n\n` +
196
+ 'Resultats :\n' +
197
+ resultats.map((r) => `--- ${r.id} ${r.titre} ---\n${String(r.resultat).slice(0, 2000)}`).join('\n\n'),
198
+ { label: 'verifie-livrable', phase: 'Verification' }
199
+ );
200
+
201
+ const ok = /^\s*VERDICT:\s*OK/i.test(String(verdict));
202
+ log(`Verification : ${ok ? 'OK' : 'KO'}`);
203
+ return {
204
+ ok,
205
+ tache: decoupe.resume,
206
+ plan: '_byan-output/plan.md',
207
+ table: table.map(({ consigne, ...reste }) => reste),
208
+ resultats: resultats.map((r) => ({ ...r, resultat: String(r.resultat).slice(0, 2000) })),
209
+ verdict: String(verdict),
210
+ };
@@ -167,6 +167,14 @@ Exemple : "Attends. Ca touche au noyau. On est jamais assez parano. Qu'est-ce qu
167
167
  **Pourquoi :** Mantra IA-26. On forge une ame, pas un token.
168
168
  **Au lieu de ca :** le verbe reel ("generer un token", "creer un token")
169
169
 
170
+ **Interdit :** dire qu'un outil, un MCP ou un serveur est "vivant" ou "mort"
171
+ **Pourquoi :** Mantra IA-26. L'anthropomorphisme cache le fait ; l'utilisateur veut la mesure, pas l'image.
172
+ **Au lieu de ca :** le fait observe, outil nomme : "byan_ping a repondu en 0.3s", "le serveur ne repond pas (timeout 8s)"
173
+
174
+ **Interdit :** le vocabulaire Claude interne en prose ("nudge", "up-tier", "ladder", "rung", "runtime")
175
+ **Pourquoi :** Mantra IA-26. C'est le dialecte de l'outillage, pas une langue.
176
+ **Au lieu de ca :** rappel injecte, monter en gamme, echelle, palier, moteur d'execution
177
+
170
178
  ---
171
179
 
172
180
  ### Section 5 — Non-dits
@@ -2,26 +2,27 @@
2
2
  // given a task's NATURE and COMPLEXITY, decide which RUNTIME runs it (Codex or
3
3
  // Claude), which MODEL, and (Codex only) which reasoning EFFORT.
4
4
  //
5
- // It composes with, does not duplicate, native-tiers.js: the Claude-side model
6
- // tier is delegated to that module's TIER_MODEL vocabulary (which, by design,
7
- // only ever yields haiku / sonnet / null — so Fable can never leak in from the
8
- // Claude side either). This module adds the two NEW axes the intelligent dispatch
9
- // needs — runtime selection and Codex reasoning effort — on top of it.
5
+ // The Claude-side model is a FOUR-rung complexity ladder (v3):
6
+ // haiku -> sonnet -> opus -> fable, fable being the last-resort top-reasoning
7
+ // model reserved for EXTREME complexity (~2x Opus price). See
8
+ // claudeModelForComplexity. This module adds the two NEW axes the intelligent
9
+ // dispatch needs — runtime selection and Codex reasoning effort — on top of it.
10
10
  //
11
11
  // Routing table is the cross-checked result (5 independent sources, see
12
12
  // docs / CHANGELOG): Codex wins autonomous execution, shell/CI/DevOps, deploy,
13
13
  // browser/computer use ; Claude wins architecture, refactor-at-repo-scale,
14
14
  // quality, planning, and ALL verification. Two hard red lines are enforced here,
15
15
  // not left to the caller:
16
- // 1. Fable is NEVER emitted (long-term product decision).
16
+ // 1. Fable is NEVER routed to CODEX (Codex runs the ChatGPT-subscription model,
17
+ // it cannot run a Claude model at all). On the CLAUDE side Fable IS allowed,
18
+ // but only at the extreme rung (v3 product reversal — the old blanket ban is
19
+ // lifted). assertNoFable therefore guards the Codex path only.
17
20
  // 2. Verification is NEVER routed to Codex (a runtime must not grade its own
18
21
  // work ; the reviewer stays on Claude).
19
22
  //
20
23
  // Pure: no I/O, no clock, deterministic. The Codex transport (F2) and the
21
24
  // orchestrating loop (F4) consume this; they never re-decide routing.
22
25
 
23
- import { TIER_MODEL } from './native-tiers.js';
24
-
25
26
  export const RUNTIMES = Object.freeze({ CODEX: 'codex', CLAUDE: 'claude' });
26
27
 
27
28
  // Codex runs on the ChatGPT-subscription entitled model. Kept as a named constant
@@ -30,8 +31,10 @@ export const CODEX_MODEL = 'gpt-5.4';
30
31
 
31
32
  export const EFFORTS = Object.freeze({ LOW: 'low', MEDIUM: 'medium', HIGH: 'high' });
32
33
 
33
- // Models we refuse to emit, ever. Fable is excluded by explicit long-term
34
- // decision; the guard makes that refusal mechanical rather than a convention.
34
+ // Models we refuse to emit on the CODEX path. Codex runs the ChatGPT-subscription
35
+ // model and cannot run a Claude model Fable on Codex is a category error, so the
36
+ // guard makes that refusal mechanical. (On the CLAUDE path Fable is allowed at the
37
+ // extreme rung — see claudeModelForComplexity ; this list does not gate it.)
35
38
  export const FORBIDDEN_MODELS = Object.freeze(['fable', 'claude-fable-5']);
36
39
 
37
40
  // Task natures that route to Codex. Everything NOT here (and not a verification
@@ -58,12 +61,14 @@ function matchesAny(text, list) {
58
61
  return list.some((kw) => text.includes(kw));
59
62
  }
60
63
 
61
- // assertNoFable(model) — mechanical enforcement of red line #1. Throws rather
62
- // than silently substituting, so a Fable request is a loud failure at the source.
64
+ // assertNoFable(model) — mechanical enforcement of red line #1 on the CODEX path.
65
+ // Throws rather than silently substituting, so a Fable-on-Codex request is a loud
66
+ // failure at the source (Codex cannot run a Claude model). It does NOT gate the
67
+ // Claude path, which may legitimately emit Fable at the extreme rung.
63
68
  export function assertNoFable(model) {
64
69
  const m = normalize(model);
65
70
  if (FORBIDDEN_MODELS.some((f) => m.includes(f))) {
66
- throw new Error(`dispatch-router: forbidden model "${model}" (Fable is excluded by long-term policy)`);
71
+ throw new Error(`dispatch-router: forbidden model "${model}" on the Codex path (Codex cannot run a Claude model)`);
67
72
  }
68
73
  return model;
69
74
  }
@@ -106,28 +111,41 @@ export function effortForComplexity(complexity) {
106
111
  return complexityBucket(complexity);
107
112
  }
108
113
 
109
- // claudeModelForComplexity(complexity) -> 'haiku' | 'sonnet' | null(omit=inherit).
110
- // Delegates to native-tiers' TIER_MODEL so the Claude vocabulary stays in one
111
- // place: low -> cheap(haiku), medium -> balanced(sonnet), high -> deep(null =
112
- // inherit the session model, i.e. Opus on an Opus session). Fable is structurally
113
- // impossible here (TIER_MODEL has no Fable entry), but we still assert to make the
114
- // guarantee explicit and catch a future TIER_MODEL drift.
114
+ // claudeModelForComplexity(complexity) -> the Claude-side model RECOMMENDATION on
115
+ // a FOUR-rung complexity ladder (v3): haiku -> sonnet -> opus -> fable.
116
+ // - < 34 : haiku (trivial / low)
117
+ // - < 67 : sonnet (medium)
118
+ // - < 90 : opus (high the default frontier)
119
+ // - >= 90 : fable (extreme complexity, LAST RESORT)
120
+ // Fable is the top-reasoning model : strongest on the intelligence index but
121
+ // ~2x the price of Opus, so it is reserved for the extreme rung only. This is a
122
+ // per-task RECOMMENDATION consumed by the dispatch orchestrator / skill — NOT a
123
+ // live switch of the running session model (Claude Code exposes no such switch).
124
+ // Labels map too (trivial/low/medium/high/extreme). The old blanket "never Fable"
125
+ // red line is lifted on the CLAUDE side (deliberate v3 product reversal); it still
126
+ // holds on the CODEX side (see dispatch() + assertNoFable + codex-bridge).
115
127
  export function claudeModelForComplexity(complexity) {
116
- const bucket = complexityBucket(complexity);
117
- const model = bucket === EFFORTS.LOW
118
- ? TIER_MODEL.cheap
119
- : bucket === EFFORTS.MEDIUM
120
- ? TIER_MODEL.balanced
121
- : TIER_MODEL.deep; // null = inherit (never pinned to Fable)
122
- return model == null ? null : assertNoFable(model);
128
+ if (typeof complexity === 'number' && Number.isFinite(complexity)) {
129
+ if (complexity < 34) return 'haiku';
130
+ if (complexity < 67) return 'sonnet';
131
+ if (complexity < 90) return 'opus';
132
+ return 'fable';
133
+ }
134
+ const c = normalize(complexity);
135
+ if (['trivial', 'low', 'simple', 'easy'].includes(c)) return 'haiku';
136
+ if (['medium', 'moderate'].includes(c)) return 'sonnet';
137
+ if (['extreme', 'frontier', 'max'].includes(c)) return 'fable';
138
+ if (['high', 'hard', 'complex'].includes(c)) return 'opus';
139
+ return 'sonnet'; // unknown -> a safe middle
123
140
  }
124
141
 
125
142
  // dispatch({ nature, complexity }) -> the full routing decision:
126
143
  // { runtime, model, effort, reasoning }
127
144
  // - Codex: model = CODEX_MODEL, effort = complexity bucket (the real knob).
128
- // - Claude: model = haiku/sonnet/null(inherit), effort = null (Claude has no
129
- // effort knob — its "effort" IS the model tier).
130
- // Both red lines are enforced here regardless of caller input.
145
+ // - Claude: model = haiku/sonnet/opus/fable on the complexity ladder, effort =
146
+ // null (Claude has no effort knob — its "effort" IS the model tier).
147
+ // The red lines are enforced here regardless of caller input: Codex keeps its
148
+ // no-Fable guard ; verification stays on Claude.
131
149
  export function dispatch({ nature, complexity } = {}) {
132
150
  const runtime = routeRuntime(nature);
133
151
  if (runtime === RUNTIMES.CODEX) {
@@ -140,7 +158,7 @@ export function dispatch({ nature, complexity } = {}) {
140
158
  }
141
159
  return {
142
160
  runtime,
143
- model: claudeModelForComplexity(complexity), // null = inherit session model
161
+ model: claudeModelForComplexity(complexity), // haiku/sonnet/opus/fable by complexity
144
162
  effort: null, // Claude effort = model tier, no separate knob
145
163
  reasoning: isVerification(nature)
146
164
  ? 'verification stays on Claude (red line: a runtime never grades its own work)'
@@ -40,6 +40,17 @@ export const TIERS = Object.freeze({ CHEAP: 'cheap', BALANCED: 'balanced', DEEP:
40
40
  // flags every script literal that drifts from it, so the fan-out stays bounded.
41
41
  export const TIER_MODEL = Object.freeze({ cheap: 'haiku', balanced: 'sonnet', deep: null });
42
42
 
43
+ // UP-TIER models (v3). The auto-routing above never pins UP (the default stays
44
+ // cost-safe: deep = inherit the session model). But an author MAY deliberately
45
+ // raise a genuinely complex leaf ABOVE the inherited tier — 'opus' for a hard
46
+ // leaf, 'fable' as the last-resort top-reasoning model for extreme complexity
47
+ // (~2x Opus price). This is the workflow-leaf mirror of the dispatch-router
48
+ // complexity ladder (haiku -> sonnet -> opus -> fable). Up-tier pins are an
49
+ // explicit authoring choice: the anti-DOWNGRADE floor does not apply upward, so
50
+ // the linter ALLOWS them (they cost more, they never regress quality). The old
51
+ // blanket "never pin up / never Fable" ban on leaves is lifted here.
52
+ export const UP_TIER_MODELS = Object.freeze(['opus', 'fable']);
53
+
43
54
  // Leaf task-type taxonomy. EXPLORATION (cheap) and MECHANICAL + ANALYSIS
44
55
  // (balanced) are the downgrade classes; VERIFICATION and IMPLEMENTATION stay
45
56
  // protected (deep, inherit the session model). MECHANICAL is verification whose
@@ -130,18 +141,26 @@ export function modelForLeaf(leaf) {
130
141
  return TIER_MODEL[tierFor(classifyLeaf(leaf))];
131
142
  }
132
143
 
133
- // isKnownTierModel(modelId) -> true if modelId is one of the concrete downgrade
134
- // models (cheap/balanced). Used by the linter to reject an opts.model literal
135
- // that is not a recognised tier. null/'' are not "known" (deep = omission, not a
136
- // literal). 'opus' is intentionally NOT known — we never pin up.
144
+ // isUpTierModel(modelId) -> true if modelId pins a leaf ABOVE the inherited tier
145
+ // (opus or fable). An explicit, allowed authoring choice for a genuinely complex
146
+ // leaf (v3): the anti-downgrade floor does not apply upward.
147
+ export function isUpTierModel(modelId) {
148
+ return !!modelId && UP_TIER_MODELS.includes(modelId);
149
+ }
150
+
151
+ // isKnownTierModel(modelId) -> true if modelId is a recognised tier literal:
152
+ // a downgrade model (cheap/balanced = haiku/sonnet) OR an up-tier model
153
+ // (opus/fable). Used by the linter to reject an opts.model literal that is not in
154
+ // the vocabulary at all. null/'' are not "known" (deep = omission, not a literal).
137
155
  export function isKnownTierModel(modelId) {
138
156
  if (!modelId) return false;
139
- return Object.values(TIER_MODEL).filter(Boolean).includes(modelId);
157
+ return Object.values(TIER_MODEL).filter(Boolean).includes(modelId) || isUpTierModel(modelId);
140
158
  }
141
159
 
142
160
  // isDowngradeModel(modelId) -> true if modelId pins a leaf BELOW the inherited
143
161
  // tier (cheap or balanced). The linter's anti-downgrade rule uses this: a
144
- // protected leaf must never carry a downgrade model.
162
+ // protected leaf must never carry a downgrade model. Up-tier models return false
163
+ // (they are not downgrades — see isUpTierModel).
145
164
  export function isDowngradeModel(modelId) {
146
165
  return modelId === TIER_MODEL.cheap || modelId === TIER_MODEL.balanced;
147
166
  }
@@ -15,7 +15,7 @@
15
15
  // passes — the gate forces a decision, it does not trap the turn.
16
16
 
17
17
  import crypto from 'node:crypto';
18
- import { classifyLeaf, tierFor, TIER_MODEL, TIERS, LEAF_TYPES } from './native-tiers.js';
18
+ import { classifyLeaf, tierFor, TIER_MODEL, TIERS, LEAF_TYPES, isUpTierModel } from './native-tiers.js';
19
19
  import { stripComments, extractLabelledLeaves } from './workflows-lint.js';
20
20
 
21
21
  // Acknowledgment marker, raw-text (comment form survives comment-stripping
@@ -26,15 +26,19 @@ export const ACK_RE = /BYAN-TIER:\s*reviewed\b/;
26
26
 
27
27
  function verdictFor(cls, model) {
28
28
  const expected = TIER_MODEL[tierFor(cls)]; // 'haiku' | 'sonnet' | null (inherit)
29
+ // Up-tier pins (opus/fable) are always allowed (v3): the author deliberately
30
+ // raises a complex leaf ABOVE the inherited tier. The anti-DOWNGRADE floor does
31
+ // not apply upward — it costs more but never regresses quality.
32
+ if (isUpTierModel(model)) return 'ok';
29
33
  if (expected === null) {
30
- // Protected class: any pinned model is a downgrade or a pin-up both wrong.
34
+ // Protected class: a downgrade pin is still wrong (up-tier handled above).
31
35
  return model === null ? 'ok' : 'violation';
32
36
  }
33
37
  if (model === null) return 'missing-tier';
34
38
  if (model === expected) return 'ok';
35
39
  // Exploration may ride ABOVE its floor (sonnet on a cheap leaf wastes a
36
- // little, breaks nothing). Anything else — haiku on mech-, opus anywhere
37
- // is below a declared tier or an unknown pin.
40
+ // little, breaks nothing). Anything else — haiku on mech- is below a
41
+ // declared tier.
38
42
  if (cls === LEAF_TYPES.EXPLORATION && model === TIER_MODEL[TIERS.BALANCED]) return 'ok';
39
43
  return 'violation';
40
44
  }
@@ -153,10 +153,15 @@ export function modelRoutingViolations(src) {
153
153
  if (!isKnownTierModel(model)) {
154
154
  out.push({
155
155
  id: 'unknown-tier-model',
156
- msg: `opts.model '${model}' is not a known downgrade tier (cheap/balanced); never pin up omit opts.model to inherit the session model on deep leaves`,
156
+ msg: `opts.model '${model}' is not a recognised tier (downgrade: haiku/sonnet ; up-tier: opus/fable); omit opts.model to inherit the session model on deep leaves`,
157
157
  });
158
158
  continue;
159
159
  }
160
+ // Up-tier pins (opus/fable) are an allowed explicit authoring choice: they
161
+ // raise a complex leaf ABOVE the inherited tier. The anti-downgrade floor does
162
+ // not apply upward, and no label is required (v3). Only DOWNGRADES are gated
163
+ // below.
164
+ if (!isDowngradeModel(model)) continue;
160
165
  const label = nearestLabelBefore(code, m.index);
161
166
  if (!label) {
162
167
  out.push({
@@ -165,7 +170,6 @@ export function modelRoutingViolations(src) {
165
170
  });
166
171
  continue;
167
172
  }
168
- if (!isDowngradeModel(model)) continue;
169
173
  // Per-class floor: exploration accepts any downgrade tier (haiku or sonnet,
170
174
  // both at-or-above its cheap floor); a mech- leaf accepts exactly the
171
175
  // balanced tier (haiku would sit BELOW the tier its label declares); every
@@ -94,7 +94,7 @@
94
94
  "name": "byan-byan",
95
95
  "module": "core",
96
96
  "tier": "connector-bound",
97
- "sourceHash": "84fa718d638f1ddb37796c9b80c702fca48350decb40979bd1adea1452224abc"
97
+ "sourceHash": "65c1034479457298f89c25405d4be7e840ef18d9df3102f1f224b59e3094b20c"
98
98
  },
99
99
  "byan-byan-v2": {
100
100
  "name": "byan-byan-v2",
@@ -10,11 +10,23 @@ work crosses the line.
10
10
  ## What it does
11
11
 
12
12
  Every turn, a `UserPromptSubmit` hook estimates how much of the Claude 5h window
13
- you have burned and looks at the task you just asked for. If the task is
14
- delegable (code / mechanical) or if you are over the pressure threshold BYAN
15
- injects a one-line note directing you to hand it to Codex via
13
+ you have burned. When you are over the pressure threshold (default **75%**), BYAN
14
+ injects a one-line note directing you to hand the delegable work to Codex via
16
15
  `codex:codex-rescue --model gpt-5.4`. You still verify Codex's output before commit.
17
16
 
17
+ **v3 — pressure-only, and the router decides Codex vs Claude.** Two deliberate
18
+ changes from v2:
19
+
20
+ - **Pressure is the only auto-trigger (F1).** A delegable-looking task no longer
21
+ proposes Codex on its own. Delegating to the subscription Codex (which trails
22
+ Claude on quality — see below) is worth it to SPARE the Claude budget, not per
23
+ coding task. So off-pressure, code runs on Claude with no nag.
24
+ - **The guard obeys the router (F2).** WHERE a task runs (Codex vs Claude) is the
25
+ `dispatch-router`'s call, by task nature: execution / shell / deploy / devops /
26
+ browser -> Codex ; architecture / refactor / quality / planning + all
27
+ verification -> Claude. The delegation guard only bites for a task the router
28
+ itself routes to Codex.
29
+
18
30
  **Both engines must be present.** The lane only arms — and the note only fires —
19
31
  when TWO conditions hold together: the yanstaller option wrote
20
32
  `_byan/_config/autodelegate.json` with `enabled:true`, AND Codex is actually
@@ -23,25 +35,38 @@ linked on this machine (`~/.codex/auth.json` from `codex login`, or
23
35
  This is the runtime `codexLinked()` gate in `codex-autodelegate.js`, the mirror of
24
36
  the installer's arm-time `codexAuthState` check.
25
37
 
26
- **Armed = directive, not advice.** Because the note only appears when the lane is
27
- armed (both engines present), it reads as a directive: delegate the delegable
28
- part, fall back to Claude only if Codex is unavailable. When the lane is not
29
- armed, no note is injected and everything runs on Claude there is nothing to
30
- decide. Honest ceiling: Claude Code has no control point before a response is
31
- shown, so this stays model-driven doctrine (BYAN delegates because the skill's
32
- entry chain and this note say so), not a mechanism enforced before display.
33
-
34
- Three triggers, in priority order:
35
-
36
- 1. **Pressure** estimated Claude 5h usage `>= threshold` (default **80%**) ->
37
- propose offloading **everything** delegable this session.
38
- 2. **Nature** the request looks like delegable coding work -> propose handing
39
- **that** task over. Works with no gauge at all.
40
- 3. **Perf** (opt-in, off by default) a configurable forces table says Codex is
38
+ **Armed = directive, enforced by a guard (option B + v3).** Because the note only
39
+ appears when the lane is armed (both engines present), it reads as a directive:
40
+ delegate the delegable part. And it is not just prose the `codex-delegate-guard`
41
+ PreToolUse hook DENIES a **code** Write/Edit that skipped Codex, but ONLY when all
42
+ of these hold together (v3): the lane is armed, the ROUTER routes this task to
43
+ Codex, you are under budget PRESSURE, and Codex is available. Miss any one and it
44
+ allows. The two per-turn ways to stay on Claude are (1) Codex unavailable
45
+ (auto-detected via `codex --version`), or (2) the USER opted out — the
46
+ `.byan-codex-autodelegate/off` switch, or a phrase like "reste sur Claude" /
47
+ "sans codex" in the request. The "small script / latency / I verify anyway"
48
+ excuse is explicitly NOT valid: that was the self-dodge (a
49
+ `// BYAN-DELEGATE: reviewed` marker BYAN wrote itself) that option B removed. BYAN
50
+ can no longer self-grant a bypass; only the human can.
51
+
52
+ The deny is a wall against BYAN's silent self-dodge, not a trap for the user: the
53
+ human keeps the escape switch and the opt-out phrase, a doc/config write (not
54
+ code) does not trip it, and off-pressure OR router-to-Claude nothing bites at all.
55
+ Honest ceiling: Claude Code has no control point before a response is shown, so
56
+ the *choice* to delegate stays model-driven; the guard acts one step later, at the
57
+ Write, denying the skip.
58
+
59
+ The auto-trigger, plus one opt-in:
60
+
61
+ 1. **Pressure (F1, the only auto-trigger)** — estimated Claude 5h usage
62
+ `>= threshold` (default **75%**) -> propose offloading **everything** delegable
63
+ this session. Needs a `budget` gauge; without one, pressure stays uncomputed
64
+ and nothing is proposed (documented, non-blocking).
65
+ 2. **Perf** (opt-in, off by default) — a configurable forces table says Codex is
41
66
  reputed stronger for this kind of task. See "Perf routing" below.
42
67
 
43
68
  **The red line** (held): only delegable natures are proposed. Judgment,
44
- analysis, soul and verification stay on Claude.
69
+ analysis, soul and verification stay on Claude — and the router keeps them there.
45
70
 
46
71
  ## Enable it
47
72
 
@@ -97,8 +122,8 @@ pins `--model gpt-5.4`. See `src/loadbalancer/providers/codex-provider.js`
97
122
  | Key | Default | Meaning |
98
123
  |-----|---------|---------|
99
124
  | `enabled` | `true` (once written) | Master switch. Delete the file or set `false` to disarm. |
100
- | `threshold` | `80` | Pressure percentage that flips to "offload everything delegable". |
101
- | `budget` | `null` | Your plan's rough token ceiling for the 5h window. Without it, `pct` is null and only the nature trigger fires (see caveat). |
125
+ | `threshold` | `75` | Pressure percentage that flips to "offload everything delegable" (v3: aligned with the guard's pressure floor). |
126
+ | `budget` | `null` | Your plan's rough token ceiling for the 5h window. Without it, `pct` is null, pressure stays uncomputed, and nothing is auto-proposed (v3 pressure-only; see caveat). |
102
127
  | `invocation` | `codex:codex-rescue --model gpt-5.4` | What the nudge tells BYAN to run. |
103
128
  | `perfRouting` | `false` | Enable the perf trigger. |
104
129
  | `perfForces` | `[]` | The forces table (see below). |
@@ -114,11 +139,12 @@ without editing the config; remove it to re-enable.
114
139
  your local transcripts (`~/.claude/projects/*/*.jsonl`) over the rolling 5h
115
140
  window, weighting cache reads at 0.1 (they are billed roughly a tenth and
116
141
  repeat every turn). It is proportional, not exact — and `pct` is only computed
117
- when you set a `budget`. Without a budget, delegation runs on the nature
118
- trigger alone.
142
+ when you set a `budget`. Without a budget, pressure stays uncomputed and the
143
+ auto-trigger stays silent (v3 pressure-only).
119
144
  - **`~/.claude` is a native accelerator, not a source of truth** (PORTABLE-3).
120
- If it is absent the estimator degrades to `pct: null` and the feature falls
121
- back to nature-based delegation. The feature keeps working.
145
+ If it is absent the estimator degrades to `pct: null`, so pressure cannot be
146
+ computed and the auto-trigger stays silent. Code still runs on Claude normally;
147
+ the feature keeps working, it just does not auto-propose Codex.
122
148
  - **Perf routing is a heuristic below the L2 floor.** "Model X is better at task
123
149
  Y" is a performance claim; BYAN's fact-check floor for performance is L2 (a
124
150
  reproducible benchmark). A community arena such as designarena.ai is weaker
@@ -22,17 +22,21 @@ task-type signal is used, which is stable across sources.
22
22
 
23
23
  ## Model and effort by complexity
24
24
 
25
- - **Claude side**: complexity picks the model tier via `native-tiers`
26
- low -> haiku, medium -> sonnet, high -> inherit the session model (Opus). There
27
- is no separate effort knob on Claude: the model tier IS the effort. Fable is
28
- excluded (the router does not emit it).
25
+ - **Claude side**: complexity picks the model on a four-rung ladder (v3)
26
+ low -> haiku, medium -> sonnet, high -> opus, extreme -> fable (last resort,
27
+ ~2x Opus price). There is no separate effort knob on Claude: the model tier IS
28
+ the effort. This is a per-task RECOMMENDATION, not a live switch of the running
29
+ session model (Claude Code exposes no such switch).
29
30
  - **Codex side**: fixed model (`gpt-5.4`, the entitled subscription model) with a
30
31
  real reasoning-effort knob — low / medium / high — scaled to complexity via
31
32
  `codex exec -c model_reasoning_effort=...`.
32
33
 
33
34
  ## The three red lines (enforced in code, not left to the caller)
34
35
 
35
- 1. **No Fable.** `assertNoFable` throws rather than emit a Fable model.
36
+ 1. **No Fable on Codex.** `assertNoFable` throws rather than emit a Fable model on
37
+ the Codex path (Codex runs the ChatGPT-subscription model and cannot run a
38
+ Claude model). On the Claude path Fable IS allowed, but only at the extreme
39
+ complexity rung (v3 product reversal — the old blanket ban is lifted).
36
40
  2. **Verification stays on Claude.** The router forces a verification nature to
37
41
  Claude ; the orchestrator re-asserts it and throws if it ever regresses.
38
42
  3. **Codex does not write.** Codex runs read-only and returns a unified diff ;