create-byan-agent 2.50.0 → 2.52.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.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [2.52.0] - 2026-07-20
13
+
14
+ ### Changed — Delegation Codex v3 : pression-only + obeissance au routeur (F1, F2)
15
+ - **F1 — la delegation ne se declenche plus que sous PRESSION budget.** La nature
16
+ d'une tache (code / mecanique) n'est plus un declencheur d'auto-delegation a elle
17
+ seule. Deleguer au Codex de l'abonnement (qui reste en dessous de Claude en
18
+ qualite) vaut le coup pour epargner le budget Claude, pas par tache de code. Hors
19
+ pression, le code tourne sur Claude sans rappel. Le seuil par defaut passe a 75 %
20
+ (`autodelegate-decision.js` + `codex-delegate-guard.js` alignes sur un seul
21
+ nombre). Sans jauge `budget`, la pression n'est pas calculee et rien n'est
22
+ propose (documente, non bloquant).
23
+ - **F2 — le garde `codex-delegate-guard` obeit au routeur.** Le blocage ne mord
24
+ que pour une tache que le `dispatch-router` route lui-meme vers Codex (nature
25
+ execution / shell / deploiement / devops / navigateur). Une tache
26
+ architecture / refactor / qualite / planif — et toute verification — route vers
27
+ Claude, donc le garde reste silencieux. Le garde recalcule router + pression
28
+ uniquement dans la branche ou un blocage est encore possible (les chemins
29
+ d'autorisation courants ne paient rien) ; les deux signaux echouent en mode
30
+ ouvert (signal irresolu -> on autorise, sans blocage a tort).
31
+
32
+ ### Changed — Echelle de modele par complexite + Fable dernier recours (F3)
33
+ - **Cote routeur (`dispatch-router.claudeModelForComplexity`)** : echelle a quatre
34
+ rungs — `haiku` (bas) -> `sonnet` (moyen) -> `opus` (haut) -> `fable` (extreme,
35
+ dernier recours, ~2x le prix d'Opus). C'est une RECOMMANDATION par tache, pas un
36
+ changement du modele de session en cours (Claude Code n'expose pas ce levier).
37
+ - **Cote leaves de workflow (`native-tiers` + linter)** : un up-tier explicite
38
+ (`opus` / `fable`) devient un choix d'auteur AUTORISE sur n'importe quel leaf,
39
+ miroir de l'echelle du routeur. Le plancher anti-downgrade est conserve (haiku /
40
+ sonnet interdits sur un leaf protege) ; seul le plafond est ouvert (le pin-up
41
+ n'est plus une violation). Nouveaux : `UP_TIER_MODELS`, `isUpTierModel`.
42
+ - **Revirement d'invariant assume** : l'ancienne ligne rouge "pas de Fable /
43
+ pas de pin-up" est levee cote Claude. Cote Codex elle tient (Codex ne peut pas
44
+ lancer un modele Claude) : `assertNoFable` garde le chemin Codex + `codex-bridge`.
45
+ - Doctrine alignee : `native-workflows.md`, `native-workflows-contract.md`,
46
+ `intelligent-dispatch.md`, `codex-auto-delegation.md`, skill `byan-byan`.
47
+
48
+ ## [2.51.0] - 2026-07-20
49
+
50
+ ### Changed — Delegation Codex : fermeture de l'auto-esquive (option B)
51
+ - Le blocage `codex-delegate-guard` ne peut plus etre contourne par BYAN lui-meme.
52
+ Retire : la marque de contenu auto-ecrite (`// BYAN-DELEGATE: reviewed`) et
53
+ l'auto-passe par re-soumission/fenetre de grace — les deux permettaient a Claude
54
+ de s'auto-accorder la sortie. Les seules sorties valides sont desormais : une
55
+ delegation Codex reelle ce tour, Codex indisponible (detecte via `codex --version`),
56
+ ou un opt-out HUMAIN ("reste sur claude" / "sans codex" dans la demande, ou le
57
+ fichier `.byan-codex-autodelegate/off`). Le motif "script court / latence / je
58
+ verifie" n'est plus une raison valide. Coeur pur `lib/codex-delegate-gate.js`
59
+ (`humanOptOutFromText`, `decideDelegateGate` durci) + `codex-delegate-guard.js`
60
+ (probe Codex, opt-out depuis la demande). Le rendu injecte (`autodelegate-decision`
61
+ renderNudge), le skill `byan-byan` et `docs/codex-auto-delegation.md` sont alignes.
62
+
63
+ ### Notes
64
+ - Constat de session : le CLI Codex de l'abo ChatGPT est plafonne a gpt-5.5 (5.6
65
+ refuse). Recherche croisee : gpt-5.5 est sous le meilleur Claude sur tous les
66
+ domaines mesures. Donc la delegation Codex vaut pour economiser le budget Claude,
67
+ pas pour la qualite. Prochain chantier (FD dedie) : delegation declenchee par la
68
+ PRESSION budget (~75%) au lieu de par-nature ; le blocage obeit au routeur ;
69
+ echelle modele Claude par complexite (haiku/sonnet/opus) + Fable en dernier recours.
70
+
12
71
  ## [2.50.0] - 2026-07-17
13
72
 
14
73
  ### Added — BYAN souverainete (les concepts BYAN pilotent Claude, ne se font plus doubler)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.50.0",
3
+ "version": "2.51.0",
4
4
  "description": "BYAN - Intelligent AI agent installer with multi-platform native support (Claude Code, Codex/OpenCode)",
5
5
  "bin": {
6
6
  "create-byan-agent": "bin/create-byan-agent-v2.js"
@@ -1,35 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- // PreToolUse hook — WI-1, the DENT for the armed Codex-delegation lane.
4
+ // PreToolUse hook — the armed Codex-delegation guard (v3 : router-obedient +
5
+ // pressure-gated). See lib/codex-delegate-gate.js for the policy.
5
6
  //
6
- // When the Codex lane is ARMED (yanstaller option on AND Codex linked) and the
7
- // current turn is about to WRITE delegable code without having delegated to Codex
8
- // first, DENY ONCE with the exact instruction to delegate. This is the tooth that
9
- // makes "delegate to Codex when armed" govern instead of being prose Claude can
10
- // double with a polite excuse (the session incident).
7
+ // It DENIES a code Write/Edit only when ALL hold : the lane is armed (yanstaller
8
+ // option on AND Codex linked), the ROUTER routes the task to Codex, we are under
9
+ // Claude budget PRESSURE, Codex is available, and no delegation happened this turn
10
+ // and the human has not opted out. Otherwise it allows. Never traps a turn on an
11
+ // internal error (fails open), always exits 0.
11
12
  //
12
- // Speed-bump, not a wall (the user's red line) :
13
- // - escape-hatch : `.byan-codex-autodelegate/off` silences it (shared with the
14
- // nudge hook one switch for the whole Codex-delegation posture).
15
- // - `// BYAN-DELEGATE: reviewed` in the file acknowledges a deliberate
16
- // Claude-lane choice (Codex unavailable, judgment/verify) and passes.
17
- // - deny-once : an identical resubmission passes, and a grace window after a
18
- // deny lets a multi-file build proceed without nagging on every file.
19
- //
20
- // Never traps a turn, always exits 0. Pure decision in lib/codex-delegate-gate.js;
21
- // armed/linked detection reused from codex-autodelegate.js; delegability from
22
- // autodelegate-decision.js; the turn transcript via transcript-read.js.
13
+ // The two computed signals (router decision, budget pressure) are only evaluated
14
+ // in the would-deny branch, so the common allow paths pay nothing. Both fail open
15
+ // (an unresolvable signal -> allow, never a wrong block).
23
16
 
24
17
  const fs = require('fs');
25
18
  const path = require('path');
19
+ const { spawnSync } = require('child_process');
20
+ const { pathToFileURL } = require('url');
26
21
  const gate = require('./lib/codex-delegate-gate');
27
22
  const { loadConfig, codexLinked, toggledOff } = require('./codex-autodelegate');
28
- const { looksDelegable } = require('./lib/autodelegate-decision');
29
- const { extractRecentMessages, extractLastAssistantText, contentToText } = require('./lib/transcript-read');
23
+ const { estimateClaudeUsage } = require('./lib/usage-estimator');
24
+ const { extractRecentMessages, contentToText } = require('./lib/transcript-read');
30
25
 
31
26
  const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
32
- const GRACE_MS = 120000;
27
+ const ROUTER_PATH = path.resolve(__dirname, '../../_byan/mcp/byan-mcp-server/lib/dispatch-router.js');
28
+ const DEFAULT_PRESSURE_THRESHOLD = 75; // % of the Claude budget window
33
29
 
34
30
  function readStdin() {
35
31
  return new Promise((resolve) => {
@@ -40,44 +36,47 @@ function readStdin() {
40
36
  });
41
37
  }
42
38
 
43
- function allow() {
44
- return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } };
45
- }
46
-
47
- function deny(reason) {
48
- return {
49
- hookSpecificOutput: {
50
- hookEventName: 'PreToolUse',
51
- permissionDecision: 'deny',
52
- permissionDecisionReason: reason,
53
- },
54
- };
55
- }
39
+ const allow = () => ({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } });
40
+ const deny = (reason) => ({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } });
56
41
 
57
- function sidecarPath(root) {
58
- return path.join(root, '.byan-codex-delegate', 'last-deny.json');
42
+ // Codex availability probe : `codex --version` exits 0. Absent/broken -> false ->
43
+ // the guard must NOT deny (staying on Claude is the legit fallback).
44
+ function probeCodexAvailable(runner = spawnSync) {
45
+ try {
46
+ const res = runner('codex', ['--version'], { timeout: 8000, encoding: 'utf8' });
47
+ return res && res.status === 0;
48
+ } catch (_e) {
49
+ return false;
50
+ }
59
51
  }
60
52
 
61
- function readPriorDeny(root) {
53
+ // routerSaysCodex(text) : does dispatch-router route this task's text to Codex?
54
+ // The router keyword-matches on the text (execution/shell/deploy/script...). ESM
55
+ // module dynamically imported from this CJS hook. Fails open (false = don't block)
56
+ // if the router cannot be loaded.
57
+ async function defaultRouteProbe(text) {
62
58
  try {
63
- const p = JSON.parse(fs.readFileSync(sidecarPath(root), 'utf8'));
64
- return p && typeof p === 'object' ? p : null;
59
+ const mod = await import(pathToFileURL(ROUTER_PATH).href);
60
+ return mod.routeRuntime(String(text || '')) === 'codex';
65
61
  } catch (_e) {
66
- return null;
62
+ return false;
67
63
  }
68
64
  }
69
65
 
70
- function writePriorDeny(root, entry) {
66
+ // underPressure : Claude budget usage >= threshold. Needs a budget in the config
67
+ // (no budget -> pct null -> not under pressure -> no delegation, documented).
68
+ function computeUnderPressure(config) {
71
69
  try {
72
- fs.mkdirSync(path.dirname(sidecarPath(root)), { recursive: true });
73
- fs.writeFileSync(sidecarPath(root), JSON.stringify(entry));
70
+ const budget = config && config.budget ? config.budget : null;
71
+ if (!budget) return false;
72
+ const threshold = typeof config.threshold === 'number' ? config.threshold : DEFAULT_PRESSURE_THRESHOLD;
73
+ const usage = estimateClaudeUsage({ budget });
74
+ return typeof usage.pct === 'number' && usage.pct >= threshold;
74
75
  } catch (_e) {
75
- // best-effort : losing the deny memory only costs one extra deny, never a trap
76
+ return false;
76
77
  }
77
78
  }
78
79
 
79
- // The last user message text this turn, for the delegability signal (a code
80
- // target already suffices, this only broadens it).
81
80
  function lastUserText(payload) {
82
81
  const msgs = extractRecentMessages(payload, 12);
83
82
  if (!Array.isArray(msgs)) return '';
@@ -87,45 +86,38 @@ function lastUserText(payload) {
87
86
  return '';
88
87
  }
89
88
 
90
- // Pure-ish assembly of the decision from a payload (exported for tests, fs
91
- // injected via opts.root + opts.now).
92
- function runGuard(payload, { root = ROOT, now = Date.now() } = {}) {
89
+ // runGuard async (the router import is async). opts inject the probes for tests.
90
+ async function runGuard(payload, {
91
+ root = ROOT,
92
+ codexProbe = probeCodexAvailable,
93
+ routeProbe = defaultRouteProbe,
94
+ pressureProbe = computeUnderPressure,
95
+ } = {}) {
93
96
  const toolName = payload.tool_name || payload.toolName || '';
94
97
  if (toolName !== 'Write' && toolName !== 'Edit') return allow();
95
98
 
96
99
  const config = loadConfig(root);
97
100
  const armed = config.enabled === true && codexLinked();
98
- const escaped = toggledOff(root);
101
+ if (!armed) return allow(); // fast path, no transcript/probe work when off
99
102
 
103
+ const escaped = toggledOff(root);
100
104
  const input = payload.tool_input || payload.toolInput || {};
101
105
  const filePath = input.file_path || input.filePath || '';
102
- const content = toolName === 'Edit'
103
- ? String(input.new_string || input.newString || '')
104
- : String(input.content || '');
105
-
106
- const userText = lastUserText(payload);
107
- const delegable = gate.targetIsCode(filePath) || looksDelegable(userText);
108
- const reviewedMarker = gate.hasReviewedMarker(content) || gate.hasReviewedMarker(extractLastAssistantText(payload));
106
+ const isCode = gate.targetIsCode(filePath);
107
+ const humanOptOut = gate.humanOptOutFromText(lastUserText(payload));
109
108
  const delegationSeen = gate.delegationSeenThisTurn(extractRecentMessages(payload, 12) || []);
110
- const turnHash = gate.hashTurn(filePath, content);
109
+
110
+ // Only compute the two expensive signals when a deny is otherwise possible.
111
+ const maybeDeny = !escaped && !humanOptOut && isCode && !delegationSeen;
112
+ const routerSaysCodex = maybeDeny ? await routeProbe(lastUserText(payload)) : false;
113
+ const underPressure = (maybeDeny && routerSaysCodex) ? pressureProbe(config) : false;
114
+ const codexAvailable = (maybeDeny && routerSaysCodex && underPressure) ? codexProbe() : true;
111
115
 
112
116
  const decision = gate.decideDelegateGate({
113
- armed,
114
- delegable,
115
- delegationSeen,
116
- escaped,
117
- reviewedMarker,
118
- priorDeny: readPriorDeny(root),
119
- turnHash,
120
- now,
121
- graceMs: GRACE_MS,
117
+ armed, routerSaysCodex, targetIsCode: isCode, underPressure,
118
+ delegationSeen, escaped, humanOptOut, codexAvailable,
122
119
  });
123
-
124
- if (decision.decision === 'deny') {
125
- writePriorDeny(root, { hash: turnHash, ts: now });
126
- return deny(decision.reason);
127
- }
128
- return allow();
120
+ return decision.decision === 'deny' ? deny(decision.reason) : allow();
129
121
  }
130
122
 
131
123
  if (require.main === module) {
@@ -133,7 +125,7 @@ if (require.main === module) {
133
125
  let out;
134
126
  try {
135
127
  const payload = JSON.parse((await readStdin()) || '{}');
136
- out = runGuard(payload);
128
+ out = await runGuard(payload);
137
129
  } catch (_e) {
138
130
  out = allow();
139
131
  }
@@ -142,4 +134,4 @@ if (require.main === module) {
142
134
  })();
143
135
  }
144
136
 
145
- module.exports = { runGuard, sidecarPath, readPriorDeny, writePriorDeny, lastUserText };
137
+ module.exports = { runGuard, probeCodexAvailable, defaultRouteProbe, computeUnderPressure, lastUserText };
@@ -22,7 +22,7 @@
22
22
 
23
23
  const { perfFavors } = require('./perf-routing');
24
24
 
25
- const DEFAULT_THRESHOLD = 80;
25
+ const DEFAULT_THRESHOLD = 75;
26
26
  const DEFAULT_INVOCATION = 'codex:codex-rescue --model gpt-5.4';
27
27
  const RED_LINE = 'delegable work only (code / mechanical) — judgment, analysis, soul and verification stay on Claude';
28
28
 
@@ -71,16 +71,12 @@ function decideAutodelegation({ requestText = '', usage = null, config = {} } =
71
71
  };
72
72
  }
73
73
 
74
- if (delegable) {
75
- return {
76
- delegate: true,
77
- mode: 'delegable-only',
78
- pct,
79
- reason: 'request looks like delegable coding work — propose handing it to Codex to spare the Claude 5h budget',
80
- redLine: RED_LINE,
81
- invocation,
82
- };
83
- }
74
+ // v3 : NATURE alone is no longer an auto-delegate trigger. The user's decision
75
+ // is PRESSURE-only — delegating to the subscription Codex (which trails Claude
76
+ // on quality) is worth it to spare the Claude budget, not per coding task. So a
77
+ // delegable task off-pressure does NOT propose Codex. `delegable` is kept only as
78
+ // a signal the perf-routing opt-in below can still use.
79
+ void delegable;
84
80
 
85
81
  // PERF (opt-in): the forces table (user-populated) may favor Codex for this
86
82
  // kind of task even at low pressure. Off by default; always heuristic.
@@ -141,8 +137,10 @@ function renderNudge(decision) {
141
137
  `[BYAN auto-delegate]${gauge}: ${scope} via \`${invocation}\` `
142
138
  + '(runs on the ChatGPT subscription, no API credit).',
143
139
  `Red line: ${decision.redLine}. The lane is ARMED (option on AND Codex linked), so this is a `
144
- + 'DIRECTIVE, not a mere suggestion: delegate the delegable part unless you have a specific reason '
145
- + "not to, fall back to Claude only if Codex is unavailable, and verify Codex's output before commit.",
140
+ + 'DIRECTIVE, not a suggestion: delegate the delegable part. The ONLY valid reasons to stay on '
141
+ + 'Claude are (1) Codex unavailable, or (2) the user asked to stay on Claude. "Small script / '
142
+ + 'latency / I verify anyway" is NOT a valid reason — the codex-delegate-guard denies a delegable '
143
+ + "code write that skips Codex. Verify Codex's output before commit.",
146
144
  ].join(' ');
147
145
  }
148
146
 
@@ -26,7 +26,8 @@ A native workflow script mutates FD/strict state only through the `byan_fd_*` /
26
26
 
27
27
  Each `agent()` leaf runs on the session model unless the call sets `opts.model`.
28
28
  The tiering decision lives in one place — `_byan/mcp/byan-mcp-server/lib/native-tiers.js`
29
- (tier vocabulary, leaf classifier, model map). Three tiers:
29
+ (tier vocabulary, leaf classifier, model map). Three AUTO-routed tiers, plus an
30
+ explicit UP-TIER an author may pin by complexity (v3):
30
31
 
31
32
  - **cheap (`model: 'haiku'`)** — a pure EXPLORATION leaf (read/load/parse/detect).
32
33
  - **balanced (`model: 'sonnet'`)** — two classes land here. (1) MECHANICAL
@@ -40,16 +41,25 @@ The tiering decision lives in one place — `_byan/mcp/byan-mcp-server/lib/nativ
40
41
  leaf may carry `model: 'sonnet'` (its tier) but not `haiku` (`analysis-below-tier`).
41
42
  Semantic/adversarial VERIFICATION is NOT analysis and stays deep.
42
43
  - **deep (OMIT `opts.model`)** — implementation + verification leaves inherit the
43
- session model (no pin-up to opus). A genuinely frontier ANALYSIS leaf opts back to
44
- deep with the `deep-` label prefix (`deep-assess-architecture`), the mirror of the
45
- `mech-` opt-in.
44
+ session model (the cost-safe default: no AUTO pin-up). A genuinely frontier
45
+ ANALYSIS leaf opts back to deep with the `deep-` label prefix
46
+ (`deep-assess-architecture`), the mirror of the `mech-` opt-in.
47
+ - **up-tier (`model: 'opus'` / `model: 'fable'`)** — an EXPLICIT authoring choice
48
+ to raise a genuinely complex leaf ABOVE the inherited tier (v3). `opus` for a
49
+ hard leaf, `fable` as the last-resort top-reasoning model for extreme complexity
50
+ (~2x Opus price). This is the workflow-leaf mirror of the dispatch-router
51
+ complexity ladder (haiku -> sonnet -> opus -> fable). Auto-routing does not pick
52
+ it — the author writes it deliberately. The anti-downgrade floor does not apply
53
+ upward, so the linter allows an up-tier pin on any leaf (it costs more, it does
54
+ not regress quality). The old blanket "no pin-up / no Fable" ban is lifted.
46
55
 
47
56
  The linter splits the two directions:
48
57
 
49
- - **Floor (HARD, blocks the commit)** — a downgrade model on a PROTECTED leaf, a
50
- pin-up, or a half-applied `mech-` opt-in is a contract violation
58
+ - **Floor (HARD, blocks the commit)** — a DOWNGRADE model (haiku/sonnet) on a
59
+ PROTECTED leaf, or a half-applied `mech-` opt-in, is a contract violation
51
60
  (`modelRoutingViolations` + `mechanicalLabelViolations`). This is the STRICT-2
52
- No Downgrade net.
61
+ No Downgrade net. An UP-TIER pin (opus/fable) is not a violation — the floor
62
+ guards downward only.
53
63
  - **Ceiling (ADVISORY, non-blocking)** — an exploration- OR analysis-labelled leaf
54
64
  that runs deep is *reported* (`byan-lint-workflows.js --advise`:
55
65
  `untiered-exploration` -> `model: 'haiku'`, `untiered-analysis` -> `model: 'sonnet'`),
@@ -21,8 +21,10 @@ the user should not have to ask for it. Four moments:
21
21
  (`_byan/mcp/byan-mcp-server/lib/agent-matcher.js`), the right model + effort
22
22
  (`dispatch-router.js` / native-tiers), and the right runtime — Codex for
23
23
  execution / shell / deploy / devops / browser ; Claude for architecture /
24
- refactor / quality / planning ; verification stays on Claude ; Fable is not
25
- emitted. No user ask. A suited agent -> use it. NO suited agent -> interview to
24
+ refactor / quality / planning ; verification stays on Claude. The Claude model
25
+ scales on a four-rung complexity ladder (haiku -> sonnet -> opus -> fable, fable
26
+ the extreme last resort) ; Fable is refused on the Codex path only. No user ask.
27
+ A suited agent -> use it. NO suited agent -> interview to
26
28
  frame the need, web-research the trade's competencies + best practices, create
27
29
  the tailored agent (the one place the human is required upstream).
28
30
  3. **Execute in party-mode with a live visual.** The user SEES what happens, live:
@@ -30,13 +32,17 @@ the user should not have to ask for it. Four moments:
30
32
  `TaskCreate` / `TaskUpdate`) plus the dispatch table shown at the top of BUILD.
31
33
  - **Codex-lane** — when the lane is ARMED (yanstaller option on AND Codex
32
34
  linked, i.e. `_byan/_config/autodelegate.json` present with `enabled:true`
33
- AND `~/.codex/auth.json` or `CODEX_API_KEY`) and the task is delegable,
35
+ AND `~/.codex/auth.json` or `CODEX_API_KEY`) and the task is delegable code,
34
36
  ACTUALLY delegate via `codex-bridge.js` (`codex exec` -> unified diff -> YOU
35
- apply it). Fall back to Claude if Codex is unavailable. When armed, the
36
- injected `[BYAN auto-delegate]` note is a DIRECTIVE, not a mere suggestion:
37
- you delegate the delegable work rather than doing it on Claude by default.
38
- When NOT armed (option off or Codex not linked), there is no delegation
39
- you run it on Claude.
37
+ apply it) or the `codex:codex-rescue` agent. This is a DIRECTIVE, enforced by
38
+ the `codex-delegate-guard` PreToolUse tooth : it DENIES a delegable code
39
+ Write/Edit that skipped Codex. The ONLY valid reasons to stay on Claude are
40
+ (1) Codex unavailable (auto-detected), or (2) the USER asked to stay on Claude
41
+ ("reste sur Claude" / "sans codex", or the `.byan-codex-autodelegate/off`
42
+ switch). "Small script / latency / I verify anyway" is NOT valid — that is
43
+ exactly the self-dodge the tooth closes (option B). You cannot self-grant a
44
+ bypass. When NOT armed (option off or Codex not linked), there is no
45
+ delegation — you run it on Claude.
40
46
  - **Claude-lane** — run it on Claude at the chosen model.
41
47
  4. **User gate at the END.** The user-validation gate lands at the end, once the
42
48
  feature/task is done and there is a deliverable to review — not upstream. (In a
@@ -129,9 +135,9 @@ Never call `byan_update_apply` without explicit user consent. That tool returns
129
135
  - nature `exploration` (load/read/scan/list/parse/fetch...) → `haiku`
130
136
  - nature `mechanical` (binary judgment-free checks : JSON parses, schema matches, lint passes — label prefix `mech-`, explicit opt-in only) → `sonnet`
131
137
  - nature `implementation` / `verification` / `analysis` / unknown → deep = **inherit the session model**
132
- - Keep protected work (verify/analysis/implement) off haiku/sonnet regardless of size ; no pin-up to opus. Pass an explicit `nature` to `byan_dispatch` when you know it.
138
+ - Keep protected work (verify/analysis/implement) off haiku/sonnet regardless of size. Auto-routing does not pin up ; but an author MAY deliberately UP-TIER a genuinely complex leaf to `opus` (hard) or `fable` (extreme, last resort) — the linter allows it (the floor guards downward only). Pass an explicit `nature` to `byan_dispatch` when you know it.
133
139
  - **Inside native workflow scripts** (`.claude/workflows/*.js` OR ad-hoc) the SAME tiering applies per `agent()` leaf via `opts.model`, enforced as a FLOOR not a ceiling on TWO nets :
134
- - **Repo linter** (committed scripts, pre-commit) : `modelRoutingViolations` HARD-blocks a downgrade on a protected leaf, a pin-up, or a half-applied `mech-` opt-in (`mechanical-without-model` / `mechanical-below-tier`) ; an exploration-labelled leaf left deep is a NON-blocking ADVISORY (`byan-lint-workflows.js --advise`), since many such leaves bear a gate/classification/exact-conversion and must stay deep — the human owns that call.
140
+ - **Repo linter** (committed scripts, pre-commit) : `modelRoutingViolations` HARD-blocks a downgrade (haiku/sonnet) on a protected leaf, or a half-applied `mech-` opt-in (`mechanical-without-model` / `mechanical-below-tier`) ; an UP-TIER pin (opus/fable) is allowed, not blocked (the floor guards downward only) ; an exploration-labelled leaf left deep is a NON-blocking ADVISORY (`byan-lint-workflows.js --advise`), since many such leaves bear a gate/classification/exact-conversion and must stay deep — the human owns that call.
135
141
  - **Tier gate hook** (EVERY Workflow invocation, inline or scriptPath) : `tier-script-guard.js` (PreToolUse) runs the same analysis (`lib/tier-script.js`) and DENIES ONCE when exploration/`mech-` leaves have no tier, with the exact leaf list to fix. Acknowledge deliberate deep choices with the `// BYAN-TIER: reviewed` comment marker ; an identical resubmission passes (deny-once by design, no trap). Every decision lands in `_byan-output/tier-ledger.jsonl` (the measurement basis for token gains). Escape hatch : `.byan-tier/off`.
136
142
  - **Authoring aid** : BEFORE writing a script, call `byan_dispatch` with `{ leaves: [{ label, nature? }] }` (batch mode) to get the `opts.model` per leaf ; write `model:` only where non-null. Report with `node _byan/mcp/byan-mcp-server/bin/byan-tier-script.js <file> [--json]`.
137
143
  - No per-leaf effort knob exists (the API exposes only `model`), so effort-by-complexity reduces to model-by-complexity.
@@ -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": "fcea02854887283c8ff1d4b7d09778d62fc7ce52959a00577e87091e7c31f991"
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 ;
@@ -95,16 +95,21 @@ tier vocabulary, the leaf classifier, and the model map.
95
95
 
96
96
  | Tier | `opts.model` | Used for |
97
97
  |------|--------------|----------|
98
+ | `up-tier` | `opus` / `fable` | EXPLICIT authoring choice: raise a genuinely complex leaf above the inherited tier (opus for hard, fable for extreme, last resort) |
98
99
  | `deep` | **omitted** (inherit the session model) | implement, verify, analysis — the default |
99
100
  | `balanced` | `sonnet` | MECHANICAL verification, opt-in only via the `mech-` label prefix |
100
101
  | `cheap` | `haiku` | a pure exploration leaf: read / load / parse / detect |
101
102
 
102
103
  Three hard rules:
103
104
 
104
- - **No pin-up.** `deep` is an omission, not `model: 'opus'`. Omitting lets a
105
- leaf inherit whatever the session runs Opus by default, Sonnet if the user
106
- chose Sonnet. Pinning a fixed high tier would override that and could silently
107
- downgrade a Sonnet/Opus session's heavy leaf.
105
+ - **No AUTO pin-up, but an explicit up-tier is allowed (v3).** Auto-routing does
106
+ not raise a leaf above the inherited tier: `deep` is an omission, letting a leaf
107
+ inherit whatever the session runs (Opus by default, Sonnet if the user chose
108
+ Sonnet). An author MAY still deliberately pin `opus` or `fable` on a genuinely
109
+ complex leaf — the workflow-leaf mirror of the dispatch-router complexity ladder
110
+ (haiku -> sonnet -> opus -> fable). The anti-downgrade floor does not apply
111
+ upward, so the linter allows an up-tier pin; the old blanket "no pin-up / no
112
+ Fable" ban is lifted.
108
113
  - **Only exploration and mech- downgrade.** A leaf is pinned to `cheap` only
109
114
  when it is unambiguous read/extract work. `classifyLeaf` keys off the LABEL
110
115
  (the prompt is too noisy — an exploration leaf often says "report what you
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.50.0",
3
+ "version": "2.52.0",
4
4
  "description": "BYAN v2.8 - Intelligent AI agent creator with ELO trust system + scientific fact-check + Hermes universal dispatcher + native Claude Code integration (hooks, skills, MCP server). Multi-platform (Claude Code, Codex). Merise Agile + TDD + 71 Mantras. ~54% LLM cost savings.",
5
5
  "main": "src/index.js",
6
6
  "bin": {