create-byan-agent 2.39.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.
- package/CHANGELOG.md +102 -1
- package/install/bin/create-byan-agent-v2.js +40 -0
- package/install/lib/codex-autodelegate-setup.js +100 -0
- package/install/templates/.claude/hooks/codex-autodelegate.js +74 -0
- package/install/templates/.claude/hooks/lib/autodelegate-decision.js +152 -0
- package/install/templates/.claude/hooks/lib/perf-routing.js +47 -0
- package/install/templates/.claude/hooks/lib/usage-estimator.js +262 -0
- package/install/templates/.claude/hooks/tier-script-guard.js +185 -0
- package/install/templates/.claude/rules/native-workflows.md +33 -7
- package/install/templates/.claude/settings.json +13 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +7 -2
- package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +3 -1
- package/install/templates/.claude/workflows/sprint-planning.js +2 -2
- package/install/templates/.claude/workflows/testarch-trace.js +3 -2
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-tier-script.js +52 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch.js +22 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/native-tiers.js +26 -9
- package/install/templates/_byan/mcp/byan-mcp-server/lib/tier-script.js +104 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-lint.js +92 -6
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +22 -7
- package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +2 -2
- package/install/templates/dist/skill-bundles/byan-byan.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
- package/install/templates/docs/native-workflows-contract.md +55 -12
- package/package.json +1 -1
- package/src/loadbalancer/capability-matrix.js +14 -0
- package/src/loadbalancer/degradation-ladder.js +121 -0
- package/src/loadbalancer/loadbalancer.default.yaml +23 -1
- package/src/loadbalancer/mcp-server.js +200 -18
- package/src/loadbalancer/providers/codex-provider.js +260 -0
- package/src/loadbalancer/providers/factory.js +36 -0
- package/src/loadbalancer/subscription-window.js +142 -0
- package/src/loadbalancer/switch-tolerance.js +63 -0
- package/src/loadbalancer/tools/index.js +17 -2
package/CHANGELOG.md
CHANGED
|
@@ -9,7 +9,108 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
-
|
|
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.
|
|
13
114
|
|
|
14
115
|
### Added - shippable soul stays in sync with the active soul (byan-sync-soul)
|
|
15
116
|
|
|
@@ -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 };
|