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
|
@@ -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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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 };
|