create-byan-agent 2.39.0 → 2.42.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 +153 -1
- package/install/bin/byan-handoff.js +139 -0
- package/install/bin/create-byan-agent-v2.js +70 -9
- package/install/lib/codex-autodelegate-setup.js +100 -0
- package/install/lib/codex-native-setup.js +94 -2
- package/install/lib/project-handoff.js +300 -0
- package/install/lib/rtk-integration.js +81 -14
- package/install/templates/.claude/CLAUDE.md +16 -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 +35 -22
- package/install/templates/.claude/skills/byan-byan/SKILL.md +21 -2
- package/install/templates/.claude/skills/byan-codex/SKILL.md +7 -0
- 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/.codex/skills/byan/SKILL.md +77 -0
- package/install/templates/_byan/INDEX.md +6 -2
- package/install/templates/_byan/_config/workflow-manifest.csv +1 -1
- 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 +3 -3
- package/install/templates/_byan/workflow/simple/byan/project-handoff-workflow.md +90 -0
- package/install/templates/dist/skill-bundles/byan-byan.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-codex.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
- package/install/templates/docs/codex-auto-delegation.md +140 -0
- package/install/templates/docs/native-workflows-contract.md +55 -12
- package/package.json +2 -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,159 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
-
## [2.
|
|
12
|
+
## [2.42.0] - 2026-07-06
|
|
13
|
+
|
|
14
|
+
### Fixed - Shipped Claude Code hooks no longer spam MODULE_NOT_FOUND
|
|
15
|
+
- **Every hook command in the shipped `.claude/settings.json` is now
|
|
16
|
+
self-guarded.** The hooks run on every tool (empty matcher); a bare
|
|
17
|
+
`node "$CLAUDE_PROJECT_DIR"/.claude/hooks/X.js` threw `MODULE_NOT_FOUND` on
|
|
18
|
+
every tool call whenever the script was not resolvable — a non-BYAN project, an
|
|
19
|
+
empty `$CLAUDE_PROJECT_DIR`, a partial install, or version drift. Each command
|
|
20
|
+
is now `p="$CLAUDE_PROJECT_DIR/.claude/hooks/X.js"; [ -f "$p" ] || exit 0; exec
|
|
21
|
+
node "$p"`: a missing script no-ops with exit 0 (the tool proceeds), a present
|
|
22
|
+
script runs via `exec` so its exit code is preserved (a blocking PreToolUse /
|
|
23
|
+
Stop guard still blocks). Covered by
|
|
24
|
+
`install/__tests__/hook-invocation-guard.test.js` (guarded-shape on all 24
|
|
25
|
+
commands + runtime sh simulation: absent/empty -> 0, blocker -> 2, stdin
|
|
26
|
+
passthrough).
|
|
27
|
+
|
|
28
|
+
### Changed - Handoff auto-import instructions
|
|
29
|
+
- **Claude and Codex activation surfaces now know how to handle
|
|
30
|
+
`importe depuis claude` / `importe depuis codex` automatically.** `CLAUDE.md`
|
|
31
|
+
and BYAN skills instruct the assistant to run
|
|
32
|
+
`byan-handoff latest --from <source> --prompt` and resume from the generated
|
|
33
|
+
context without relying on native assistant memory.
|
|
34
|
+
|
|
35
|
+
### Added - Portable Claude/Codex Markdown handoff
|
|
36
|
+
- **BYAN can now export/import project state as a portable Markdown handoff for
|
|
37
|
+
switching between Claude Code and Codex.** The new `byan-handoff` CLI writes
|
|
38
|
+
handoffs under `_byan-output/handoffs/`, embeds a parseable
|
|
39
|
+
`json byan-handoff` block, and can print a compact resume prompt via
|
|
40
|
+
`byan-handoff latest --from <source> --prompt` or
|
|
41
|
+
`byan-handoff import <file> --prompt`.
|
|
42
|
+
A new `project-handoff` BYAN workflow documents the limit-switch protocol.
|
|
43
|
+
Covered by `install/__tests__/project-handoff.test.js`.
|
|
44
|
+
|
|
45
|
+
### Changed - RTK offered on Codex-selected installs
|
|
46
|
+
- **Yanstaller now offers RTK when the selected target includes Codex, not only
|
|
47
|
+
Claude Code.** Claude Code keeps the transparent `rtk init -g --auto-patch`
|
|
48
|
+
hook path. Codex installs get the verified native `rtk` binary and an explicit
|
|
49
|
+
no-transparent-hook status, matching BYAN's current Codex adapter model.
|
|
50
|
+
Covered by `install/__tests__/rtk-integration.test.js`.
|
|
51
|
+
|
|
52
|
+
### Added - Codex native skills in yanstaller
|
|
53
|
+
- **Codex installs now get real native skills, not just project prompt stubs.**
|
|
54
|
+
When the yanstaller target includes Codex (Codex-only or Claude+Codex), it
|
|
55
|
+
writes the BYAN MCP entry to `~/.codex/config.toml` and copies BYAN skill
|
|
56
|
+
folders into `~/.codex/skills`, creating `~/.codex` when Codex was explicitly
|
|
57
|
+
selected. The project template also ships `.codex/skills/byan/SKILL.md`, so a
|
|
58
|
+
fresh Codex install has a native skill named `byan` in addition to the
|
|
59
|
+
Claude-derived BYAN specialty skills. This closes the gap where a fresh
|
|
60
|
+
machine had `.codex/prompts/` and MCP wiring but no native `$byan` / BYAN skill
|
|
61
|
+
surface for delegation. Covered by `install/__tests__/codex-native-setup.test.js`.
|
|
62
|
+
|
|
63
|
+
### Added - Codex auto-delegation (opt-in, native)
|
|
64
|
+
- **BYAN now proposes handing delegable work to Codex on your ChatGPT
|
|
65
|
+
subscription (no API credit) when Claude nears its 5h limit.** A
|
|
66
|
+
`UserPromptSubmit` hook estimates the rolling-5h Claude consumption from the
|
|
67
|
+
local transcripts (live) + session-meta (fallback), and nudges delegation on
|
|
68
|
+
three triggers: pressure (>= 80% estimated, configurable), task nature
|
|
69
|
+
(delegable coding work), and an opt-in perf forces table. The red line holds:
|
|
70
|
+
only delegable natures are proposed; judgment / soul / verification stay on
|
|
71
|
+
Claude. Disarmed by default — the yanstaller arms it on opt-in (device-flow
|
|
72
|
+
`codex login --device-auth`, entitled model gpt-5.4) by writing
|
|
73
|
+
`_byan/_config/autodelegate.json`. The 5h gauge is an honest ESTIMATE (no
|
|
74
|
+
provider exposes a machine-readable quota; `pct` is null without a configured
|
|
75
|
+
budget), and perf routing ships neutral (below the L2 perf floor, tagged
|
|
76
|
+
heuristic). New: `.claude/hooks/codex-autodelegate.js` +
|
|
77
|
+
`.claude/hooks/lib/{usage-estimator,autodelegate-decision,perf-routing}.js`,
|
|
78
|
+
`install/lib/codex-autodelegate-setup.js`. 47 unit tests. See
|
|
79
|
+
`docs/codex-auto-delegation.md`.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
- **lb: Codex pool targeted a non-entitled model on ChatGPT subscription.** The
|
|
83
|
+
provider defaulted to `gpt-5-codex`; the OpenAI backend rejects every
|
|
84
|
+
`-codex`-suffixed model on a subscription account (API-key only). Default is
|
|
85
|
+
now the entitled `gpt-5.4`, and a new pure `resolveCodexModel({ requested,
|
|
86
|
+
authPool })` remaps a `-codex` request to `gpt-5.4` under subscription auth
|
|
87
|
+
only (passthrough on api-key or an already-plain id). `loadbalancer.default.yaml`
|
|
88
|
+
codex models set to `gpt-5.4`. Covered by 7 new unit tests (mock runner, no
|
|
89
|
+
real CLI/network). See `docs/loadbalancer-multipool.md`.
|
|
90
|
+
|
|
91
|
+
## [2.41.0] - 2026-07-03
|
|
92
|
+
|
|
93
|
+
### Added - multi-pool subscription arbitrage: Codex as a second pool + the 5h-window ladder
|
|
94
|
+
|
|
95
|
+
The load-balancer had claude + copilot + byan_api but no OpenAI pool, and its
|
|
96
|
+
pressure-score modelled API-burst (429) pressure, not the rolling 5h subscription
|
|
97
|
+
window that users actually hit. This release adds the second pool and the window
|
|
98
|
+
tracking that switches before the wall, without denaturing BYAN.
|
|
99
|
+
|
|
100
|
+
- **CodexProvider** (`src/loadbalancer/providers/codex-provider.js`) - wraps the
|
|
101
|
+
`codex exec --json` SYSTEM CLI (not an npm SDK); degrades to disabled if the
|
|
102
|
+
binary is absent; two auth pools (CODEX_API_KEY per-token, else the
|
|
103
|
+
ChatGPT-subscription session). Rate-limit exhaustion read from stderr (no
|
|
104
|
+
machine-readable quota, OpenAI issue #10233). Registered in the yaml +
|
|
105
|
+
capability-matrix; provider factory (`providers/factory.js`).
|
|
106
|
+
- **subscription-window tracker** (`subscription-window.js`) - per-pool rolling-5h
|
|
107
|
+
+ weekly token burn, a `window-proximity` signal (distinct from 429-pressure) +
|
|
108
|
+
ETA. Honest estimate: `null` proximity without a configured budget, no
|
|
109
|
+
fabricated percentage.
|
|
110
|
+
- **Execution stubs unblocked** (`mcp-server.js`) - `lb_send` / `lb_switch` /
|
|
111
|
+
`lb_get_context` now really route / transfer context / read state; the
|
|
112
|
+
SessionBridge and GracefulDegradation (previously orphaned) are wired; all
|
|
113
|
+
seams injectable so tests avoid spawning codex or hitting the OpenAI quota.
|
|
114
|
+
- **switch-tolerance** (`switch-tolerance.js`) - the red line: only delegable
|
|
115
|
+
natures (exploration / mechanical / implementation) may cross to Codex;
|
|
116
|
+
verification / analysis / soul / identity / review / gate stay on Claude and
|
|
117
|
+
queue rather than denature.
|
|
118
|
+
- **4-rung degradation ladder** (`degradation-ladder.js`) - HEALTHY ->
|
|
119
|
+
PRIMARY_HOT -> PRIMARY_EXHAUSTED -> ALL_EXHAUSTED, driven by the window
|
|
120
|
+
proximity, obeying the red line at every rung. `planRoute(nature)` on the live
|
|
121
|
+
shell.
|
|
122
|
+
- **lb_budget** MCP tool + `getBudget()` - the anti-"5h limit reached" dashboard:
|
|
123
|
+
per-pool 5h/weekly burn, proximity, ETA, rung, with the honest estimate/
|
|
124
|
+
doubles-the-ceiling note.
|
|
125
|
+
- **Docs** - `docs/loadbalancer-multipool.md` (architecture, the red line, the two
|
|
126
|
+
hard truths, honest shipping status).
|
|
127
|
+
|
|
128
|
+
### Added - native workflow model tiering: the sonnet tier lives, ad-hoc scripts are gated
|
|
129
|
+
|
|
130
|
+
Claude Code's Workflow tool runs every `agent()` leaf on the session model
|
|
131
|
+
unless the script pins `opts.model`, and the tiering contract only reached
|
|
132
|
+
committed `.claude/workflows/*.js` through the repo linter. Two consequences,
|
|
133
|
+
both observed live: ad-hoc scripts (written inline for one run) executed
|
|
134
|
+
all-deep (14 agents on Opus for one review), and the `balanced` tier was
|
|
135
|
+
unreachable (0/131 committed leaves on sonnet). This release closes both.
|
|
136
|
+
|
|
137
|
+
- **`mech-` opt-in class (native-tiers)** - a `mech-` label prefix
|
|
138
|
+
(`mech-validate-json`) declares a MECHANICAL verification: a binary,
|
|
139
|
+
judgment-free check (JSON parses, schema matches, lint passes) that tiers to
|
|
140
|
+
`balanced` (sonnet). Explicit opt-in only, no keyword fuzziness:
|
|
141
|
+
`validate-json` without the prefix stays protected (deep). The linter holds
|
|
142
|
+
the script to the declaration: `mechanical-without-model` and
|
|
143
|
+
`mechanical-below-tier` are hard contract violations.
|
|
144
|
+
- **tier-script engine + CLI** - `lib/tier-script.js` analyzes any workflow
|
|
145
|
+
script TEXT (committed, ad-hoc or draft): one verdict per labelled leaf
|
|
146
|
+
against native-tiers, plus the deny-once gate decision. Parsing stays in
|
|
147
|
+
`workflows-lint.js` (`extractLabelledLeaves`). `bin/byan-tier-script.js`
|
|
148
|
+
prints the report (exit 0 clean/acknowledged, 1 gaps, 2 violations).
|
|
149
|
+
- **tier gate hook** - `.claude/hooks/tier-script-guard.js` (PreToolUse,
|
|
150
|
+
matcher `Workflow`) gates EVERY Workflow invocation at the one chokepoint an
|
|
151
|
+
ad-hoc script crosses. Undecided exploration/`mech-` leaves deny ONCE with
|
|
152
|
+
the exact leaf list; `// BYAN-TIER: reviewed` acknowledges deliberate deep
|
|
153
|
+
choices; an identical resubmission passes (deny-once by design); registry
|
|
154
|
+
invocations pass. It rewrites nothing (STRICT-2 No Downgrade). Every
|
|
155
|
+
decision lands in `_byan-output/tier-ledger.jsonl` with a per-model
|
|
156
|
+
histogram - the measurement basis for token gains. Escape hatch:
|
|
157
|
+
`.byan-tier/off`.
|
|
158
|
+
- **`byan_dispatch` batch mode** - `{ leaves: [{ label, nature? }] }` returns
|
|
159
|
+
the `opts.model` per planned leaf BEFORE the script is written; the nature
|
|
160
|
+
enum gains `mechanical` on both axes.
|
|
161
|
+
- **Docs** - `native-workflows.md` rule + `docs/native-workflows-contract.md`
|
|
162
|
+
describe the three live tiers, the two nets (repo linter floor + tier gate
|
|
163
|
+
hook) and the authoring flow; byan-byan and hermes-dispatch skills carry the
|
|
164
|
+
same doctrine.
|
|
13
165
|
|
|
14
166
|
### Added - shippable soul stays in sync with the active soul (byan-sync-soul)
|
|
15
167
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const {
|
|
7
|
+
buildHandoff,
|
|
8
|
+
latestHandoff,
|
|
9
|
+
parseMarkdown,
|
|
10
|
+
renderMarkdown,
|
|
11
|
+
renderResumePrompt,
|
|
12
|
+
writeHandoff,
|
|
13
|
+
} = require('../lib/project-handoff');
|
|
14
|
+
|
|
15
|
+
function usage() {
|
|
16
|
+
return [
|
|
17
|
+
'byan-handoff export [--from claude|codex] [--to codex|claude] [--task <text>] [--summary <text>] [--next <text>] [--out <file>] [--root <dir>] [--stdout]',
|
|
18
|
+
'byan-handoff import <file> [--prompt] [--json] [--root <dir>]',
|
|
19
|
+
'byan-handoff latest [--from claude|codex] [--prompt] [--json] [--root <dir>]',
|
|
20
|
+
].join('\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
const args = {
|
|
25
|
+
cmd: argv[2] || 'export',
|
|
26
|
+
root: process.cwd(),
|
|
27
|
+
from: null,
|
|
28
|
+
to: null,
|
|
29
|
+
task: null,
|
|
30
|
+
summary: null,
|
|
31
|
+
out: null,
|
|
32
|
+
stdout: false,
|
|
33
|
+
prompt: false,
|
|
34
|
+
json: false,
|
|
35
|
+
next: [],
|
|
36
|
+
decisions: [],
|
|
37
|
+
blockers: [],
|
|
38
|
+
commands: [],
|
|
39
|
+
notes: [],
|
|
40
|
+
file: null,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (let i = 3; i < argv.length; i++) {
|
|
44
|
+
const a = argv[i];
|
|
45
|
+
if (a === '--root') args.root = argv[++i];
|
|
46
|
+
else if (a === '--from') args.from = argv[++i];
|
|
47
|
+
else if (a === '--to') args.to = argv[++i];
|
|
48
|
+
else if (a === '--task') args.task = argv[++i];
|
|
49
|
+
else if (a === '--summary') args.summary = argv[++i];
|
|
50
|
+
else if (a === '--out') args.out = argv[++i];
|
|
51
|
+
else if (a === '--stdout') args.stdout = true;
|
|
52
|
+
else if (a === '--prompt') args.prompt = true;
|
|
53
|
+
else if (a === '--json') args.json = true;
|
|
54
|
+
else if (a === '--next') args.next.push(argv[++i]);
|
|
55
|
+
else if (a === '--decision') args.decisions.push(argv[++i]);
|
|
56
|
+
else if (a === '--blocker') args.blockers.push(argv[++i]);
|
|
57
|
+
else if (a === '--command') args.commands.push(argv[++i]);
|
|
58
|
+
else if (a === '--note') args.notes.push(argv[++i]);
|
|
59
|
+
else if (a === '-h' || a === '--help') args.help = true;
|
|
60
|
+
else if (!args.file) args.file = a;
|
|
61
|
+
else throw new Error(`Unexpected argument: ${a}`);
|
|
62
|
+
}
|
|
63
|
+
return args;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readHandoffFile(file) {
|
|
67
|
+
return parseMarkdown(fs.readFileSync(file, 'utf8'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printImported(handoff, args) {
|
|
71
|
+
if (args.json) {
|
|
72
|
+
process.stdout.write(JSON.stringify(handoff, null, 2) + '\n');
|
|
73
|
+
} else if (args.prompt) {
|
|
74
|
+
process.stdout.write(renderResumePrompt(handoff) + '\n');
|
|
75
|
+
} else {
|
|
76
|
+
process.stdout.write(renderMarkdown(handoff) + '\n');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function main() {
|
|
81
|
+
try {
|
|
82
|
+
const args = parseArgs(process.argv);
|
|
83
|
+
if (args.help) {
|
|
84
|
+
console.log(usage());
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const root = path.resolve(args.root);
|
|
89
|
+
if (args.cmd === 'export') {
|
|
90
|
+
const handoff = buildHandoff({
|
|
91
|
+
root,
|
|
92
|
+
from: args.from,
|
|
93
|
+
to: args.to,
|
|
94
|
+
task: args.task,
|
|
95
|
+
summary: args.summary,
|
|
96
|
+
nextActions: args.next,
|
|
97
|
+
decisions: args.decisions,
|
|
98
|
+
blockers: args.blockers,
|
|
99
|
+
commands: args.commands,
|
|
100
|
+
notes: args.notes,
|
|
101
|
+
});
|
|
102
|
+
if (args.stdout) {
|
|
103
|
+
process.stdout.write(renderMarkdown(handoff) + '\n');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const result = writeHandoff(root, handoff, { outPath: args.out });
|
|
107
|
+
process.stdout.write(`${result.path}\n`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (args.cmd === 'import') {
|
|
112
|
+
if (!args.file) throw new Error('Missing handoff file');
|
|
113
|
+
printImported(readHandoffFile(path.resolve(root, args.file)), args);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (args.cmd === 'latest') {
|
|
118
|
+
const file = latestHandoff(root, { from: args.from });
|
|
119
|
+
if (!file) {
|
|
120
|
+
const suffix = args.from ? ` from ${args.from}` : '';
|
|
121
|
+
throw new Error(`No handoff${suffix} found under _byan-output/handoffs`);
|
|
122
|
+
}
|
|
123
|
+
if (!args.prompt && !args.json) {
|
|
124
|
+
process.stdout.write(`${file}\n`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
printImported(readHandoffFile(file), args);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw new Error(`Unknown command: ${args.cmd}`);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
process.stderr.write(`byan-handoff: ${err.message}\n`);
|
|
134
|
+
process.stderr.write(usage() + '\n');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
main();
|
|
@@ -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');
|
|
@@ -1323,17 +1324,56 @@ async function install(options = {}) {
|
|
|
1323
1324
|
|
|
1324
1325
|
if (needsCodex) {
|
|
1325
1326
|
console.log();
|
|
1326
|
-
console.log(chalk.cyan('Codex CLI
|
|
1327
|
+
console.log(chalk.cyan('Codex CLI native setup (~/.codex/config.toml + ~/.codex/skills)'));
|
|
1327
1328
|
try {
|
|
1328
|
-
await setupCodexNative(projectRoot);
|
|
1329
|
+
await setupCodexNative(projectRoot, { templateDir, force: true });
|
|
1329
1330
|
} catch (error) {
|
|
1330
|
-
console.log(chalk.red(` ✘ Codex
|
|
1331
|
+
console.log(chalk.red(` ✘ Codex native setup failed: ${error.message}`));
|
|
1331
1332
|
console.log(
|
|
1332
1333
|
chalk.yellow(
|
|
1333
|
-
` → Edit ~/.codex/config.toml manually to add the byan MCP entry.`
|
|
1334
|
+
` → Edit ~/.codex/config.toml manually to add the byan MCP entry, then copy BYAN skills into ~/.codex/skills.`
|
|
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) {
|
|
@@ -1386,26 +1426,47 @@ async function install(options = {}) {
|
|
|
1386
1426
|
}
|
|
1387
1427
|
}
|
|
1388
1428
|
|
|
1389
|
-
if (needsClaude && shouldOfferRtk()) {
|
|
1429
|
+
if ((needsClaude || needsCodex) && shouldOfferRtk()) {
|
|
1390
1430
|
console.log();
|
|
1391
1431
|
console.log(chalk.cyan('RTK token optimizer (optional — Rust binary, cuts dev-command tokens 60-90%)'));
|
|
1392
1432
|
try {
|
|
1433
|
+
const rtkTargets = [
|
|
1434
|
+
...(needsClaude ? ['claude'] : []),
|
|
1435
|
+
...(needsCodex ? ['codex'] : []),
|
|
1436
|
+
];
|
|
1437
|
+
const rtkMessage = needsClaude && needsCodex
|
|
1438
|
+
? 'Install rtk now? Runs its official installer, adds the GLOBAL Claude Code hook via `rtk init -g --auto-patch`, and makes the native rtk binary available for Codex (no transparent Codex hook).'
|
|
1439
|
+
: needsClaude
|
|
1440
|
+
? 'Install rtk now? Runs its official installer (brew/cargo, or a pinned curl|sh) and adds a GLOBAL Claude Code hook via `rtk init -g --auto-patch`.'
|
|
1441
|
+
: 'Install rtk now? Runs its official installer (brew/cargo, or a pinned curl|sh) and makes the native rtk binary available for Codex workflows. Codex has no transparent RTK hook here.';
|
|
1393
1442
|
const { proceed } = await inquirer.prompt([
|
|
1394
1443
|
{
|
|
1395
1444
|
type: 'confirm',
|
|
1396
1445
|
name: 'proceed',
|
|
1397
|
-
message:
|
|
1446
|
+
message: rtkMessage,
|
|
1398
1447
|
default: false,
|
|
1399
1448
|
},
|
|
1400
1449
|
]);
|
|
1401
1450
|
if (proceed) {
|
|
1402
1451
|
console.log(chalk.gray(' Installing... progress streams below. The cargo fallback compiles from source (can take minutes); Ctrl+C is safe — BYAN is already installed.'));
|
|
1403
|
-
const r = setupRtkIntegration({ log: (m) => console.log(chalk.gray(' ' + m)) });
|
|
1452
|
+
const r = setupRtkIntegration({ targetPlatforms: rtkTargets, log: (m) => console.log(chalk.gray(' ' + m)) });
|
|
1404
1453
|
if (r.synced) {
|
|
1405
|
-
|
|
1454
|
+
if (r.claudeHook && r.codexReady) {
|
|
1455
|
+
console.log(chalk.green(` ✓ rtk ready (${r.installedVia}, v${r.version || '?'}) — restart Claude Code to activate; Codex can use the native rtk binary`));
|
|
1456
|
+
} else if (r.claudeHook) {
|
|
1457
|
+
console.log(chalk.green(` ✓ rtk ready (${r.installedVia}, v${r.version || '?'}) — restart Claude Code to activate`));
|
|
1458
|
+
} else if (r.codexReady) {
|
|
1459
|
+
console.log(chalk.green(` ✓ rtk ready for Codex (${r.installedVia}, v${r.version || '?'}) — native binary installed; no transparent Codex hook`));
|
|
1460
|
+
} else {
|
|
1461
|
+
console.log(chalk.green(` ✓ rtk ready (${r.installedVia}, v${r.version || '?'})`));
|
|
1462
|
+
}
|
|
1406
1463
|
if (r.pathHint) console.log(chalk.yellow(` ⚠ rtk is not on your PATH — add it: ${r.pathHint}`));
|
|
1407
1464
|
} else {
|
|
1408
|
-
|
|
1465
|
+
if (r.codexReady) {
|
|
1466
|
+
console.log(chalk.yellow(` ⚠ rtk Claude hook not wired (${r.reason}) — Codex can still use the native rtk binary; re-run \`npm run setup-rtk\` anytime`));
|
|
1467
|
+
} else {
|
|
1468
|
+
console.log(chalk.yellow(` ⚠ rtk not ready (${r.reason}) — BYAN unaffected; re-run \`npm run setup-rtk\` anytime`));
|
|
1469
|
+
}
|
|
1409
1470
|
if (r.pathHint) console.log(chalk.yellow(` ⚠ rtk found off-PATH — add it then re-run: ${r.pathHint}`));
|
|
1410
1471
|
}
|
|
1411
1472
|
} else {
|
|
@@ -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
|
+
};
|
|
@@ -25,6 +25,10 @@ function getCodexConfigPath() {
|
|
|
25
25
|
return path.join(os.homedir(), '.codex', 'config.toml');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function getCodexSkillsDir() {
|
|
29
|
+
return path.join(os.homedir(), '.codex', 'skills');
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
async function detectCodex() {
|
|
29
33
|
return fs.pathExists(path.join(os.homedir(), '.codex'));
|
|
30
34
|
}
|
|
@@ -123,6 +127,81 @@ async function patchCodexConfig(projectRoot, options = {}) {
|
|
|
123
127
|
return { path: configPath, hadExisting: existing.length > 0, tokenSet: apiToken.length > 0 };
|
|
124
128
|
}
|
|
125
129
|
|
|
130
|
+
function uniqueExistingDirs(dirs) {
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
const out = [];
|
|
133
|
+
for (const dir of dirs.filter(Boolean)) {
|
|
134
|
+
const resolved = path.resolve(dir);
|
|
135
|
+
if (seen.has(resolved)) continue;
|
|
136
|
+
seen.add(resolved);
|
|
137
|
+
out.push(resolved);
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function findSkillDirs(sourceDirs = []) {
|
|
143
|
+
const skills = [];
|
|
144
|
+
const seenNames = new Set();
|
|
145
|
+
|
|
146
|
+
for (const sourceDir of uniqueExistingDirs(sourceDirs)) {
|
|
147
|
+
if (!(await fs.pathExists(sourceDir))) continue;
|
|
148
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
if (!entry.isDirectory()) continue;
|
|
151
|
+
const skillName = entry.name;
|
|
152
|
+
if (seenNames.has(skillName)) continue;
|
|
153
|
+
const skillDir = path.join(sourceDir, skillName);
|
|
154
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
155
|
+
if (!(await fs.pathExists(skillFile))) continue;
|
|
156
|
+
seenNames.add(skillName);
|
|
157
|
+
skills.push({ name: skillName, path: skillDir, sourceDir });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return skills;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function defaultSkillSourceDirs(projectRoot, options = {}) {
|
|
165
|
+
const templateDir = options.templateDir;
|
|
166
|
+
return [
|
|
167
|
+
path.join(projectRoot, '.codex', 'skills'),
|
|
168
|
+
path.join(projectRoot, '.claude', 'skills'),
|
|
169
|
+
templateDir ? path.join(templateDir, '.codex', 'skills') : null,
|
|
170
|
+
templateDir ? path.join(templateDir, '.claude', 'skills') : null,
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function installCodexNativeSkills(projectRoot, options = {}) {
|
|
175
|
+
const destDir = options.destDir || getCodexSkillsDir();
|
|
176
|
+
const sourceDirs = options.sourceDirs || defaultSkillSourceDirs(projectRoot, options);
|
|
177
|
+
const overwrite = options.overwrite !== false;
|
|
178
|
+
|
|
179
|
+
await fs.ensureDir(destDir);
|
|
180
|
+
|
|
181
|
+
const skills = await findSkillDirs(sourceDirs);
|
|
182
|
+
const result = {
|
|
183
|
+
destDir,
|
|
184
|
+
installed: 0,
|
|
185
|
+
skipped: 0,
|
|
186
|
+
skills: [],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
for (const skill of skills) {
|
|
190
|
+
const dest = path.join(destDir, skill.name);
|
|
191
|
+
const exists = await fs.pathExists(dest);
|
|
192
|
+
if (exists && !overwrite) {
|
|
193
|
+
result.skipped++;
|
|
194
|
+
result.skills.push({ name: skill.name, status: 'skipped-existing', path: dest });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
await fs.copy(skill.path, dest, { overwrite: true, errorOnExist: false });
|
|
198
|
+
result.installed++;
|
|
199
|
+
result.skills.push({ name: skill.name, status: exists ? 'updated' : 'installed', path: dest });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
126
205
|
async function setupCodexNative(projectRoot, options = {}) {
|
|
127
206
|
const log = options.quiet ? () => {} : (...a) => console.log(...a);
|
|
128
207
|
|
|
@@ -142,15 +221,28 @@ async function setupCodexNative(projectRoot, options = {}) {
|
|
|
142
221
|
);
|
|
143
222
|
log(chalk.gray(' (or rerun with BYAN_API_TOKEN=byan_xxx in the env)'));
|
|
144
223
|
}
|
|
145
|
-
|
|
146
|
-
|
|
224
|
+
const skills = await installCodexNativeSkills(projectRoot, options);
|
|
225
|
+
if (skills.installed > 0) {
|
|
226
|
+
log(chalk.green(` ✓ Codex native skills installed to ${skills.destDir} (${skills.installed})`));
|
|
227
|
+
} else {
|
|
228
|
+
log(chalk.yellow(` ! No Codex native skills found to install into ${skills.destDir}`));
|
|
229
|
+
}
|
|
230
|
+
if (skills.skipped > 0) {
|
|
231
|
+
log(chalk.gray(` ${skills.skipped} existing skill(s) skipped`));
|
|
232
|
+
}
|
|
233
|
+
log(chalk.gray(' Restart Codex CLI for the new MCP server and skills to load'));
|
|
234
|
+
return { ...result, skills };
|
|
147
235
|
}
|
|
148
236
|
|
|
149
237
|
module.exports = {
|
|
150
238
|
setupCodexNative,
|
|
151
239
|
patchCodexConfig,
|
|
240
|
+
installCodexNativeSkills,
|
|
241
|
+
findSkillDirs,
|
|
242
|
+
defaultSkillSourceDirs,
|
|
152
243
|
stripServerSections,
|
|
153
244
|
buildByanBlock,
|
|
154
245
|
getCodexConfigPath,
|
|
246
|
+
getCodexSkillsDir,
|
|
155
247
|
detectCodex,
|
|
156
248
|
};
|