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.
Files changed (46) hide show
  1. package/CHANGELOG.md +153 -1
  2. package/install/bin/byan-handoff.js +139 -0
  3. package/install/bin/create-byan-agent-v2.js +70 -9
  4. package/install/lib/codex-autodelegate-setup.js +100 -0
  5. package/install/lib/codex-native-setup.js +94 -2
  6. package/install/lib/project-handoff.js +300 -0
  7. package/install/lib/rtk-integration.js +81 -14
  8. package/install/templates/.claude/CLAUDE.md +16 -0
  9. package/install/templates/.claude/hooks/codex-autodelegate.js +74 -0
  10. package/install/templates/.claude/hooks/lib/autodelegate-decision.js +152 -0
  11. package/install/templates/.claude/hooks/lib/perf-routing.js +47 -0
  12. package/install/templates/.claude/hooks/lib/usage-estimator.js +262 -0
  13. package/install/templates/.claude/hooks/tier-script-guard.js +185 -0
  14. package/install/templates/.claude/rules/native-workflows.md +33 -7
  15. package/install/templates/.claude/settings.json +35 -22
  16. package/install/templates/.claude/skills/byan-byan/SKILL.md +21 -2
  17. package/install/templates/.claude/skills/byan-codex/SKILL.md +7 -0
  18. package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +3 -1
  19. package/install/templates/.claude/workflows/sprint-planning.js +2 -2
  20. package/install/templates/.claude/workflows/testarch-trace.js +3 -2
  21. package/install/templates/.codex/skills/byan/SKILL.md +77 -0
  22. package/install/templates/_byan/INDEX.md +6 -2
  23. package/install/templates/_byan/_config/workflow-manifest.csv +1 -1
  24. package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-tier-script.js +52 -0
  25. package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch.js +22 -0
  26. package/install/templates/_byan/mcp/byan-mcp-server/lib/native-tiers.js +26 -9
  27. package/install/templates/_byan/mcp/byan-mcp-server/lib/tier-script.js +104 -0
  28. package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-lint.js +92 -6
  29. package/install/templates/_byan/mcp/byan-mcp-server/server.js +22 -7
  30. package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +3 -3
  31. package/install/templates/_byan/workflow/simple/byan/project-handoff-workflow.md +90 -0
  32. package/install/templates/dist/skill-bundles/byan-byan.zip +0 -0
  33. package/install/templates/dist/skill-bundles/byan-codex.zip +0 -0
  34. package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
  35. package/install/templates/docs/codex-auto-delegation.md +140 -0
  36. package/install/templates/docs/native-workflows-contract.md +55 -12
  37. package/package.json +2 -1
  38. package/src/loadbalancer/capability-matrix.js +14 -0
  39. package/src/loadbalancer/degradation-ladder.js +121 -0
  40. package/src/loadbalancer/loadbalancer.default.yaml +23 -1
  41. package/src/loadbalancer/mcp-server.js +200 -18
  42. package/src/loadbalancer/providers/codex-provider.js +260 -0
  43. package/src/loadbalancer/providers/factory.js +36 -0
  44. package/src/loadbalancer/subscription-window.js +142 -0
  45. package/src/loadbalancer/switch-tolerance.js +63 -0
  46. 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.39.0] - 2026-07-02
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 MCP setup (~/.codex/config.toml)'));
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 MCP setup failed: ${error.message}`));
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: '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`.',
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
- console.log(chalk.green(` ✓ rtk ready (${r.installedVia}, v${r.version || '?'}) — restart Claude Code to activate`));
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
- console.log(chalk.yellow(` ⚠ rtk not wired (${r.reason}) — BYAN unaffected; re-run \`npm run setup-rtk\` anytime`));
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
- log(chalk.gray(' Restart Codex CLI for the new MCP server to load'));
146
- return result;
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
  };