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
@@ -0,0 +1,300 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+
7
+ const FORMAT = 'byan-project-handoff';
8
+ const VERSION = '1.0';
9
+
10
+ function nowIso() {
11
+ return new Date().toISOString();
12
+ }
13
+
14
+ function slugify(s) {
15
+ return String(s || 'handoff')
16
+ .trim()
17
+ .toLowerCase()
18
+ .replace(/[^a-z0-9]+/g, '-')
19
+ .replace(/^-+|-+$/g, '')
20
+ .slice(0, 80) || 'handoff';
21
+ }
22
+
23
+ function timestampForFile(d = new Date()) {
24
+ return d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
25
+ }
26
+
27
+ function handoffDir(root) {
28
+ return path.join(root, '_byan-output', 'handoffs');
29
+ }
30
+
31
+ function defaultHandoffPath(root, handoff, date = new Date()) {
32
+ const from = slugify(handoff.from || 'unknown');
33
+ const to = slugify(handoff.to || 'next');
34
+ const task = slugify(handoff.currentTask || handoff.project || 'project');
35
+ return path.join(handoffDir(root), `${timestampForFile(date)}-${from}-to-${to}-${task}.md`);
36
+ }
37
+
38
+ function arrayify(value) {
39
+ if (Array.isArray(value)) return value.filter((v) => String(v || '').trim()).map(String);
40
+ if (value == null || value === '') return [];
41
+ return [String(value)];
42
+ }
43
+
44
+ function readJsonFile(file) {
45
+ try {
46
+ if (!fs.existsSync(file)) return null;
47
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function safeRun(cmd, { cwd, maxBuffer = 1024 * 1024 } = {}) {
54
+ try {
55
+ return String(execSync(cmd, { cwd, stdio: 'pipe', encoding: 'utf8', maxBuffer })).trim();
56
+ } catch {
57
+ return '';
58
+ }
59
+ }
60
+
61
+ function collectGitSnapshot(root, { run = safeRun } = {}) {
62
+ const status = run('git status --short', { cwd: root });
63
+ const branch = run('git branch --show-current', { cwd: root }) || run('git rev-parse --abbrev-ref HEAD', { cwd: root });
64
+ const head = run('git rev-parse --short HEAD', { cwd: root });
65
+ const changedFiles = status
66
+ .split('\n')
67
+ .map((line) => line.trim())
68
+ .filter(Boolean)
69
+ .map((line) => line.replace(/^.. /, '').replace(/^..\s+/, ''));
70
+ return { branch: branch || null, head: head || null, status, changedFiles };
71
+ }
72
+
73
+ function collectFdState(root) {
74
+ const state = readJsonFile(path.join(root, '_byan-output', 'fd-state.json'));
75
+ if (!state) return null;
76
+ return {
77
+ fd_id: state.fd_id || null,
78
+ feature_name: state.feature_name || null,
79
+ phase: state.phase || null,
80
+ strict_mode: Boolean(state.strict_mode),
81
+ backlog: Array.isArray(state.backlog) ? state.backlog : [],
82
+ validate_verdict: state.validate_verdict || null,
83
+ };
84
+ }
85
+
86
+ function buildHandoff(input = {}) {
87
+ const root = path.resolve(input.root || process.cwd());
88
+ const git = input.git || collectGitSnapshot(root, input);
89
+ const fd = input.fd === undefined ? collectFdState(root) : input.fd;
90
+ const project = input.project || path.basename(root);
91
+ const from = input.from || process.env.BYAN_HANDOFF_FROM || 'unknown';
92
+ const to = input.to || process.env.BYAN_HANDOFF_TO || 'next-assistant';
93
+ const currentTask = input.currentTask || input.task || (fd && fd.feature_name) || 'Resume project work';
94
+ const filesTouched = arrayify(input.filesTouched || input.files || git.changedFiles);
95
+ const decisions = arrayify(input.decisions);
96
+ const blockers = arrayify(input.blockers);
97
+ const nextActions = arrayify(input.nextActions || input.next);
98
+ const commands = arrayify(input.commands);
99
+ const notes = arrayify(input.notes || input.note);
100
+
101
+ return {
102
+ format: FORMAT,
103
+ version: VERSION,
104
+ project,
105
+ root,
106
+ from,
107
+ to,
108
+ createdAt: input.createdAt || nowIso(),
109
+ currentTask,
110
+ summary: input.summary || '',
111
+ decisions,
112
+ filesTouched,
113
+ commands,
114
+ blockers,
115
+ nextActions,
116
+ notes,
117
+ git,
118
+ fd,
119
+ };
120
+ }
121
+
122
+ function renderList(items, empty = '- (none recorded)') {
123
+ const arr = arrayify(items);
124
+ if (!arr.length) return empty;
125
+ return arr.map((item) => `- ${item}`).join('\n');
126
+ }
127
+
128
+ function renderJsonBlock(handoff) {
129
+ return [
130
+ '```json byan-handoff',
131
+ JSON.stringify(handoff, null, 2),
132
+ '```',
133
+ ].join('\n');
134
+ }
135
+
136
+ function renderMarkdown(handoff) {
137
+ const h = buildHandoff(handoff);
138
+ const lines = [];
139
+ lines.push(`# BYAN Project Handoff: ${h.from} -> ${h.to}`);
140
+ lines.push('');
141
+ lines.push('> Portable Markdown handoff for switching between Claude Code and Codex. This file is the source of truth for the next assistant; native assistant memory is optional context only.');
142
+ lines.push('');
143
+ lines.push('## Machine Data');
144
+ lines.push('');
145
+ lines.push(renderJsonBlock(h));
146
+ lines.push('');
147
+ lines.push('## Resume Prompt');
148
+ lines.push('');
149
+ lines.push(renderResumePrompt(h));
150
+ lines.push('');
151
+ lines.push('## Human Summary');
152
+ lines.push('');
153
+ lines.push(h.summary || '_A completer avant le passage si le resume automatique est insuffisant._');
154
+ lines.push('');
155
+ lines.push('## Current Task');
156
+ lines.push('');
157
+ lines.push(h.currentTask);
158
+ lines.push('');
159
+ lines.push('## Decisions');
160
+ lines.push('');
161
+ lines.push(renderList(h.decisions));
162
+ lines.push('');
163
+ lines.push('## Files Touched');
164
+ lines.push('');
165
+ lines.push(renderList(h.filesTouched));
166
+ lines.push('');
167
+ lines.push('## Commands And Tests');
168
+ lines.push('');
169
+ lines.push(renderList(h.commands));
170
+ lines.push('');
171
+ lines.push('## Blockers And Risks');
172
+ lines.push('');
173
+ lines.push(renderList(h.blockers));
174
+ lines.push('');
175
+ lines.push('## Next Actions');
176
+ lines.push('');
177
+ lines.push(renderList(h.nextActions, '- Re-read this handoff, inspect the listed files, then continue the current task.'));
178
+ lines.push('');
179
+ lines.push('## Notes');
180
+ lines.push('');
181
+ lines.push(renderList(h.notes));
182
+ lines.push('');
183
+ lines.push('## Git Snapshot');
184
+ lines.push('');
185
+ lines.push(`- Branch: ${h.git && h.git.branch ? h.git.branch : 'unknown'}`);
186
+ lines.push(`- Head: ${h.git && h.git.head ? h.git.head : 'unknown'}`);
187
+ lines.push('');
188
+ lines.push('```text');
189
+ lines.push((h.git && h.git.status) || '(clean or unavailable)');
190
+ lines.push('```');
191
+ lines.push('');
192
+ if (h.fd) {
193
+ lines.push('## FD Snapshot');
194
+ lines.push('');
195
+ lines.push(`- FD: ${h.fd.fd_id || 'unknown'}`);
196
+ lines.push(`- Feature: ${h.fd.feature_name || 'unknown'}`);
197
+ lines.push(`- Phase: ${h.fd.phase || 'unknown'}`);
198
+ lines.push(`- Strict: ${h.fd.strict_mode ? 'yes' : 'no'}`);
199
+ lines.push('');
200
+ lines.push('### Backlog');
201
+ lines.push('');
202
+ lines.push(renderList((h.fd.backlog || []).map((b) => `${b.id || '-'} [${b.status || '?'}] ${b.title || ''}`)));
203
+ lines.push('');
204
+ }
205
+ return lines.join('\n');
206
+ }
207
+
208
+ function extractJsonBlock(markdown) {
209
+ const text = String(markdown || '');
210
+ const re = /```json\s+byan-handoff\s*\n([\s\S]*?)\n```/m;
211
+ const m = text.match(re);
212
+ if (!m) throw new Error('Missing ```json byan-handoff block');
213
+ try {
214
+ return JSON.parse(m[1]);
215
+ } catch (err) {
216
+ throw new Error(`Invalid byan-handoff JSON: ${err.message}`);
217
+ }
218
+ }
219
+
220
+ function parseMarkdown(markdown) {
221
+ const h = extractJsonBlock(markdown);
222
+ if (h.format !== FORMAT) throw new Error(`Invalid handoff format: ${h.format || 'missing'}`);
223
+ if (!h.version) throw new Error('Missing handoff version');
224
+ return h;
225
+ }
226
+
227
+ function normalizeProvider(value) {
228
+ return slugify(value || '');
229
+ }
230
+
231
+ function providerMatches(actual, expected) {
232
+ const a = normalizeProvider(actual);
233
+ const e = normalizeProvider(expected);
234
+ if (!e) return true;
235
+ return a === e || a.startsWith(`${e}-`);
236
+ }
237
+
238
+ function renderResumePrompt(handoff) {
239
+ const h = buildHandoff(handoff);
240
+ const lines = [];
241
+ lines.push(`Continue this BYAN project from a portable handoff created by ${h.from} for ${h.to}.`);
242
+ lines.push(`Project: ${h.project}`);
243
+ lines.push(`Task: ${h.currentTask}`);
244
+ if (h.summary) lines.push(`Summary: ${h.summary}`);
245
+ if (h.decisions.length) lines.push(`Decisions: ${h.decisions.join(' | ')}`);
246
+ if (h.filesTouched.length) lines.push(`Files to inspect first: ${h.filesTouched.join(', ')}`);
247
+ if (h.commands.length) lines.push(`Known commands/tests: ${h.commands.join(' | ')}`);
248
+ if (h.blockers.length) lines.push(`Blockers/risks: ${h.blockers.join(' | ')}`);
249
+ if (h.nextActions.length) lines.push(`Next actions: ${h.nextActions.join(' | ')}`);
250
+ if (h.fd && h.fd.phase) lines.push(`FD state: ${h.fd.feature_name || h.fd.fd_id || 'unknown'} is in phase ${h.fd.phase}.`);
251
+ lines.push('Use repo files and BYAN portable state as source of truth; do not rely on native assistant memory.');
252
+ return lines.join('\n');
253
+ }
254
+
255
+ function writeHandoff(root, handoff, { outPath, mkdirp = fs.mkdirSync, writeFile = fs.writeFileSync } = {}) {
256
+ const h = buildHandoff({ ...handoff, root });
257
+ const target = path.resolve(outPath || defaultHandoffPath(path.resolve(root), h));
258
+ mkdirp(path.dirname(target), { recursive: true });
259
+ writeFile(target, renderMarkdown(h), 'utf8');
260
+ return { path: target, handoff: h };
261
+ }
262
+
263
+ function latestHandoff(root, { from = null, exists = fs.existsSync, readdir = fs.readdirSync, readFile = fs.readFileSync } = {}) {
264
+ const dir = handoffDir(path.resolve(root));
265
+ if (!exists(dir)) return null;
266
+ const files = readdir(dir)
267
+ .filter((f) => /\.md$/i.test(f))
268
+ .sort()
269
+ .reverse();
270
+ if (!files.length) return null;
271
+ if (!from) return path.join(dir, files[0]);
272
+
273
+ for (const file of files) {
274
+ const full = path.join(dir, file);
275
+ try {
276
+ const handoff = parseMarkdown(readFile(full, 'utf8'));
277
+ if (providerMatches(handoff.from, from)) return full;
278
+ } catch {
279
+ // Ignore non-handoff markdown files in the handoff directory.
280
+ }
281
+ }
282
+ return null;
283
+ }
284
+
285
+ module.exports = {
286
+ FORMAT,
287
+ VERSION,
288
+ buildHandoff,
289
+ collectFdState,
290
+ collectGitSnapshot,
291
+ defaultHandoffPath,
292
+ extractJsonBlock,
293
+ handoffDir,
294
+ latestHandoff,
295
+ parseMarkdown,
296
+ providerMatches,
297
+ renderMarkdown,
298
+ renderResumePrompt,
299
+ writeHandoff,
300
+ };
@@ -5,16 +5,20 @@
5
5
  *
6
6
  * RTK ("Rust Token Killer", Apache-2.0) is a single zero-dependency binary that
7
7
  * filters/compresses dev-command output before it reaches the LLM context
8
- * (-60/90% tokens). It wires into Claude Code through its OWN hook installer.
8
+ * (-60/90% tokens). It wires into Claude Code through its OWN hook installer;
9
+ * for Codex installs BYAN can make the native binary available, but does not
10
+ * claim a transparent hook layer where Codex has no BYAN hook adapter.
9
11
  *
10
12
  * MAINTAINABILITY by design (the whole point of choosing this shape):
11
13
  * - We do NOT reimplement per-OS download / checksum / version pinning. We
12
14
  * DELEGATE the install to rtk's own canonical installer (brew / the official
13
15
  * install.sh / cargo), and DELEGATE the Claude Code hook wiring to rtk's own
14
16
  * `rtk init -g --auto-patch` (the `--auto-patch` is what makes it non-
15
- * interactive — see wireHook). BYAN maintains only "pick the available
16
- * installer, run it bounded + visible, locate the binary, ask rtk to wire its
17
- * hook" a tiny surface, bumped via one constant.
17
+ * interactive — see wireHook). For Codex, BYAN stops at verified native
18
+ * binary availability and reports that honestly. BYAN maintains only "pick
19
+ * the available installer, run it bounded + visible, locate the binary, ask
20
+ * rtk to wire its hook when Claude is targeted" — a tiny surface, bumped via
21
+ * one constant.
18
22
  * - SUPPLY-CHAIN: we pin to a TAG, never a moving branch. cargo builds the
19
23
  * tagged source (`--tag`), and install.sh is fetched from the IMMUTABLE tag ref.
20
24
  * That script verifies a SHA-256 of the downloaded binary against a published
@@ -39,8 +43,8 @@
39
43
  *
40
44
  * Mirrors the ENTRY-POINT shape of byan-web-integration.js / byan-leantime-
41
45
  * integration.js (one `setup*Integration`); the return contract is { ok, synced,
42
- * reason, ... } because persistence is owned by rtk's own `rtk init -g --auto-patch`, not by
43
- * byan-platform-config.
46
+ * reason, ... } because persistence is owned by rtk's own `rtk init -g
47
+ * --auto-patch`, not by byan-platform-config.
44
48
  */
45
49
 
46
50
  const { execSync } = require('child_process');
@@ -128,6 +132,48 @@ function pathHintFor(bin, { env = process.env, platform = process.platform } = {
128
132
  return `export PATH="${dir}:$PATH"`;
129
133
  }
130
134
 
135
+ function normalizeTargetPlatforms(targetPlatforms = ['claude']) {
136
+ const raw = Array.isArray(targetPlatforms) ? targetPlatforms : [targetPlatforms];
137
+ const set = new Set();
138
+ for (const p of raw) {
139
+ const key = String(p || '').trim().toLowerCase();
140
+ if (key === 'claude' || key === 'claude-code') set.add('claude');
141
+ if (key === 'codex') set.add('codex');
142
+ }
143
+ if (!set.size) set.add('claude');
144
+ return set;
145
+ }
146
+
147
+ function codexReady({
148
+ log,
149
+ installedVia,
150
+ version,
151
+ bin = 'rtk',
152
+ env = process.env,
153
+ platform = process.platform,
154
+ claudeHook = false,
155
+ } = {}) {
156
+ const offPath = bin !== 'rtk';
157
+ log(`rtk: ready for Codex (${installedVia}, v${version || '?'}) — native binary available; no transparent Codex hook is wired.`);
158
+ const r = {
159
+ ok: true,
160
+ synced: true,
161
+ reason: 'codex-binary-ready',
162
+ installed: true,
163
+ installedVia,
164
+ version,
165
+ hook: Boolean(claudeHook),
166
+ claudeHook: Boolean(claudeHook),
167
+ codexReady: true,
168
+ bin,
169
+ };
170
+ if (offPath) {
171
+ r.pathHint = pathHintFor(bin, { env, platform });
172
+ log(`rtk: note — installed at ${bin}, not on your PATH. Add it: ${r.pathHint}`);
173
+ }
174
+ return r;
175
+ }
176
+
131
177
  /**
132
178
  * rtkStatus() -> { installed, version, bin }. Never throws. Probes `<bin>
133
179
  * --version` ("rtk 0.42.4"), default bin "rtk" (on PATH). An installed-but-
@@ -209,7 +255,7 @@ function wireHook({ run, log, installedVia, version, bin = 'rtk', env = process.
209
255
  try {
210
256
  run(initCmd, { stdio: 'pipe' });
211
257
  log(`rtk: ready (${installedVia}, v${version || '?'}) — hook wired via '${initCmd}'. Restart Claude Code to activate.`);
212
- const r = { ok: true, synced: true, reason: 'wired', installed: true, installedVia, version, hook: true, bin };
258
+ const r = { ok: true, synced: true, reason: 'wired', installed: true, installedVia, version, hook: true, claudeHook: true, bin };
213
259
  if (offPath) {
214
260
  r.pathHint = pathHintFor(bin, { env, platform });
215
261
  log(`rtk: note — installed at ${bin}, not on your PATH. Add it: ${r.pathHint}`);
@@ -217,7 +263,7 @@ function wireHook({ run, log, installedVia, version, bin = 'rtk', env = process.
217
263
  return r;
218
264
  } catch (err) {
219
265
  log(`rtk: installed (${installedVia}) but '${initCmd}' failed (${oneLine(err)}) — run it manually, BYAN unaffected.`);
220
- const r = { ok: true, synced: false, reason: 'hook-failed', installed: true, installedVia, version, hook: false, bin };
266
+ const r = { ok: true, synced: false, reason: 'hook-failed', installed: true, installedVia, version, hook: false, claudeHook: false, bin };
221
267
  if (offPath) r.pathHint = pathHintFor(bin, { env, platform });
222
268
  return r;
223
269
  }
@@ -240,6 +286,9 @@ function wireHook({ run, log, installedVia, version, bin = 'rtk', env = process.
240
286
  * @param {Function} [o.resolve] binary resolver (default resolveBinary) — injected in tests
241
287
  * @param {Function} [o.log] one-line breadcrumb sink (default no-op)
242
288
  * @param {object} [o.env] environment (default process.env) — for the timeout override
289
+ * @param {string[]|string} [o.targetPlatforms] platforms to prepare:
290
+ * claude wires RTK's Claude Code hook; codex verifies the
291
+ * native binary and reports no transparent hook.
243
292
  */
244
293
  function setupRtkIntegration({
245
294
  run = execSync,
@@ -248,16 +297,33 @@ function setupRtkIntegration({
248
297
  log = () => {},
249
298
  env = process.env,
250
299
  platform = process.platform,
300
+ targetPlatforms = ['claude'],
251
301
  } = {}) {
302
+ const targets = normalizeTargetPlatforms(targetPlatforms);
303
+ const wantsClaude = targets.has('claude');
304
+ const wantsCodex = targets.has('codex');
305
+ const finishReady = ({ installedVia, version, bin }) => {
306
+ if (!wantsClaude && wantsCodex) {
307
+ return codexReady({ log, installedVia, version, bin, env, platform });
308
+ }
309
+ const r = wireHook({ run, log, installedVia, version, bin, env, platform });
310
+ if (wantsCodex && r.installed) {
311
+ r.codexReady = true;
312
+ if (r.reason === 'wired') r.reason = 'wired+codex-binary-ready';
313
+ log('rtk: also ready for Codex — native binary available; no transparent Codex hook is wired.');
314
+ }
315
+ return r;
316
+ };
317
+
252
318
  const before = locateRtk({ run, has, resolve, env });
253
319
  if (before.installed) {
254
- return wireHook({ run, log, installedVia: 'already-present', version: before.version, bin: before.bin, env, platform });
320
+ return finishReady({ installedVia: 'already-present', version: before.version, bin: before.bin });
255
321
  }
256
322
 
257
323
  const strat = pickStrategy({ has });
258
324
  if (!strat) {
259
325
  log('rtk: no installer found (brew / curl / cargo) — skipped. BYAN install unaffected.');
260
- return { ok: true, synced: false, reason: 'no-installer', installed: false, hook: false };
326
+ return { ok: true, synced: false, reason: 'no-installer', installed: false, hook: false, claudeHook: false, codexReady: false };
261
327
  }
262
328
 
263
329
  // Breadcrumb BEFORE the delegated install. stdio is INHERITED so the installer's
@@ -269,10 +335,10 @@ function setupRtkIntegration({
269
335
  } catch (err) {
270
336
  if (isTimeout(err)) {
271
337
  log(`rtk: install via ${strat.id} exceeded ${Math.round(timeoutMs / MIN)}min — stopped (a background build may still continue). Retry with 'npm run setup-rtk' (or widen BYAN_RTK_TIMEOUT_MS). BYAN unaffected.`);
272
- return { ok: true, synced: false, reason: `install-timeout:${strat.id}`, installed: false, hook: false };
338
+ return { ok: true, synced: false, reason: `install-timeout:${strat.id}`, installed: false, hook: false, claudeHook: false, codexReady: false };
273
339
  }
274
340
  log(`rtk: install via ${strat.id} failed (${oneLine(err)}) — skipped gracefully.`);
275
- return { ok: true, synced: false, reason: `install-failed:${strat.id}`, installed: false, hook: false };
341
+ return { ok: true, synced: false, reason: `install-failed:${strat.id}`, installed: false, hook: false, claudeHook: false, codexReady: false };
276
342
  }
277
343
 
278
344
  // The binary may be installed but OFF PATH (cargo -> ~/.cargo/bin). Resolve it
@@ -280,10 +346,10 @@ function setupRtkIntegration({
280
346
  const after = locateRtk({ run, has, resolve, env });
281
347
  if (!after.installed) {
282
348
  log(`rtk: install via ${strat.id} ran but 'rtk --version' did not confirm — skipped.`);
283
- return { ok: true, synced: false, reason: 'install-unverified', installed: false, hook: false };
349
+ return { ok: true, synced: false, reason: 'install-unverified', installed: false, hook: false, claudeHook: false, codexReady: false };
284
350
  }
285
351
 
286
- return wireHook({ run, log, installedVia: strat.id, version: after.version, bin: after.bin, env, platform });
352
+ return finishReady({ installedVia: strat.id, version: after.version, bin: after.bin });
287
353
  }
288
354
 
289
355
  /**
@@ -307,6 +373,7 @@ module.exports = {
307
373
  timeoutFor,
308
374
  isTimeout,
309
375
  pathHintFor,
376
+ normalizeTargetPlatforms,
310
377
  doctor,
311
378
  shouldOfferRtk,
312
379
  RTK_VERSION,
@@ -67,6 +67,22 @@ Doctrine d'equipe complete (template role-in-team, analogie orchestre, principes
67
67
  - Architecture portable (noyau portable, projection native): voir .claude/rules/portable-core.md (charge a la demande)
68
68
  - Systeme API byan_web: voir @.claude/rules/byan-api.md
69
69
 
70
+ ## Handoff Claude/Codex
71
+
72
+ Quand l'utilisateur demande `importe depuis claude` ou `importe depuis codex`,
73
+ ne demande pas de precision si un handoff depuis cette source existe. Lance:
74
+
75
+ ```bash
76
+ byan-handoff latest --from <source demandee> --prompt
77
+ ```
78
+
79
+ Utilise le prompt produit comme contexte de reprise, inspecte les fichiers listes,
80
+ puis continue le travail. Si aucun fichier ne correspond, signale simplement
81
+ qu'aucun handoff depuis cette source n'a ete trouve et propose
82
+ `byan-handoff latest --prompt` comme fallback seulement si l'utilisateur accepte
83
+ de reprendre depuis le dernier handoff toutes sources confondues. Ne t'appuie
84
+ pas sur les memoires natives Claude/Codex comme source de verite.
85
+
70
86
  ## API byan_web
71
87
 
72
88
  BYAN expose une API REST via `$BYAN_API_URL` avec authentification par token (`ApiKey` ou `Bearer`).
@@ -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 };