create-byan-agent 2.41.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 +51 -0
- package/install/bin/byan-handoff.js +139 -0
- package/install/bin/create-byan-agent-v2.js +30 -9
- 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/settings.json +24 -24
- package/install/templates/.claude/skills/byan-byan/SKILL.md +14 -0
- package/install/templates/.claude/skills/byan-codex/SKILL.md +7 -0
- 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/skill-bundles-manifest.json +2 -2
- 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/docs/codex-auto-delegation.md +140 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
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
|
+
|
|
12
63
|
### Added - Codex auto-delegation (opt-in, native)
|
|
13
64
|
- **BYAN now proposes handing delegable work to Codex on your ChatGPT
|
|
14
65
|
subscription (no API credit) when Claude nears its 5h limit.** A
|
|
@@ -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();
|
|
@@ -1324,14 +1324,14 @@ async function install(options = {}) {
|
|
|
1324
1324
|
|
|
1325
1325
|
if (needsCodex) {
|
|
1326
1326
|
console.log();
|
|
1327
|
-
console.log(chalk.cyan('Codex CLI
|
|
1327
|
+
console.log(chalk.cyan('Codex CLI native setup (~/.codex/config.toml + ~/.codex/skills)'));
|
|
1328
1328
|
try {
|
|
1329
|
-
await setupCodexNative(projectRoot);
|
|
1329
|
+
await setupCodexNative(projectRoot, { templateDir, force: true });
|
|
1330
1330
|
} catch (error) {
|
|
1331
|
-
console.log(chalk.red(` ✘ Codex
|
|
1331
|
+
console.log(chalk.red(` ✘ Codex native setup failed: ${error.message}`));
|
|
1332
1332
|
console.log(
|
|
1333
1333
|
chalk.yellow(
|
|
1334
|
-
` → 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.`
|
|
1335
1335
|
)
|
|
1336
1336
|
);
|
|
1337
1337
|
}
|
|
@@ -1426,26 +1426,47 @@ async function install(options = {}) {
|
|
|
1426
1426
|
}
|
|
1427
1427
|
}
|
|
1428
1428
|
|
|
1429
|
-
if (needsClaude && shouldOfferRtk()) {
|
|
1429
|
+
if ((needsClaude || needsCodex) && shouldOfferRtk()) {
|
|
1430
1430
|
console.log();
|
|
1431
1431
|
console.log(chalk.cyan('RTK token optimizer (optional — Rust binary, cuts dev-command tokens 60-90%)'));
|
|
1432
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.';
|
|
1433
1442
|
const { proceed } = await inquirer.prompt([
|
|
1434
1443
|
{
|
|
1435
1444
|
type: 'confirm',
|
|
1436
1445
|
name: 'proceed',
|
|
1437
|
-
message:
|
|
1446
|
+
message: rtkMessage,
|
|
1438
1447
|
default: false,
|
|
1439
1448
|
},
|
|
1440
1449
|
]);
|
|
1441
1450
|
if (proceed) {
|
|
1442
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.'));
|
|
1443
|
-
const r = setupRtkIntegration({ log: (m) => console.log(chalk.gray(' ' + m)) });
|
|
1452
|
+
const r = setupRtkIntegration({ targetPlatforms: rtkTargets, log: (m) => console.log(chalk.gray(' ' + m)) });
|
|
1444
1453
|
if (r.synced) {
|
|
1445
|
-
|
|
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
|
+
}
|
|
1446
1463
|
if (r.pathHint) console.log(chalk.yellow(` ⚠ rtk is not on your PATH — add it: ${r.pathHint}`));
|
|
1447
1464
|
} else {
|
|
1448
|
-
|
|
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
|
+
}
|
|
1449
1470
|
if (r.pathHint) console.log(chalk.yellow(` ⚠ rtk found off-PATH — add it then re-run: ${r.pathHint}`));
|
|
1450
1471
|
}
|
|
1451
1472
|
} else {
|
|
@@ -25,6 +25,10 @@ function getCodexConfigPath() {
|
|
|
25
25
|
return path.join(os.homedir(), '.codex', 'config.toml');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function getCodexSkillsDir() {
|
|
29
|
+
return path.join(os.homedir(), '.codex', 'skills');
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
async function detectCodex() {
|
|
29
33
|
return fs.pathExists(path.join(os.homedir(), '.codex'));
|
|
30
34
|
}
|
|
@@ -123,6 +127,81 @@ async function patchCodexConfig(projectRoot, options = {}) {
|
|
|
123
127
|
return { path: configPath, hadExisting: existing.length > 0, tokenSet: apiToken.length > 0 };
|
|
124
128
|
}
|
|
125
129
|
|
|
130
|
+
function uniqueExistingDirs(dirs) {
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
const out = [];
|
|
133
|
+
for (const dir of dirs.filter(Boolean)) {
|
|
134
|
+
const resolved = path.resolve(dir);
|
|
135
|
+
if (seen.has(resolved)) continue;
|
|
136
|
+
seen.add(resolved);
|
|
137
|
+
out.push(resolved);
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function findSkillDirs(sourceDirs = []) {
|
|
143
|
+
const skills = [];
|
|
144
|
+
const seenNames = new Set();
|
|
145
|
+
|
|
146
|
+
for (const sourceDir of uniqueExistingDirs(sourceDirs)) {
|
|
147
|
+
if (!(await fs.pathExists(sourceDir))) continue;
|
|
148
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
if (!entry.isDirectory()) continue;
|
|
151
|
+
const skillName = entry.name;
|
|
152
|
+
if (seenNames.has(skillName)) continue;
|
|
153
|
+
const skillDir = path.join(sourceDir, skillName);
|
|
154
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
155
|
+
if (!(await fs.pathExists(skillFile))) continue;
|
|
156
|
+
seenNames.add(skillName);
|
|
157
|
+
skills.push({ name: skillName, path: skillDir, sourceDir });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return skills;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function defaultSkillSourceDirs(projectRoot, options = {}) {
|
|
165
|
+
const templateDir = options.templateDir;
|
|
166
|
+
return [
|
|
167
|
+
path.join(projectRoot, '.codex', 'skills'),
|
|
168
|
+
path.join(projectRoot, '.claude', 'skills'),
|
|
169
|
+
templateDir ? path.join(templateDir, '.codex', 'skills') : null,
|
|
170
|
+
templateDir ? path.join(templateDir, '.claude', 'skills') : null,
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function installCodexNativeSkills(projectRoot, options = {}) {
|
|
175
|
+
const destDir = options.destDir || getCodexSkillsDir();
|
|
176
|
+
const sourceDirs = options.sourceDirs || defaultSkillSourceDirs(projectRoot, options);
|
|
177
|
+
const overwrite = options.overwrite !== false;
|
|
178
|
+
|
|
179
|
+
await fs.ensureDir(destDir);
|
|
180
|
+
|
|
181
|
+
const skills = await findSkillDirs(sourceDirs);
|
|
182
|
+
const result = {
|
|
183
|
+
destDir,
|
|
184
|
+
installed: 0,
|
|
185
|
+
skipped: 0,
|
|
186
|
+
skills: [],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
for (const skill of skills) {
|
|
190
|
+
const dest = path.join(destDir, skill.name);
|
|
191
|
+
const exists = await fs.pathExists(dest);
|
|
192
|
+
if (exists && !overwrite) {
|
|
193
|
+
result.skipped++;
|
|
194
|
+
result.skills.push({ name: skill.name, status: 'skipped-existing', path: dest });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
await fs.copy(skill.path, dest, { overwrite: true, errorOnExist: false });
|
|
198
|
+
result.installed++;
|
|
199
|
+
result.skills.push({ name: skill.name, status: exists ? 'updated' : 'installed', path: dest });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
126
205
|
async function setupCodexNative(projectRoot, options = {}) {
|
|
127
206
|
const log = options.quiet ? () => {} : (...a) => console.log(...a);
|
|
128
207
|
|
|
@@ -142,15 +221,28 @@ async function setupCodexNative(projectRoot, options = {}) {
|
|
|
142
221
|
);
|
|
143
222
|
log(chalk.gray(' (or rerun with BYAN_API_TOKEN=byan_xxx in the env)'));
|
|
144
223
|
}
|
|
145
|
-
|
|
146
|
-
|
|
224
|
+
const skills = await installCodexNativeSkills(projectRoot, options);
|
|
225
|
+
if (skills.installed > 0) {
|
|
226
|
+
log(chalk.green(` ✓ Codex native skills installed to ${skills.destDir} (${skills.installed})`));
|
|
227
|
+
} else {
|
|
228
|
+
log(chalk.yellow(` ! No Codex native skills found to install into ${skills.destDir}`));
|
|
229
|
+
}
|
|
230
|
+
if (skills.skipped > 0) {
|
|
231
|
+
log(chalk.gray(` ${skills.skipped} existing skill(s) skipped`));
|
|
232
|
+
}
|
|
233
|
+
log(chalk.gray(' Restart Codex CLI for the new MCP server and skills to load'));
|
|
234
|
+
return { ...result, skills };
|
|
147
235
|
}
|
|
148
236
|
|
|
149
237
|
module.exports = {
|
|
150
238
|
setupCodexNative,
|
|
151
239
|
patchCodexConfig,
|
|
240
|
+
installCodexNativeSkills,
|
|
241
|
+
findSkillDirs,
|
|
242
|
+
defaultSkillSourceDirs,
|
|
152
243
|
stripServerSections,
|
|
153
244
|
buildByanBlock,
|
|
154
245
|
getCodexConfigPath,
|
|
246
|
+
getCodexSkillsDir,
|
|
155
247
|
detectCodex,
|
|
156
248
|
};
|
|
@@ -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
|
+
};
|