create-byan-agent 2.38.0 → 2.41.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/install/bin/create-byan-agent-v2.js +40 -0
  3. package/install/lib/codex-autodelegate-setup.js +100 -0
  4. package/install/templates/.claude/hooks/codex-autodelegate.js +74 -0
  5. package/install/templates/.claude/hooks/lib/autodelegate-decision.js +152 -0
  6. package/install/templates/.claude/hooks/lib/perf-routing.js +47 -0
  7. package/install/templates/.claude/hooks/lib/strict-config.json +14 -0
  8. package/install/templates/.claude/hooks/lib/usage-estimator.js +262 -0
  9. package/install/templates/.claude/hooks/tier-script-guard.js +185 -0
  10. package/install/templates/.claude/rules/native-workflows.md +33 -7
  11. package/install/templates/.claude/settings.json +13 -0
  12. package/install/templates/.claude/skills/byan-byan/SKILL.md +7 -2
  13. package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +3 -1
  14. package/install/templates/.claude/skills/byan-strict/SKILL.md +9 -0
  15. package/install/templates/.claude/workflows/sprint-planning.js +2 -2
  16. package/install/templates/.claude/workflows/testarch-trace.js +3 -2
  17. package/install/templates/.githooks/pre-commit +18 -0
  18. package/install/templates/_byan/_config/strict-mode.yaml +25 -0
  19. package/install/templates/_byan/agent/byan/byan-soul.md +40 -0
  20. package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-tier-script.js +52 -0
  21. package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch.js +22 -0
  22. package/install/templates/_byan/mcp/byan-mcp-server/lib/native-tiers.js +26 -9
  23. package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +40 -2
  24. package/install/templates/_byan/mcp/byan-mcp-server/lib/tier-script.js +104 -0
  25. package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-lint.js +92 -6
  26. package/install/templates/_byan/mcp/byan-mcp-server/server.js +22 -7
  27. package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +3 -3
  28. package/install/templates/dist/skill-bundles/byan-byan.zip +0 -0
  29. package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
  30. package/install/templates/dist/skill-bundles/byan-strict.zip +0 -0
  31. package/install/templates/docs/native-workflows-contract.md +55 -12
  32. package/package.json +1 -1
  33. package/src/loadbalancer/capability-matrix.js +14 -0
  34. package/src/loadbalancer/degradation-ladder.js +121 -0
  35. package/src/loadbalancer/loadbalancer.default.yaml +23 -1
  36. package/src/loadbalancer/mcp-server.js +200 -18
  37. package/src/loadbalancer/providers/codex-provider.js +260 -0
  38. package/src/loadbalancer/providers/factory.js +36 -0
  39. package/src/loadbalancer/subscription-window.js +142 -0
  40. package/src/loadbalancer/switch-tolerance.js +63 -0
  41. package/src/loadbalancer/tools/index.js +17 -2
package/CHANGELOG.md CHANGED
@@ -9,6 +9,171 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ### Added - Codex auto-delegation (opt-in, native)
13
+ - **BYAN now proposes handing delegable work to Codex on your ChatGPT
14
+ subscription (no API credit) when Claude nears its 5h limit.** A
15
+ `UserPromptSubmit` hook estimates the rolling-5h Claude consumption from the
16
+ local transcripts (live) + session-meta (fallback), and nudges delegation on
17
+ three triggers: pressure (>= 80% estimated, configurable), task nature
18
+ (delegable coding work), and an opt-in perf forces table. The red line holds:
19
+ only delegable natures are proposed; judgment / soul / verification stay on
20
+ Claude. Disarmed by default — the yanstaller arms it on opt-in (device-flow
21
+ `codex login --device-auth`, entitled model gpt-5.4) by writing
22
+ `_byan/_config/autodelegate.json`. The 5h gauge is an honest ESTIMATE (no
23
+ provider exposes a machine-readable quota; `pct` is null without a configured
24
+ budget), and perf routing ships neutral (below the L2 perf floor, tagged
25
+ heuristic). New: `.claude/hooks/codex-autodelegate.js` +
26
+ `.claude/hooks/lib/{usage-estimator,autodelegate-decision,perf-routing}.js`,
27
+ `install/lib/codex-autodelegate-setup.js`. 47 unit tests. See
28
+ `docs/codex-auto-delegation.md`.
29
+
30
+ ### Fixed
31
+ - **lb: Codex pool targeted a non-entitled model on ChatGPT subscription.** The
32
+ provider defaulted to `gpt-5-codex`; the OpenAI backend rejects every
33
+ `-codex`-suffixed model on a subscription account (API-key only). Default is
34
+ now the entitled `gpt-5.4`, and a new pure `resolveCodexModel({ requested,
35
+ authPool })` remaps a `-codex` request to `gpt-5.4` under subscription auth
36
+ only (passthrough on api-key or an already-plain id). `loadbalancer.default.yaml`
37
+ codex models set to `gpt-5.4`. Covered by 7 new unit tests (mock runner, no
38
+ real CLI/network). See `docs/loadbalancer-multipool.md`.
39
+
40
+ ## [2.41.0] - 2026-07-03
41
+
42
+ ### Added - multi-pool subscription arbitrage: Codex as a second pool + the 5h-window ladder
43
+
44
+ The load-balancer had claude + copilot + byan_api but no OpenAI pool, and its
45
+ pressure-score modelled API-burst (429) pressure, not the rolling 5h subscription
46
+ window that users actually hit. This release adds the second pool and the window
47
+ tracking that switches before the wall, without denaturing BYAN.
48
+
49
+ - **CodexProvider** (`src/loadbalancer/providers/codex-provider.js`) - wraps the
50
+ `codex exec --json` SYSTEM CLI (not an npm SDK); degrades to disabled if the
51
+ binary is absent; two auth pools (CODEX_API_KEY per-token, else the
52
+ ChatGPT-subscription session). Rate-limit exhaustion read from stderr (no
53
+ machine-readable quota, OpenAI issue #10233). Registered in the yaml +
54
+ capability-matrix; provider factory (`providers/factory.js`).
55
+ - **subscription-window tracker** (`subscription-window.js`) - per-pool rolling-5h
56
+ + weekly token burn, a `window-proximity` signal (distinct from 429-pressure) +
57
+ ETA. Honest estimate: `null` proximity without a configured budget, no
58
+ fabricated percentage.
59
+ - **Execution stubs unblocked** (`mcp-server.js`) - `lb_send` / `lb_switch` /
60
+ `lb_get_context` now really route / transfer context / read state; the
61
+ SessionBridge and GracefulDegradation (previously orphaned) are wired; all
62
+ seams injectable so tests avoid spawning codex or hitting the OpenAI quota.
63
+ - **switch-tolerance** (`switch-tolerance.js`) - the red line: only delegable
64
+ natures (exploration / mechanical / implementation) may cross to Codex;
65
+ verification / analysis / soul / identity / review / gate stay on Claude and
66
+ queue rather than denature.
67
+ - **4-rung degradation ladder** (`degradation-ladder.js`) - HEALTHY ->
68
+ PRIMARY_HOT -> PRIMARY_EXHAUSTED -> ALL_EXHAUSTED, driven by the window
69
+ proximity, obeying the red line at every rung. `planRoute(nature)` on the live
70
+ shell.
71
+ - **lb_budget** MCP tool + `getBudget()` - the anti-"5h limit reached" dashboard:
72
+ per-pool 5h/weekly burn, proximity, ETA, rung, with the honest estimate/
73
+ doubles-the-ceiling note.
74
+ - **Docs** - `docs/loadbalancer-multipool.md` (architecture, the red line, the two
75
+ hard truths, honest shipping status).
76
+
77
+ ### Added - native workflow model tiering: the sonnet tier lives, ad-hoc scripts are gated
78
+
79
+ Claude Code's Workflow tool runs every `agent()` leaf on the session model
80
+ unless the script pins `opts.model`, and the tiering contract only reached
81
+ committed `.claude/workflows/*.js` through the repo linter. Two consequences,
82
+ both observed live: ad-hoc scripts (written inline for one run) executed
83
+ all-deep (14 agents on Opus for one review), and the `balanced` tier was
84
+ unreachable (0/131 committed leaves on sonnet). This release closes both.
85
+
86
+ - **`mech-` opt-in class (native-tiers)** - a `mech-` label prefix
87
+ (`mech-validate-json`) declares a MECHANICAL verification: a binary,
88
+ judgment-free check (JSON parses, schema matches, lint passes) that tiers to
89
+ `balanced` (sonnet). Explicit opt-in only, no keyword fuzziness:
90
+ `validate-json` without the prefix stays protected (deep). The linter holds
91
+ the script to the declaration: `mechanical-without-model` and
92
+ `mechanical-below-tier` are hard contract violations.
93
+ - **tier-script engine + CLI** - `lib/tier-script.js` analyzes any workflow
94
+ script TEXT (committed, ad-hoc or draft): one verdict per labelled leaf
95
+ against native-tiers, plus the deny-once gate decision. Parsing stays in
96
+ `workflows-lint.js` (`extractLabelledLeaves`). `bin/byan-tier-script.js`
97
+ prints the report (exit 0 clean/acknowledged, 1 gaps, 2 violations).
98
+ - **tier gate hook** - `.claude/hooks/tier-script-guard.js` (PreToolUse,
99
+ matcher `Workflow`) gates EVERY Workflow invocation at the one chokepoint an
100
+ ad-hoc script crosses. Undecided exploration/`mech-` leaves deny ONCE with
101
+ the exact leaf list; `// BYAN-TIER: reviewed` acknowledges deliberate deep
102
+ choices; an identical resubmission passes (deny-once by design); registry
103
+ invocations pass. It rewrites nothing (STRICT-2 No Downgrade). Every
104
+ decision lands in `_byan-output/tier-ledger.jsonl` with a per-model
105
+ histogram - the measurement basis for token gains. Escape hatch:
106
+ `.byan-tier/off`.
107
+ - **`byan_dispatch` batch mode** - `{ leaves: [{ label, nature? }] }` returns
108
+ the `opts.model` per planned leaf BEFORE the script is written; the nature
109
+ enum gains `mechanical` on both axes.
110
+ - **Docs** - `native-workflows.md` rule + `docs/native-workflows-contract.md`
111
+ describe the three live tiers, the two nets (repo linter floor + tier gate
112
+ hook) and the authoring flow; byan-byan and hermes-dispatch skills carry the
113
+ same doctrine.
114
+
115
+ ### Added - shippable soul stays in sync with the active soul (byan-sync-soul)
116
+
117
+ BYAN's active identity (`_byan/agent/byan/soul.md`, `tao.md`) is shipped to a
118
+ fresh install via the prefixed copies `byan-soul.md` / `byan-tao.md`, which the
119
+ installer copies into place at setup. Those copies were a MANUAL mirror and had
120
+ drifted: `byan-soul.md` had missed the `## Valeurs` section and the 2026-07-02
121
+ couche-vivante revision, so a fresh install shipped a stale identity. The
122
+ 2026-03-27 revision had warned that soul transmission needs maintenance — the
123
+ manual sync did not hold.
124
+
125
+ - **New generator `byan-sync-soul`** (`lib/sync-soul.js` + `bin/byan-sync-soul.js`)
126
+ mirrors the active soul + tao into the shippable prefixed copies. `--check`
127
+ reports drift and exits non-zero.
128
+ - **Pre-commit gate**: `.githooks/pre-commit` runs `byan-sync-soul --check`, so a
129
+ commit whose shippable soul drifted from the active one is blocked. Dev-repo
130
+ tooling — the bin is not shipped, so the gate no-ops in installed projects
131
+ (`[ -f ]` guard), where it is not needed.
132
+ - **Caught up**: `byan-soul.md` now carries the `## Valeurs` section and the
133
+ 2026-07-02 revision; `byan-tao.md` was already in sync.
134
+ - **soul-memory is out of scope**: `byan-soul-memory.md` is a curated seed journal
135
+ (distinct from this repo's living `soul-memory.md`), so it is not mirrored.
136
+
137
+ Files: `_byan/mcp/byan-mcp-server/lib/sync-soul.js` + `bin/byan-sync-soul.js` +
138
+ `test/sync-soul.test.js`, `.githooks/pre-commit` (+ template mirror),
139
+ `_byan/agent/byan/byan-soul.md` (+ template mirror). Adversarial review
140
+ (bmad-compliance) approved, 0 must_fix; it corrected an inaccurate rationale in
141
+ the comments (the installer copies, it does not rename), fixed before merge.
142
+ MCP node --test 733/733, jest root 2473/2473.
143
+
144
+ ### Added - strict self-verify checklist from measured recurring gaps
145
+
146
+ Strict mode now carries a self-verify checklist of BYAN's OWN most frequent
147
+ blind spots, so each self-verify pass checks them on top of the locked
148
+ acceptance criteria. The three themes are not guessed — they were harvested from
149
+ the strict audit log by `byan_insight_digest` (self-verify gap clustering):
150
+ tests/coverage (observed 20x), doc-follows-code (10x), scope-discovery (7x). The
151
+ heterogeneous "other" cluster (18x) was deliberately excluded — too mixed to
152
+ become a single check.
153
+
154
+ - **Single source of truth**: the checklist lives under `self_verify.checklist`
155
+ in `_byan/_config/strict-mode.yaml`. Each item is a testable question plus an
156
+ `observed` count kept as the WHY (a signal, not a target).
157
+ - **Propagated by the generator**: `byan-sync-rules` renders it into the three
158
+ operative surfaces — the `byan-strict` SKILL (a `## Self-verify checklist`
159
+ section), `.claude/hooks/lib/strict-config.json` (`self_verify_checklist`, with
160
+ `observed` stripped as source-only), and the `AGENTS.md` block (Codex parity).
161
+ Idempotent; an older config with no checklist renders an empty list and no
162
+ section.
163
+ - **Anti-drop**: a test asserts the real `strict-mode.yaml` carries the three
164
+ themes and that they survive the generator, so a future edit cannot silently
165
+ drop them. Additive only — `min_passes`, `last_verdict_must_be`, mantras,
166
+ banners and the scope guard are unchanged.
167
+
168
+ This closes the insight loop from the 2026-07-02 soul revision: a measured,
169
+ recurring gap becomes a mechanized check rather than a remembered intention.
170
+
171
+ Files: `_byan/_config/strict-mode.yaml`, `_byan/mcp/byan-mcp-server/lib/sync-rules.js`,
172
+ `_byan/mcp/byan-mcp-server/test/sync-rules.test.js`, regenerated
173
+ `.claude/skills/byan-strict/SKILL.md` + `.claude/hooks/lib/strict-config.json` +
174
+ `AGENTS.md` (+ install template mirrors). Adversarial review (bmad-compliance)
175
+ approved, 0 must_fix, 7/7 criteria. MCP node --test 726/726, jest root 2473/2473.
176
+
12
177
  ## [2.38.0] - 2026-06-30
13
178
 
14
179
  ### Added - yanstaller installs the byan-channel MCP entry by default (inert)
@@ -20,6 +20,7 @@ const { setupRtkIntegration, shouldOfferRtk } = require('../lib/rtk-integration'
20
20
  const { setupGdocPublish, shouldOfferGdoc } = require('../lib/gdoc-setup');
21
21
  const { setupClaudeNative } = require('../lib/claude-native-setup');
22
22
  const { setupCodexNative } = require('../lib/codex-native-setup');
23
+ const { setupCodexAutodelegate, DEVICE_FLOW_INSTRUCTION } = require('../lib/codex-autodelegate-setup');
23
24
  const { setupMcpExtensions } = require('../lib/mcp-extensions');
24
25
  const { setupStagingConsent } = require('../lib/staging-consent');
25
26
  const { getLatestVersion, compareVersions } = require('../lib/utils/version-compare');
@@ -1334,6 +1335,45 @@ async function install(options = {}) {
1334
1335
  )
1335
1336
  );
1336
1337
  }
1338
+
1339
+ // Optional: opt into a Codex BACKUP pool. When enabled, BYAN auto-proposes
1340
+ // handing delegable work to Codex on the ChatGPT subscription (no API credit)
1341
+ // to spare the Claude 5h budget. Arms the F5 hook by writing
1342
+ // _byan/_config/autodelegate.json. TTY -> prompt; non-TTY -> env opt-in.
1343
+ console.log();
1344
+ console.log(chalk.cyan('Codex backup pool (auto-delegation)'));
1345
+ let wantAutodelegate = false;
1346
+ if (process.stdin.isTTY) {
1347
+ try {
1348
+ const ans = await inquirer.prompt([
1349
+ {
1350
+ type: 'confirm',
1351
+ name: 'enable',
1352
+ message:
1353
+ 'Add Codex as a backup pool? BYAN will propose offloading delegable work to Codex (your ChatGPT subscription, no API credit) when Claude is under 5h pressure.',
1354
+ default: true,
1355
+ },
1356
+ ]);
1357
+ wantAutodelegate = ans.enable;
1358
+ } catch {
1359
+ wantAutodelegate = false;
1360
+ }
1361
+ } else {
1362
+ wantAutodelegate = process.env.BYAN_CODEX_AUTODELEGATE === '1';
1363
+ if (!wantAutodelegate) {
1364
+ console.log(
1365
+ chalk.gray(' · non-TTY — skipped (set BYAN_CODEX_AUTODELEGATE=1 to arm)')
1366
+ );
1367
+ }
1368
+ }
1369
+ if (wantAutodelegate) {
1370
+ try {
1371
+ await setupCodexAutodelegate(projectRoot);
1372
+ } catch (error) {
1373
+ console.log(chalk.yellow(` ! Codex auto-delegation setup skipped: ${error.message}`));
1374
+ console.log(chalk.gray(` ${DEVICE_FLOW_INSTRUCTION.replace(/\n/g, '\n ')}`));
1375
+ }
1376
+ }
1337
1377
  }
1338
1378
 
1339
1379
  if (needsClaude) {
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Codex auto-delegation setup (installer F4) — opts the user into a Codex backup
3
+ * pool and ARMS the F5 auto-delegation hook by writing
4
+ * `_byan/_config/autodelegate.json`.
5
+ *
6
+ * Linking uses the LOCAL Codex auth — the ChatGPT subscription established by
7
+ * `codex login` — NOT an API key: delegated turns cost subscription quota, not
8
+ * per-token API credit. The entitled model is `gpt-5.4` (the `-codex` ids are
9
+ * API-only on a subscription; see src/loadbalancer/providers/codex-provider.js).
10
+ *
11
+ * On a headless server the browser localhost redirect of the default login
12
+ * fails, so we surface the DEVICE-FLOW instruction (`codex login --device-auth`)
13
+ * rather than pretend the link worked.
14
+ *
15
+ * Companion to codex-native-setup.js (which wires the BYAN MCP into
16
+ * ~/.codex/config.toml). This module owns only the auto-delegation opt-in. All
17
+ * fs is injectable so the unit tests write nothing real; the hook it arms is a
18
+ * no-op until this config exists (disarmed-by-default, see the F5 hook).
19
+ */
20
+
21
+ const realFs = require('fs');
22
+ const realPath = require('path');
23
+ const os = require('os');
24
+ let chalk;
25
+ try { chalk = require('chalk'); } catch { chalk = null; }
26
+
27
+ const paint = (fn, s) => (chalk && chalk[fn] ? chalk[fn](s) : s);
28
+
29
+ const DEVICE_FLOW_INSTRUCTION = [
30
+ 'Codex is not linked yet. On THIS machine run:',
31
+ ' codex login --device-auth',
32
+ 'then open the printed URL, enter the code, and re-run the installer.',
33
+ '(device-auth avoids the localhost browser redirect that fails on a headless server).',
34
+ ].join('\n');
35
+
36
+ // Which local Codex auth backs the CLI: 'api-key' (CODEX_API_KEY, per-token),
37
+ // 'subscription' (~/.codex/auth.json, the 5h window), or null (not linked).
38
+ function codexAuthState({ home = os.homedir(), fs = realFs, path = realPath } = {}) {
39
+ if (process.env.CODEX_API_KEY) return 'api-key';
40
+ try {
41
+ if (fs.existsSync(path.join(home, '.codex', 'auth.json'))) return 'subscription';
42
+ } catch {
43
+ /* fall through */
44
+ }
45
+ return null;
46
+ }
47
+
48
+ // The config that ARMS the F5 hook. enabled:true is the whole point — the hook
49
+ // no-ops without this file.
50
+ function autodelegateConfig({ threshold = 80, budget = null, invocation = 'codex:codex-rescue --model gpt-5.4' } = {}) {
51
+ return {
52
+ enabled: true,
53
+ threshold,
54
+ budget,
55
+ invocation,
56
+ model: 'gpt-5.4',
57
+ note: 'Written by the BYAN installer (F4). Delete or set enabled:false to disarm auto-delegation.',
58
+ };
59
+ }
60
+
61
+ function writeAutodelegateConfig({ projectRoot, config, fs = realFs, path = realPath }) {
62
+ const dir = path.join(projectRoot, '_byan', '_config');
63
+ fs.mkdirSync(dir, { recursive: true });
64
+ const p = path.join(dir, 'autodelegate.json');
65
+ fs.writeFileSync(p, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
66
+ return p;
67
+ }
68
+
69
+ // Orchestrate the opt-in step. Called only when the user chose "add a Codex
70
+ // backup" at install. Arms the hook when Codex is linked; otherwise leaves it
71
+ // disarmed and surfaces the device-flow so the link can be completed and the
72
+ // installer re-run. Never throws on a link check — an unlinked Codex is a normal
73
+ // outcome, not an installer failure.
74
+ async function setupCodexAutodelegate(projectRoot, options = {}) {
75
+ const log = options.quiet ? () => {} : (...a) => console.log(...a);
76
+ const fs = options.fs || realFs;
77
+ const path = options.path || realPath;
78
+
79
+ const auth = codexAuthState({ fs, path, home: options.home });
80
+ if (!auth) {
81
+ log(paint('yellow', ' ! Codex not linked - auto-delegation left DISARMED'));
82
+ log(paint('gray', ` ${DEVICE_FLOW_INSTRUCTION.replace(/\n/g, '\n ')}`));
83
+ return { armed: false, reason: 'codex-not-linked' };
84
+ }
85
+
86
+ const config = autodelegateConfig(options);
87
+ config.linkedVia = auth;
88
+ const p = writeAutodelegateConfig({ projectRoot, config, fs, path });
89
+ log(paint('green', ` + Codex auto-delegation ARMED (${auth}) -> ${p}`));
90
+ log(paint('gray', ' BYAN will now propose handing delegable work to Codex (no API credit).'));
91
+ return { armed: true, path: p, authPool: auth, config };
92
+ }
93
+
94
+ module.exports = {
95
+ DEVICE_FLOW_INSTRUCTION,
96
+ codexAuthState,
97
+ autodelegateConfig,
98
+ writeAutodelegateConfig,
99
+ setupCodexAutodelegate,
100
+ };
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * UserPromptSubmit hook — Codex auto-delegation nudge (F5).
4
+ *
5
+ * Every turn, estimates Claude's rolling-5h consumption (F1 usage-estimator) and
6
+ * asks the pure decision core (F5 autodelegate-decision) whether to nudge BYAN to
7
+ * hand delegable work to Codex — on the ChatGPT subscription, no API credit. The
8
+ * nudge is ADVISORY context only: it proposes, never forces, and always names the
9
+ * red line (delegable work only; judgment/soul/verify stay on Claude).
10
+ *
11
+ * DISARMED BY DEFAULT. The hook no-ops unless `_byan/_config/autodelegate.json`
12
+ * exists with `enabled: true` (written by the yanstaller F4 when the user opts
13
+ * into a Codex backup). No config file -> silence. This keeps it from nagging on
14
+ * machines where Codex is not set up, and honors "only once the user chose it at
15
+ * install".
16
+ *
17
+ * Escape hatch: a `.byan-codex-autodelegate/off` file in the project root forces
18
+ * silence even when enabled. Always exits 0; never blocks prompt submission.
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { readStdin, parseJson } = require('./lib/strict-runtime');
24
+ const { decideAutodelegation, renderNudge } = require('./lib/autodelegate-decision');
25
+ const { estimateClaudeUsage } = require('./lib/usage-estimator');
26
+
27
+ // Load the opt-in config; absent/invalid -> disarmed ({ enabled: false }).
28
+ function loadConfig(projectRoot) {
29
+ try {
30
+ const p = path.join(projectRoot, '_byan', '_config', 'autodelegate.json');
31
+ if (!fs.existsSync(p)) return { enabled: false };
32
+ const cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
33
+ return cfg && typeof cfg === 'object' ? cfg : { enabled: false };
34
+ } catch {
35
+ return { enabled: false };
36
+ }
37
+ }
38
+
39
+ function toggledOff(projectRoot) {
40
+ try {
41
+ return fs.existsSync(path.join(projectRoot, '.byan-codex-autodelegate', 'off'));
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ if (require.main === module) {
48
+ (async () => {
49
+ let additionalContext = '';
50
+ try {
51
+ const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
52
+ const config = loadConfig(projectRoot);
53
+ if (config.enabled === true && !toggledOff(projectRoot)) {
54
+ const payload = parseJson(await readStdin());
55
+ const prompt = payload.prompt || payload.user_prompt || payload.userPrompt || '';
56
+ // Estimate usage only when a budget is configured; else pct stays null and
57
+ // the decision falls back to nature-based delegation (still useful).
58
+ const usage = estimateClaudeUsage({ budget: config.budget || null });
59
+ const decision = decideAutodelegation({ requestText: prompt, usage, config });
60
+ additionalContext = renderNudge(decision);
61
+ }
62
+ } catch {
63
+ additionalContext = ''; // never block prompt submission
64
+ }
65
+ process.stdout.write(
66
+ JSON.stringify({
67
+ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext },
68
+ })
69
+ );
70
+ process.exit(0);
71
+ })();
72
+ }
73
+
74
+ module.exports = { loadConfig, toggledOff };
@@ -0,0 +1,152 @@
1
+ /**
2
+ * autodelegate-decision — the pure core that decides whether BYAN should hand a
3
+ * task to Codex (on the ChatGPT subscription, no API credit), and how hard to
4
+ * push. Two triggers, honest about their strength:
5
+ *
6
+ * 1. NATURE (always available, no gauge needed): the request looks like
7
+ * delegable coding work (implement / write / fix / refactor / test) -> mode
8
+ * 'delegable-only', propose handing THIS task to Codex.
9
+ * 2. PRESSURE (only when a usage gauge is supplied): the estimated Claude 5h
10
+ * consumption is at/above the threshold (default 80%) -> mode 'all', propose
11
+ * offloading everything delegable to spare the remaining budget.
12
+ * 3. PERF (opt-in, off by default): a config-driven forces table says Codex is
13
+ * reputed stronger for this kind of task -> mode 'perf-routed'. Honest: this
14
+ * is a heuristic below BYAN's L2 perf floor, so it ships neutral (empty
15
+ * table) and asserts nothing until the user populates it. See perf-routing.
16
+ *
17
+ * The red line is never crossed: only DELEGABLE natures are ever proposed —
18
+ * judgment / analysis / soul / verification stay on Claude. This module states
19
+ * that in `redLine` so the hook's nudge always carries it. Pure (no I/O): the
20
+ * hook is a thin shell that feeds it the request text + the F1 usage estimate.
21
+ */
22
+
23
+ const { perfFavors } = require('./perf-routing');
24
+
25
+ const DEFAULT_THRESHOLD = 80;
26
+ const DEFAULT_INVOCATION = 'codex:codex-rescue --model gpt-5.4';
27
+ const RED_LINE = 'delegable work only (code / mechanical) — judgment, analysis, soul and verification stay on Claude';
28
+
29
+ // Verbs/nouns that mark a delegable coding task. Heuristic by construction (free
30
+ // text has no nature field); kept deliberately conservative so conversational or
31
+ // judgment requests do not trip it.
32
+ const DELEGABLE_RE = new RegExp(
33
+ [
34
+ 'code', 'coder', 'impl[ée]mente', 'implement', '[ée]cris', 'write', 'wire',
35
+ 'fix', 'corrige', 'debug', 'd[ée]bogue', 'refactor', 'refactorise',
36
+ 'test', 'tests', 'script', 'module', 'fonction', 'function', 'patch',
37
+ 'ajoute\\s+(le|la|un|une|du)', 'build\\s+(the|a|le|la)', 'port',
38
+ ].join('|'),
39
+ 'i'
40
+ );
41
+
42
+ function looksDelegable(text) {
43
+ return DELEGABLE_RE.test(String(text || ''));
44
+ }
45
+
46
+ // Decide the auto-delegation posture for one turn.
47
+ // Returns { delegate, mode, pct, reason, redLine, invocation } — a stable shape.
48
+ // mode: 'off' | 'all' | 'delegable-only' | 'none'.
49
+ function decideAutodelegation({ requestText = '', usage = null, config = {} } = {}) {
50
+ const {
51
+ enabled = true,
52
+ threshold = DEFAULT_THRESHOLD,
53
+ invocation = DEFAULT_INVOCATION,
54
+ } = config;
55
+
56
+ if (!enabled) {
57
+ return { delegate: false, mode: 'off', pct: null, reason: 'auto-delegation disabled (toggle off)', redLine: RED_LINE, invocation };
58
+ }
59
+
60
+ const pct = usage && typeof usage.pct === 'number' ? usage.pct : null;
61
+ const delegable = looksDelegable(requestText);
62
+
63
+ if (pct != null && pct >= threshold) {
64
+ return {
65
+ delegate: true,
66
+ mode: 'all',
67
+ pct,
68
+ reason: `estimated Claude 5h usage ${pct}% >= ${threshold}% — propose offloading everything delegable to Codex`,
69
+ redLine: RED_LINE,
70
+ invocation,
71
+ };
72
+ }
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
+ }
84
+
85
+ // PERF (opt-in): the forces table (user-populated) may favor Codex for this
86
+ // kind of task even at low pressure. Off by default; always heuristic.
87
+ if (config.perfRouting) {
88
+ const pf = perfFavors(requestText, config.perfForces || []);
89
+ if (pf.favors === 'codex') {
90
+ return {
91
+ delegate: true,
92
+ mode: 'perf-routed',
93
+ pct,
94
+ reason: `perf heuristic favors Codex for '${pf.category}' (heuristic, NOT a measured benchmark)`,
95
+ redLine: RED_LINE,
96
+ invocation,
97
+ };
98
+ }
99
+ }
100
+
101
+ return {
102
+ delegate: false,
103
+ mode: 'none',
104
+ pct,
105
+ reason: pct != null
106
+ ? `usage ${pct}% below ${threshold}% and task not clearly delegable`
107
+ : 'task not clearly delegable and no usage gauge available',
108
+ redLine: RED_LINE,
109
+ invocation,
110
+ };
111
+ }
112
+
113
+ // Defense-in-depth: the invocation string is interpolated into BYAN's injected
114
+ // context. It is config/installer-controlled today (safe), but sanitising before
115
+ // interpolation closes the door on any future path feeding untrusted text in —
116
+ // strip newlines/control chars (no context-structure injection) and cap length.
117
+ function sanitizeForContext(value, max = 120) {
118
+ return String(value == null ? "" : value)
119
+ .replace(/[\u0000-\u001F\u007F]+/g, " ") // control chars + newlines -> space
120
+ .replace(/\s+/g, " ") // collapse whitespace runs
121
+ .trim()
122
+ .slice(0, max);
123
+ }
124
+
125
+ // Render a decision into the one-paragraph nudge injected into BYAN's context.
126
+ // Empty string when there is nothing to propose (so the hook injects nothing).
127
+ // The nudge is ADVISORY: it proposes, it never forces — BYAN still owns the call
128
+ // and the red line is spelled out every time.
129
+ function renderNudge(decision) {
130
+ if (!decision || !decision.delegate) return '';
131
+ const gauge = decision.pct != null ? ` (estimated Claude 5h usage ~${decision.pct}%)` : '';
132
+ const invocation = sanitizeForContext(decision.invocation) || DEFAULT_INVOCATION;
133
+ const scope = decision.mode === 'all'
134
+ ? 'Consider offloading ALL delegable work this session to Codex'
135
+ : decision.mode === 'perf-routed'
136
+ ? 'Codex is heuristically favored for this kind of task (not a measured benchmark) — consider handing it over'
137
+ : 'This looks like delegable coding work — consider handing it to Codex';
138
+ return [
139
+ `[BYAN auto-delegate]${gauge}: ${scope} via \`${invocation}\` `
140
+ + '(runs on the ChatGPT subscription, no API credit).',
141
+ `Red line: ${decision.redLine}. This is advisory — you decide, and you still verify Codex's output before commit.`,
142
+ ].join(' ');
143
+ }
144
+
145
+ module.exports = {
146
+ DEFAULT_THRESHOLD,
147
+ DEFAULT_INVOCATION,
148
+ RED_LINE,
149
+ looksDelegable,
150
+ decideAutodelegation,
151
+ renderNudge,
152
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * perf-routing — the MECHANISM for routing a task to the pool reputed stronger
3
+ * for it, WITHOUT asserting an unsourced ranking.
4
+ *
5
+ * The honest ceiling (BYAN fact-check, performance domain): "model X is better
6
+ * at task Y" needs L2 evidence (a reproducible benchmark). A community arena such
7
+ * as designarena.ai is preference-vote data — below that floor. So this module
8
+ * ships a NEUTRAL default (DEFAULT_FORCES = []): out of the box it claims nothing
9
+ * and changes no routing. It provides the config-driven forces table + a matcher;
10
+ * populating the table with rankings (from the user's own benchmarks, or an arena
11
+ * taken as a weak signal) is an explicit opt-in, and every result it returns is
12
+ * tagged `confidence: 'heuristic'` so downstream never presents it as measured.
13
+ *
14
+ * A forces entry: { category, pattern (regex source, case-insensitive), favors:
15
+ * 'codex' | 'claude' }. Pure — no I/O.
16
+ */
17
+
18
+ // Neutral by default: no unsourced perf claim ships. Populate via config.
19
+ const DEFAULT_FORCES = [];
20
+
21
+ // Return the first forces entry whose pattern matches the text, or null.
22
+ // Malformed entries (missing pattern/favors, or an invalid regex) are skipped.
23
+ function matchCategory(text, forces = DEFAULT_FORCES) {
24
+ const s = String(text || '');
25
+ if (!Array.isArray(forces)) return null;
26
+ for (const entry of forces) {
27
+ if (!entry || !entry.pattern || !entry.favors) continue;
28
+ let re;
29
+ try {
30
+ re = new RegExp(entry.pattern, 'i');
31
+ } catch {
32
+ continue; // invalid regex in a user-supplied table — skip, never throw
33
+ }
34
+ if (re.test(s)) return entry;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ // Resolve the perf preference for a text: which pool the (user-supplied) table
40
+ // favors, always tagged heuristic. { favors: 'codex'|'claude'|null, category, confidence }.
41
+ function perfFavors(text, forces = DEFAULT_FORCES) {
42
+ const hit = matchCategory(text, forces);
43
+ if (!hit) return { favors: null, category: null, confidence: 'heuristic' };
44
+ return { favors: hit.favors, category: hit.category, confidence: 'heuristic' };
45
+ }
46
+
47
+ module.exports = { DEFAULT_FORCES, matchCategory, perfFavors };
@@ -3,6 +3,20 @@
3
3
  "version": 1,
4
4
  "min_passes": 3,
5
5
  "last_verdict_must_be": "ok",
6
+ "self_verify_checklist": [
7
+ {
8
+ "theme": "tests/coverage",
9
+ "check": "Every changed branch has a test, the pre-existing suite still passes with no regression, and any new behaviour has a test that would fail without the change?"
10
+ },
11
+ {
12
+ "theme": "doc-follows-code",
13
+ "check": "Did a public-surface or contract change leave a doc behind that must move with it — CHANGELOG, README, a rule @-reference, a manifest, a template mirror?"
14
+ },
15
+ {
16
+ "theme": "scope-discovery",
17
+ "check": "Anything discovered mid-build outside the locked scope (a legacy tree, an extra decision, an adjacent bug) surfaced to the user rather than silently absorbed or cut?"
18
+ }
19
+ ],
6
20
  "min_score": 95,
7
21
  "auto_keywords": [
8
22
  "prod",