create-byan-agent 2.27.0 → 2.29.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 +61 -0
- package/install/bin/create-byan-agent-v2.js +28 -0
- package/install/lib/native-helper.js +50 -0
- package/install/lib/rtk-integration.js +182 -0
- package/install/package.json +1 -1
- package/install/setup-rtk.js +41 -0
- package/install/templates/.claude/rules/native-workflows.md +25 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +1 -0
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-lint-workflows.js +30 -4
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-okf.js +99 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/okf-bundle.js +118 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/okf-format.js +136 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-lint.js +66 -0
- package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +1 -1
- package/install/templates/_byan/workflow/simple/interview-workflow.md +2 -2
- package/install/templates/_byan/workflow/simple/validate-agent-workflow.md +1 -1
- package/install/templates/dist/skill-bundles/byan-benchmark.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmad-agent-tao.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmad-master.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmb-agent-builder.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmb-module-builder.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmb-workflow-builder.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmm-analyst.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmm-architect.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmm-dev.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmm-pm.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmm-quick-flow-solo-dev.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmm-quinn.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmm-sm.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmm-tech-writer.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-bmm-ux-designer.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-byan-v2.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-byan.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-carmack.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-cis-brainstorming-coach.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-cis-creative-problem-solver.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-cis-design-thinking-coach.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-cis-innovation-strategist.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-cis-presentation-master.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-cis-storyteller.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-claude.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-codex.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-drawio.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-elo-trust.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-fact-check.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-forge.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-insight.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-mantra-audit.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-merise-agile.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-native-dev-story.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-orchestrate.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-patnote.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-project.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-rachid.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-skeptic.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-strict.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-suitability.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-tea-tea.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-yanstaller.zip +0 -0
- package/install/templates/docs/native-workflows-contract.md +8 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [2.29.0] - 2026-06-23
|
|
13
|
+
|
|
14
|
+
### Added - Native opt-in RTK token optimizer (rtk-ai/rtk)
|
|
15
|
+
|
|
16
|
+
- The yanstaller now offers RTK ("Rust Token Killer", Apache-2.0) during install:
|
|
17
|
+
a single zero-dep binary that compresses dev-command output before the LLM
|
|
18
|
+
context (-60/90% tokens) and wires into Claude Code via its own hook.
|
|
19
|
+
- Integration is thin and delegating (`install/lib/rtk-integration.js` +
|
|
20
|
+
`native-helper.js`): the install is handed to rtk's own canonical installer
|
|
21
|
+
(brew / `cargo --tag v0.42.4` / the pinned `install.sh`, which passes
|
|
22
|
+
`RTK_VERSION` so the downloaded binary is pinned too), and the hook wiring to
|
|
23
|
+
rtk's own `rtk init -g`. No bespoke per-OS download/checksum logic.
|
|
24
|
+
- Opt-in and safe: the prompt defaults to NO and discloses the install mechanism
|
|
25
|
+
and the global hook; gated on a TTY + an available installer; opt out with
|
|
26
|
+
`BYAN_SKIP_RTK=1`. Every failure path is a graceful no-op that leaves the BYAN
|
|
27
|
+
install intact. Retry anytime with `npm run setup-rtk`. 25 unit tests.
|
|
28
|
+
|
|
29
|
+
### Added - Open Knowledge Format (OKF v0.1) adoption for the knowledge base
|
|
30
|
+
|
|
31
|
+
- BYAN's knowledge is now interoperable with the Open Knowledge Format
|
|
32
|
+
(GoogleCloudPlatform/knowledge-catalog) — markdown + YAML frontmatter, vendor-
|
|
33
|
+
neutral, zero runtime deps. Only the FORMAT is adopted; the GCP reference agent
|
|
34
|
+
(Python + BigQuery/Gemini) is deliberately left out (not vendored).
|
|
35
|
+
- `lib/okf-format.js` (parse/serialize/validate frontmatter + BYAN type mapping)
|
|
36
|
+
and `lib/okf-bundle.js` (pure, idempotent converter). `byan-okf build` emits a
|
|
37
|
+
normalized OKF bundle to the gitignored `_byan-output/okf-bundle/` (NON-
|
|
38
|
+
destructive — it leaves `_byan/connaissance` untouched); `byan-okf check`
|
|
39
|
+
validates a bundle. 24 unit tests; a real build over the 43 knowledge files
|
|
40
|
+
yields 41 valid OKF entries. The optional GCP enrichment bridge is parked as a
|
|
41
|
+
phase-2 follow-on.
|
|
42
|
+
|
|
43
|
+
## [2.28.0] - 2026-06-23
|
|
44
|
+
|
|
45
|
+
### Added - Advisory model-tiering lint for native workflows
|
|
46
|
+
|
|
47
|
+
- `workflows-lint.js` gains `untieredExplorationViolations`: a NON-blocking
|
|
48
|
+
advisory (surfaced by `byan-lint-workflows.js --advise`, plus a one-line
|
|
49
|
+
summary on every run) that flags an exploration-labelled `agent()` leaf in a
|
|
50
|
+
`.claude/workflows/*.js` script which runs on the session model instead of
|
|
51
|
+
downgrading to `haiku` (a possible token saving). It reuses `classifyLeaf` /
|
|
52
|
+
`isDowngradeModel` from `native-tiers.js` (single source of truth).
|
|
53
|
+
- It is DELIBERATELY out of `validateContract`: the anti-downgrade floor stays a
|
|
54
|
+
hard rule, but forcing `haiku` onto a judgment-bearing leaf (a gate, a
|
|
55
|
+
classification, an exact conversion consumed verbatim downstream) would be a
|
|
56
|
+
STRICT-2 regression, so the per-leaf deep-vs-cheap call stays with the author.
|
|
57
|
+
- There is no per-leaf "effort" knob in the native `agent()` / Agent API (it
|
|
58
|
+
exposes only `model`), so effort-by-complexity reduces to model-by-complexity.
|
|
59
|
+
Documented in `native-workflows.md`, the SKILL DISPATCH section, and
|
|
60
|
+
`docs/native-workflows-contract.md`.
|
|
61
|
+
|
|
62
|
+
### Changed - `.mcp.json` tests realigned to the portable-config design
|
|
63
|
+
|
|
64
|
+
- Five test suites still asserted the pre-portable `.mcp.json` shape
|
|
65
|
+
(`env.BYAN_API_URL` present) and failed after 2.27.0. Realigned them to the
|
|
66
|
+
portable invariant: the `byan` entry is present but carries no `BYAN_API_URL`
|
|
67
|
+
and no token (the server self-resolves its config). Security invariants are
|
|
68
|
+
unchanged: the no-token assertions and `verify`'s raw-token / `/api`-suffix
|
|
69
|
+
drift detection remain. Full suite back to green (2326/2326).
|
|
70
|
+
|
|
71
|
+
## [2.27.0] - 2026-06-19
|
|
72
|
+
|
|
12
73
|
### Fixed - Portable MCP config: the server resolves its own credentials
|
|
13
74
|
|
|
14
75
|
The byan MCP server now resolves its config (`BYAN_API_URL` / `BYAN_API_TOKEN`
|
|
@@ -16,6 +16,7 @@ const { generateProjectAgentsDoc } = require('../lib/project-agents-generator');
|
|
|
16
16
|
const { launchPhase2Chat, generateDefaultConfig } = require('../lib/phase2-chat');
|
|
17
17
|
const { setupByanWebIntegration, validateByanWebReachability } = require('../lib/byan-web-integration');
|
|
18
18
|
const { setupLeantimeIntegration, validateLeantimeReachability } = require('../lib/byan-leantime-integration');
|
|
19
|
+
const { setupRtkIntegration, shouldOfferRtk } = require('../lib/rtk-integration');
|
|
19
20
|
const { setupClaudeNative } = require('../lib/claude-native-setup');
|
|
20
21
|
const { setupCodexNative } = require('../lib/codex-native-setup');
|
|
21
22
|
const { setupMcpExtensions } = require('../lib/mcp-extensions');
|
|
@@ -1369,6 +1370,33 @@ async function install(options = {}) {
|
|
|
1369
1370
|
}
|
|
1370
1371
|
}
|
|
1371
1372
|
|
|
1373
|
+
if (needsClaude && shouldOfferRtk()) {
|
|
1374
|
+
console.log();
|
|
1375
|
+
console.log(chalk.cyan('RTK token optimizer (optional — Rust binary, cuts dev-command tokens 60-90%)'));
|
|
1376
|
+
try {
|
|
1377
|
+
const { proceed } = await inquirer.prompt([
|
|
1378
|
+
{
|
|
1379
|
+
type: 'confirm',
|
|
1380
|
+
name: 'proceed',
|
|
1381
|
+
message: 'Install rtk now? Runs its official installer (brew/cargo, or a pinned curl|sh) and adds a GLOBAL Claude Code hook via `rtk init -g`.',
|
|
1382
|
+
default: false,
|
|
1383
|
+
},
|
|
1384
|
+
]);
|
|
1385
|
+
if (proceed) {
|
|
1386
|
+
const r = setupRtkIntegration({ log: (m) => console.log(chalk.gray(' ' + m)) });
|
|
1387
|
+
if (r.synced) {
|
|
1388
|
+
console.log(chalk.green(` ✓ rtk ready (${r.installedVia}, v${r.version || '?'}) — restart Claude Code to activate`));
|
|
1389
|
+
} else {
|
|
1390
|
+
console.log(chalk.yellow(` ⚠ rtk not wired (${r.reason}) — BYAN unaffected`));
|
|
1391
|
+
}
|
|
1392
|
+
} else {
|
|
1393
|
+
console.log(chalk.gray(' rtk skipped — run `npm run setup-rtk` anytime to enable.'));
|
|
1394
|
+
}
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
console.log(chalk.yellow(` ⚠ rtk setup skipped: ${error.message}`));
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1372
1400
|
if (needsClaude) {
|
|
1373
1401
|
console.log();
|
|
1374
1402
|
console.log(chalk.cyan('Leantime board sync (optional — self-hosted)'));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* native-helper — dependency-light helpers for installing OPTIONAL native
|
|
5
|
+
* components (rtk today; turbo-whisper/parakeet can adopt it later to retire
|
|
6
|
+
* their bespoke per-OS branches, shrinking the maintenance surface).
|
|
7
|
+
*
|
|
8
|
+
* Everything is PURE + injectable (the command runner is a parameter) so callers
|
|
9
|
+
* stay unit-testable without shelling out for real. WHY: the install path runs on
|
|
10
|
+
* every user machine across OSes — it must be exercised by tests, not hope.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* commandExists(cmd) -> boolean. Is an executable resolvable on PATH?
|
|
17
|
+
* Uses POSIX `command -v` (and `where` on Windows). The runner is injectable.
|
|
18
|
+
*/
|
|
19
|
+
function commandExists(cmd, { run = execSync, platform = process.platform } = {}) {
|
|
20
|
+
const probe = platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`;
|
|
21
|
+
try {
|
|
22
|
+
run(probe, { stdio: 'pipe' });
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* detectPlatform() -> 'mac' | 'windows' | 'linux'. Coarse bucket for choosing an
|
|
31
|
+
* install strategy. Keeps the platform string in ONE place.
|
|
32
|
+
*/
|
|
33
|
+
function detectPlatform(platform = process.platform) {
|
|
34
|
+
if (platform === 'darwin') return 'mac';
|
|
35
|
+
if (platform === 'win32') return 'windows';
|
|
36
|
+
return 'linux';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* firstAvailable(candidates) -> the first candidate whose `tool` resolves on
|
|
41
|
+
* PATH, or null. `candidates` is an ordered preference list of
|
|
42
|
+
* { id, tool, ... }. This is the generic "pick the installer the machine has"
|
|
43
|
+
* primitive; the concrete strategy list stays with each component.
|
|
44
|
+
*/
|
|
45
|
+
function firstAvailable(candidates, { has = commandExists } = {}) {
|
|
46
|
+
if (!Array.isArray(candidates)) return null;
|
|
47
|
+
return candidates.find((c) => c && c.tool && has(c.tool)) || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { commandExists, detectPlatform, firstAvailable };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* rtk-integration — optional NATIVE token-optimizer integration (rtk-ai/rtk).
|
|
5
|
+
*
|
|
6
|
+
* RTK ("Rust Token Killer", Apache-2.0) is a single zero-dependency binary that
|
|
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.
|
|
9
|
+
*
|
|
10
|
+
* MAINTAINABILITY by design (the whole point of choosing this shape):
|
|
11
|
+
* - We do NOT reimplement per-OS download / checksum / version pinning. We
|
|
12
|
+
* DELEGATE the install to rtk's own canonical installer (brew / cargo / the
|
|
13
|
+
* official install.sh), and DELEGATE the Claude Code hook wiring to rtk's own
|
|
14
|
+
* `rtk init -g`. BYAN maintains only "pick the available installer, run it,
|
|
15
|
+
* verify, ask rtk to wire its hook" — a tiny surface, bumped via one constant.
|
|
16
|
+
* - SUPPLY-CHAIN: we pin to a TAG, never a moving branch. cargo builds the
|
|
17
|
+
* tagged source (`--tag`), and install.sh is fetched from the IMMUTABLE tag ref
|
|
18
|
+
* (and that script itself checksum-verifies the binary it downloads). So the
|
|
19
|
+
* fetched artifacts are reproducible, not "whatever master is today".
|
|
20
|
+
* - Everything is graceful: a missing installer or a failed step is a no-op that
|
|
21
|
+
* NEVER breaks the BYAN install (returns { ok:true, synced:false, reason }).
|
|
22
|
+
* - The command runner + the PATH probe are injectable, so the whole flow is
|
|
23
|
+
* unit-tested without shelling out (see install/__tests__/rtk-integration.test.js).
|
|
24
|
+
*
|
|
25
|
+
* Mirrors the ENTRY-POINT shape of byan-web-integration.js / byan-leantime-
|
|
26
|
+
* integration.js (one `setup*Integration`); the return contract is { ok, synced,
|
|
27
|
+
* reason, ... } because persistence is owned by rtk's own `rtk init -g`, not by
|
|
28
|
+
* byan-platform-config.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const { execSync } = require('child_process');
|
|
32
|
+
const { commandExists, firstAvailable } = require('./native-helper');
|
|
33
|
+
|
|
34
|
+
// Pinned install target. Bumping rtk = change these two constants only. We pin a
|
|
35
|
+
// TAG so the install is reproducible and supply-chain-bounded (see header).
|
|
36
|
+
const RTK_VERSION = '0.42.4';
|
|
37
|
+
const RTK_TAG = `v${RTK_VERSION}`;
|
|
38
|
+
const RTK_REPO = 'https://github.com/rtk-ai/rtk';
|
|
39
|
+
const RTK_INSTALL_SH = `https://raw.githubusercontent.com/rtk-ai/rtk/refs/tags/${RTK_TAG}/install.sh`;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ordered install strategies — each DELEGATES to rtk's own canonical installer,
|
|
43
|
+
* pinned to RTK_TAG where the mechanism allows. Preference: brew (cleanest
|
|
44
|
+
* upgrades, vetted formula) > cargo (any Rust dev, pinned --tag) > the official
|
|
45
|
+
* install.sh at the immutable tag ref (universal fallback, self-checksumming).
|
|
46
|
+
*/
|
|
47
|
+
function installStrategies() {
|
|
48
|
+
return [
|
|
49
|
+
{ id: 'brew', tool: 'brew', cmd: 'brew install rtk' },
|
|
50
|
+
{ id: 'cargo', tool: 'cargo', cmd: `cargo install --git ${RTK_REPO} --tag ${RTK_TAG}` },
|
|
51
|
+
// Pass RTK_VERSION into the script so it pins the BINARY too (the script
|
|
52
|
+
// builds releases/download/${VERSION}/...; without it, it resolves
|
|
53
|
+
// releases/latest). Pinning the URL alone is not enough — this pins both.
|
|
54
|
+
{ id: 'script', tool: 'curl', cmd: `curl -fsSL ${RTK_INSTALL_SH} | RTK_VERSION=${RTK_TAG} sh` },
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function oneLine(err) {
|
|
59
|
+
return String((err && err.message) || err).split('\n')[0];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* rtkStatus() -> { installed, version }. Never throws. Parses `rtk --version`
|
|
64
|
+
* ("rtk 0.42.4"). An installed-but-unparseable build yields { installed:true,
|
|
65
|
+
* version:null } (a real runtime possibility, handled downstream).
|
|
66
|
+
*/
|
|
67
|
+
function rtkStatus({ run = execSync } = {}) {
|
|
68
|
+
try {
|
|
69
|
+
const out = String(run('rtk --version', { stdio: 'pipe' }) || '');
|
|
70
|
+
const m = out.match(/rtk\s+v?([0-9]+\.[0-9]+\.[0-9]+)/i);
|
|
71
|
+
return { installed: true, version: m ? m[1] : null };
|
|
72
|
+
} catch {
|
|
73
|
+
return { installed: false, version: null };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* pickStrategy() -> the first install strategy whose tool is on PATH, or null.
|
|
79
|
+
*/
|
|
80
|
+
function pickStrategy({ has = commandExists } = {}) {
|
|
81
|
+
return firstAvailable(installStrategies(), { has });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* doctor() -> a status report for diagnostics / a `byan` doctor surface. `pinned`
|
|
86
|
+
* is the version BYAN installs/targets — NOT an enforced floor on a pre-existing
|
|
87
|
+
* rtk (we do not break a user's own older rtk).
|
|
88
|
+
*/
|
|
89
|
+
function doctor({ run = execSync } = {}) {
|
|
90
|
+
const s = rtkStatus({ run });
|
|
91
|
+
return {
|
|
92
|
+
component: 'rtk',
|
|
93
|
+
installed: s.installed,
|
|
94
|
+
version: s.version,
|
|
95
|
+
pinned: RTK_VERSION,
|
|
96
|
+
repo: RTK_REPO,
|
|
97
|
+
hookCommand: 'rtk init -g',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Delegate the Claude Code hook wiring to rtk's OWN command (idempotent). This is
|
|
102
|
+
// the maintainability win: no manual settings.json merge to keep in sync.
|
|
103
|
+
function wireHook({ run, log, installedVia, version }) {
|
|
104
|
+
try {
|
|
105
|
+
run('rtk init -g', { stdio: 'pipe' });
|
|
106
|
+
log(`rtk: ready (${installedVia}, v${version || '?'}) — hook wired via 'rtk init -g'. Restart Claude Code to activate.`);
|
|
107
|
+
return { ok: true, synced: true, reason: 'wired', installed: true, installedVia, version, hook: true };
|
|
108
|
+
} catch (err) {
|
|
109
|
+
log(`rtk: installed (${installedVia}) but 'rtk init -g' failed (${oneLine(err)}) — run it manually, BYAN unaffected.`);
|
|
110
|
+
return { ok: true, synced: false, reason: 'hook-failed', installed: true, installedVia, version, hook: false };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* setupRtkIntegration(options) -> { ok, synced, reason, ... }. The single entry.
|
|
116
|
+
*
|
|
117
|
+
* Flow: if rtk is already present, just (re)wire the hook (idempotent). Otherwise
|
|
118
|
+
* pick an available installer, delegate the install to rtk, verify, then wire the
|
|
119
|
+
* hook. Any missing tool / failed step degrades to a logged no-op — it NEVER
|
|
120
|
+
* throws and NEVER fails the surrounding BYAN install. `ok` is therefore always
|
|
121
|
+
* true; `synced` says whether rtk ended up wired.
|
|
122
|
+
*
|
|
123
|
+
* @param {object} [o]
|
|
124
|
+
* @param {Function} [o.run] command runner (default execSync) — injected in tests
|
|
125
|
+
* @param {Function} [o.has] PATH probe (default commandExists) — injected in tests
|
|
126
|
+
* @param {Function} [o.log] one-line breadcrumb sink (default no-op)
|
|
127
|
+
*/
|
|
128
|
+
function setupRtkIntegration({ run = execSync, has = commandExists, log = () => {} } = {}) {
|
|
129
|
+
const before = rtkStatus({ run });
|
|
130
|
+
if (before.installed) {
|
|
131
|
+
return wireHook({ run, log, installedVia: 'already-present', version: before.version });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const strat = pickStrategy({ has });
|
|
135
|
+
if (!strat) {
|
|
136
|
+
log('rtk: no installer found (brew / cargo / curl) — skipped. BYAN install unaffected.');
|
|
137
|
+
return { ok: true, synced: false, reason: 'no-installer', installed: false, hook: false };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Breadcrumb BEFORE the (silent, possibly slow) delegated install so the user
|
|
141
|
+
// is not left staring at a frozen prompt while rtk's installer runs.
|
|
142
|
+
log(`rtk: installing via ${strat.id} (pinned ${RTK_TAG}; this can take a moment)...`);
|
|
143
|
+
try {
|
|
144
|
+
run(strat.cmd, { stdio: 'pipe' });
|
|
145
|
+
} catch (err) {
|
|
146
|
+
log(`rtk: install via ${strat.id} failed (${oneLine(err)}) — skipped gracefully.`);
|
|
147
|
+
return { ok: true, synced: false, reason: `install-failed:${strat.id}`, installed: false, hook: false };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const after = rtkStatus({ run });
|
|
151
|
+
if (!after.installed) {
|
|
152
|
+
log(`rtk: install via ${strat.id} ran but 'rtk --version' did not confirm — skipped.`);
|
|
153
|
+
return { ok: true, synced: false, reason: 'install-unverified', installed: false, hook: false };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return wireHook({ run, log, installedVia: strat.id, version: after.version });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* shouldOfferRtk() -> boolean. The installer asks this BEFORE prompting the user:
|
|
161
|
+
* offer rtk only when (a) interactive (a TTY), (b) not opted out
|
|
162
|
+
* (BYAN_SKIP_RTK=1), and (c) an installer is actually on PATH — so we never prompt
|
|
163
|
+
* for something we cannot deliver, and never block a non-interactive/CI install.
|
|
164
|
+
*/
|
|
165
|
+
function shouldOfferRtk({ env = process.env, isTTY = !!(process.stdin && process.stdin.isTTY), has = commandExists } = {}) {
|
|
166
|
+
if (env && env.BYAN_SKIP_RTK === '1') return false;
|
|
167
|
+
if (!isTTY) return false;
|
|
168
|
+
return Boolean(pickStrategy({ has }));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
setupRtkIntegration,
|
|
173
|
+
rtkStatus,
|
|
174
|
+
pickStrategy,
|
|
175
|
+
installStrategies,
|
|
176
|
+
doctor,
|
|
177
|
+
shouldOfferRtk,
|
|
178
|
+
RTK_VERSION,
|
|
179
|
+
RTK_TAG,
|
|
180
|
+
RTK_INSTALL_SH,
|
|
181
|
+
RTK_REPO,
|
|
182
|
+
};
|
package/install/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* setup-rtk — explicit opt-in installer for the RTK token optimizer (rtk-ai/rtk).
|
|
6
|
+
*
|
|
7
|
+
* Run on demand: `npm run setup-rtk` (or `node install/setup-rtk.js`). It delegates
|
|
8
|
+
* the install to rtk's own canonical installer (brew / cargo / official script) and
|
|
9
|
+
* the Claude Code hook wiring to rtk's own `rtk init -g`. It NEVER throws: a missing
|
|
10
|
+
* installer or a failed step is reported and exits 0 (RTK is optional; BYAN works
|
|
11
|
+
* without it). The real logic lives in lib/rtk-integration.js and is unit-tested.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const chalk = require('chalk');
|
|
15
|
+
const { setupRtkIntegration, doctor } = require('./lib/rtk-integration');
|
|
16
|
+
|
|
17
|
+
function main() {
|
|
18
|
+
console.log(chalk.cyan('\nRTK token optimizer (rtk-ai/rtk) — optional native component'));
|
|
19
|
+
console.log(chalk.gray(' Cuts dev-command tokens 60-90%; wires its own Claude Code hook.\n'));
|
|
20
|
+
|
|
21
|
+
const result = setupRtkIntegration({ log: (m) => console.log(chalk.gray(' ' + m)) });
|
|
22
|
+
|
|
23
|
+
if (result.synced) {
|
|
24
|
+
console.log(chalk.green(`\n rtk ready (${result.installedVia}, v${result.version || '?'}). Restart Claude Code to activate.`));
|
|
25
|
+
} else if (result.reason === 'no-installer') {
|
|
26
|
+
console.log(chalk.yellow('\n No installer found (brew / cargo / curl). Install one, then re-run `npm run setup-rtk`.'));
|
|
27
|
+
} else {
|
|
28
|
+
console.log(chalk.yellow(`\n rtk not wired (${result.reason}). BYAN is unaffected; you can retry later.`));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const d = doctor();
|
|
32
|
+
console.log(chalk.gray(` doctor: installed=${d.installed} version=${d.version || '-'} (pinned ${d.pinned})`));
|
|
33
|
+
// Optional component -> always a clean exit; never fail the surrounding flow.
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (require.main === module) {
|
|
38
|
+
main();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { main };
|
|
@@ -22,6 +22,31 @@ A native workflow script mutates FD/strict state only through the `byan_fd_*` /
|
|
|
22
22
|
forbidden import/require in `.claude/workflows/*.js`.
|
|
23
23
|
- `.githooks/pre-commit` runs that linter, so a violation blocks the commit.
|
|
24
24
|
|
|
25
|
+
## Model tiering — floor enforced, ceiling advisory
|
|
26
|
+
|
|
27
|
+
Each `agent()` leaf runs on the session model unless the call sets `opts.model`.
|
|
28
|
+
The tiering decision lives in one place — `_byan/mcp/byan-mcp-server/lib/native-tiers.js`
|
|
29
|
+
(tier vocabulary, leaf classifier, model map). The doctrine is binary: a pure
|
|
30
|
+
EXPLORATION leaf (read/load/parse/detect) may downgrade to `model: 'haiku'`;
|
|
31
|
+
implementation / verification / analysis leaves OMIT `opts.model` and inherit the
|
|
32
|
+
session model (no pin-up to opus).
|
|
33
|
+
|
|
34
|
+
The linter splits the two directions:
|
|
35
|
+
|
|
36
|
+
- **Floor (HARD, blocks the commit)** — a downgrade model on a PROTECTED leaf, or
|
|
37
|
+
a pin-up, is a contract violation (`modelRoutingViolations`). This is the
|
|
38
|
+
STRICT-2 No Downgrade net.
|
|
39
|
+
- **Ceiling (ADVISORY, non-blocking)** — an exploration-labelled leaf that runs
|
|
40
|
+
deep is *reported* (`byan-lint-workflows.js --advise`), not forced. `classifyLeaf`
|
|
41
|
+
is permissive: many exploration-labelled leaves legitimately stay deep because
|
|
42
|
+
they bear a HALT/prerequisite gate, a classification, or an exact conversion
|
|
43
|
+
consumed verbatim downstream. The human owns that per-leaf call. Forcing haiku
|
|
44
|
+
on them would be the very downgrade the floor forbids.
|
|
45
|
+
|
|
46
|
+
A per-leaf "effort" knob is not available: the native `agent()` / Agent API exposes
|
|
47
|
+
only `model`, so model tier is the sole token lever. Effort-by-complexity reduces
|
|
48
|
+
to model-by-complexity.
|
|
49
|
+
|
|
25
50
|
## Registry + resolution
|
|
26
51
|
|
|
27
52
|
- Registry: `.claude/workflows/INDEX.md`, generated by
|
|
@@ -66,6 +66,7 @@ Never call `byan_update_apply` without explicit user consent. That tool returns
|
|
|
66
66
|
- nature `exploration` (load/read/scan/list/parse/fetch...) → `haiku`
|
|
67
67
|
- nature `implementation` / `verification` / `analysis` / unknown → deep = **inherit the session model**
|
|
68
68
|
- Keep protected work (verify/analysis/implement) off haiku regardless of size ; no pin-up to opus. Pass an explicit `nature` to `byan_dispatch` when you know it.
|
|
69
|
+
- **Inside native workflow scripts** (`.claude/workflows/*.js`) the SAME tiering applies per `agent()` leaf via `opts.model`, enforced by the workflow linter as a FLOOR not a ceiling : `modelRoutingViolations` HARD-blocks a downgrade on a protected leaf (or a pin-up) ; an exploration-labelled leaf left deep is a NON-blocking ADVISORY (`byan-lint-workflows.js --advise`), since many such leaves bear a gate/classification/exact-conversion and must stay deep — the human owns that call. No per-leaf effort knob exists (the API exposes only `model`), so effort-by-complexity reduces to model-by-complexity.
|
|
69
70
|
- **Output** : a table `{ feature → specialist → model → strategy → estimated_tokens }`.
|
|
70
71
|
- **If no specialist matches** : halt. Ask user whether to run INT (agent recruitment) first. Do NOT fallback silently to general-purpose.
|
|
71
72
|
- **Exit gate** : user validates the mapping.
|
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import { validateContract } from '../lib/workflows-lint.js';
|
|
5
|
+
import { validateContract, untieredExplorationViolations } from '../lib/workflows-lint.js';
|
|
6
6
|
|
|
7
7
|
// Validate native workflow scripts under .claude/workflows/ against the full
|
|
8
8
|
// contract (state-coupling + clock/RNG + meta-literal) AND node --check syntax.
|
|
9
|
-
// Exits non-zero on any violation (used by the pre-commit gate).
|
|
10
|
-
//
|
|
9
|
+
// Exits non-zero on any contract violation (used by the pre-commit gate).
|
|
10
|
+
//
|
|
11
|
+
// Tiering is reported SEPARATELY and NON-BLOCKING: exploration-labelled leaves
|
|
12
|
+
// that run deep are a possible token saving, but many legitimately stay deep
|
|
13
|
+
// (they bear a gate/classification/exact-conversion). So they are an ADVISORY,
|
|
14
|
+
// never a hard failure. A one-line summary always prints; --advise lists each one.
|
|
15
|
+
// Usage: node bin/byan-lint-workflows.js [--root <dir>] [--advise]
|
|
11
16
|
|
|
12
17
|
function parseArgs(argv) {
|
|
13
18
|
const args = {};
|
|
14
19
|
for (let i = 2; i < argv.length; i++) {
|
|
15
20
|
if (argv[i] === '--root') args.projectRoot = argv[++i];
|
|
21
|
+
else if (argv[i] === '--advise') args.advise = true;
|
|
16
22
|
}
|
|
17
23
|
return args;
|
|
18
24
|
}
|
|
@@ -32,8 +38,10 @@ try {
|
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
let failed = 0;
|
|
41
|
+
const advisories = [];
|
|
35
42
|
for (const file of files) {
|
|
36
|
-
const
|
|
43
|
+
const src = fs.readFileSync(file, 'utf8');
|
|
44
|
+
const violations = validateContract(src);
|
|
37
45
|
|
|
38
46
|
// Syntax gate: a native script must parse.
|
|
39
47
|
try {
|
|
@@ -48,6 +56,24 @@ for (const file of files) {
|
|
|
48
56
|
process.stderr.write(`[byan-lint-workflows] ${file}: ${v.id} - ${v.msg}\n`);
|
|
49
57
|
}
|
|
50
58
|
}
|
|
59
|
+
|
|
60
|
+
// Non-blocking tiering advisory: exploration leaves that run deep.
|
|
61
|
+
for (const a of untieredExplorationViolations(src)) {
|
|
62
|
+
advisories.push({ file, ...a });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Tiering advisory is informational only — it never changes the exit code.
|
|
67
|
+
if (advisories.length) {
|
|
68
|
+
if (args.advise) {
|
|
69
|
+
for (const a of advisories) {
|
|
70
|
+
process.stdout.write(`[byan-lint-workflows] ADVISORY ${a.file}: ${a.id} - ${a.msg}\n`);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
process.stdout.write(
|
|
74
|
+
`[byan-lint-workflows] advisory: ${advisories.length} exploration leaf(s) run deep (rerun with --advise to list; keep deep if they bear a gate/judgment, else add model: 'haiku')\n`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
51
77
|
}
|
|
52
78
|
|
|
53
79
|
if (failed === 0) {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// byan-okf — produce / validate an Open Knowledge Format bundle from BYAN's
|
|
3
|
+
// knowledge base (_byan/connaissance).
|
|
4
|
+
//
|
|
5
|
+
// byan-okf build [--root <dir>] [--out <dir>] build a bundle (non-destructive)
|
|
6
|
+
// byan-okf check [<dir>] [--root <dir>] validate every .md is OKF
|
|
7
|
+
//
|
|
8
|
+
// `build` is NON-DESTRUCTIVE: it reads _byan/connaissance and writes a normalized
|
|
9
|
+
// OKF bundle (frontmatter + index.md + log.md) to the OUT dir (default
|
|
10
|
+
// _byan-output/okf-bundle/, which is gitignored). It never mutates the source.
|
|
11
|
+
// The conversion logic is in lib/okf-bundle.js (pure, unit-tested).
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { buildBundle } from '../lib/okf-bundle.js';
|
|
16
|
+
import { parseFrontmatter, validateOkf, OKF_RESERVED } from '../lib/okf-format.js';
|
|
17
|
+
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const a = { _: [] };
|
|
20
|
+
for (let i = 2; i < argv.length; i++) {
|
|
21
|
+
const x = argv[i];
|
|
22
|
+
if (x === '--root') a.root = argv[++i];
|
|
23
|
+
else if (x === '--out') a.out = argv[++i];
|
|
24
|
+
else a._.push(x);
|
|
25
|
+
}
|
|
26
|
+
return a;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function walkMd(dir, base = dir, acc = []) {
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
33
|
+
} catch {
|
|
34
|
+
return acc;
|
|
35
|
+
}
|
|
36
|
+
for (const e of entries) {
|
|
37
|
+
const full = path.join(dir, e.name);
|
|
38
|
+
if (e.isDirectory()) walkMd(full, base, acc);
|
|
39
|
+
else if (e.isFile() && e.name.endsWith('.md')) acc.push({ relPath: path.relative(base, full), text: fs.readFileSync(full, 'utf8') });
|
|
40
|
+
}
|
|
41
|
+
return acc;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function cmdBuild(args) {
|
|
45
|
+
const root = args.root || process.cwd();
|
|
46
|
+
const src = path.join(root, '_byan', 'connaissance');
|
|
47
|
+
const out = args.out || path.join(root, '_byan-output', 'okf-bundle');
|
|
48
|
+
const files = walkMd(src);
|
|
49
|
+
if (!files.length) {
|
|
50
|
+
process.stdout.write(`[byan-okf] no markdown found under ${src} — nothing to build\n`);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
const timestamp = new Date().toISOString();
|
|
54
|
+
const { entries, index, log, errors } = buildBundle(files, { timestamp });
|
|
55
|
+
fs.mkdirSync(out, { recursive: true });
|
|
56
|
+
for (const e of entries) {
|
|
57
|
+
const dest = path.join(out, e.path);
|
|
58
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
59
|
+
fs.writeFileSync(dest, e.serialized);
|
|
60
|
+
}
|
|
61
|
+
fs.writeFileSync(path.join(out, 'index.md'), index);
|
|
62
|
+
fs.writeFileSync(path.join(out, 'log.md'), log);
|
|
63
|
+
const warned = entries.filter((e) => e.validation.warnings.length).length;
|
|
64
|
+
process.stdout.write(`[byan-okf] wrote ${entries.length} OKF entries + index.md + log.md to ${out} (${warned} with warnings, ${errors.length} invalid)\n`);
|
|
65
|
+
return errors.length ? 1 : 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cmdCheck(args) {
|
|
69
|
+
const root = args.root || process.cwd();
|
|
70
|
+
const dir = args._[1] || path.join(root, '_byan-output', 'okf-bundle');
|
|
71
|
+
const files = walkMd(dir);
|
|
72
|
+
let bad = 0;
|
|
73
|
+
for (const f of files) {
|
|
74
|
+
if (OKF_RESERVED.includes(path.basename(f.relPath))) continue;
|
|
75
|
+
const { data } = parseFrontmatter(f.text);
|
|
76
|
+
const v = validateOkf(data);
|
|
77
|
+
if (!v.ok) {
|
|
78
|
+
bad += 1;
|
|
79
|
+
process.stderr.write(`[byan-okf] INVALID ${f.relPath}: ${v.errors.join('; ')}\n`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (bad) {
|
|
83
|
+
process.stderr.write(`[byan-okf] ${bad} invalid OKF file(s) under ${dir}\n`);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
process.stdout.write(`[byan-okf] OK - ${files.length} markdown file(s) are valid OKF under ${dir}\n`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const args = parseArgs(process.argv);
|
|
91
|
+
const cmd = args._[0] || 'build';
|
|
92
|
+
let code = 0;
|
|
93
|
+
if (cmd === 'build') code = cmdBuild(args);
|
|
94
|
+
else if (cmd === 'check') code = cmdCheck(args);
|
|
95
|
+
else {
|
|
96
|
+
process.stderr.write(`[byan-okf] unknown command '${cmd}' (use: build | check)\n`);
|
|
97
|
+
code = 2;
|
|
98
|
+
}
|
|
99
|
+
process.exit(code);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// okf-bundle — pure converter that turns BYAN knowledge files into an Open
|
|
2
|
+
// Knowledge Format bundle (normalized frontmatter + index.md + log.md).
|
|
3
|
+
//
|
|
4
|
+
// PURE (no fs): the bin (byan-okf.js) does the I/O and hands content in/out.
|
|
5
|
+
// buildEntry is a FIXPOINT on its own output — feeding a normalized file back in
|
|
6
|
+
// keeps its fields verbatim (no duplication, no churn). Note: the FIRST build of a
|
|
7
|
+
// frontmatter-less source stamps a fresh timestamp, but once a file carries one,
|
|
8
|
+
// rebuilds preserve it. Unknown/extension keys are always preserved (OKF spec).
|
|
9
|
+
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import {
|
|
12
|
+
parseFrontmatter,
|
|
13
|
+
serializeFrontmatter,
|
|
14
|
+
validateOkf,
|
|
15
|
+
byanTypeFor,
|
|
16
|
+
deriveTitle,
|
|
17
|
+
deriveDescription,
|
|
18
|
+
OKF_VERSION,
|
|
19
|
+
OKF_RESERVED,
|
|
20
|
+
} from './okf-format.js';
|
|
21
|
+
|
|
22
|
+
const PREFERRED_ORDER = ['type', 'title', 'description', 'resource', 'tags', 'timestamp'];
|
|
23
|
+
|
|
24
|
+
function basenameTitle(relPath) {
|
|
25
|
+
return path.basename(String(relPath || '')).replace(/\.md$/, '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* buildEntry(relPath, text, { timestamp }) -> { path, data, body, serialized,
|
|
30
|
+
* validation }. Fills the OKF frontmatter from existing values FIRST (idempotent),
|
|
31
|
+
* deriving type/title/description only when absent. Existing + unknown keys are
|
|
32
|
+
* preserved; keys are emitted in a stable, readable order.
|
|
33
|
+
*/
|
|
34
|
+
export function buildEntry(relPath, text, { timestamp } = {}) {
|
|
35
|
+
const { data: existing, body } = parseFrontmatter(text);
|
|
36
|
+
const data = { ...existing };
|
|
37
|
+
|
|
38
|
+
if (!data.type) data.type = byanTypeFor(relPath);
|
|
39
|
+
if (!data.title) data.title = deriveTitle(body) || basenameTitle(relPath);
|
|
40
|
+
if (data.description === undefined) {
|
|
41
|
+
const d = deriveDescription(body);
|
|
42
|
+
if (d) data.description = d;
|
|
43
|
+
}
|
|
44
|
+
if (!data.timestamp && timestamp) data.timestamp = timestamp;
|
|
45
|
+
// A YAML-coerced Date (unquoted source timestamp) -> canonical ISO string, so
|
|
46
|
+
// the emitted bundle always carries a string timestamp.
|
|
47
|
+
if (data.timestamp instanceof Date) data.timestamp = data.timestamp.toISOString();
|
|
48
|
+
|
|
49
|
+
const ordered = {};
|
|
50
|
+
for (const k of PREFERRED_ORDER) if (data[k] !== undefined) ordered[k] = data[k];
|
|
51
|
+
for (const k of Object.keys(data)) if (ordered[k] === undefined) ordered[k] = data[k];
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
path: relPath,
|
|
55
|
+
data: ordered,
|
|
56
|
+
body,
|
|
57
|
+
serialized: serializeFrontmatter(ordered, body),
|
|
58
|
+
validation: validateOkf(ordered),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* buildIndex(entries) -> the bundle-root index.md content. Carries the
|
|
64
|
+
* `okf_version` frontmatter (the one place the spec allows it) and groups entries
|
|
65
|
+
* by type with relative links — the progressive-disclosure listing.
|
|
66
|
+
*/
|
|
67
|
+
export function buildIndex(entries) {
|
|
68
|
+
const byType = new Map();
|
|
69
|
+
for (const e of entries) {
|
|
70
|
+
const t = (e.data && e.data.type) || 'Knowledge';
|
|
71
|
+
if (!byType.has(t)) byType.set(t, []);
|
|
72
|
+
byType.get(t).push(e);
|
|
73
|
+
}
|
|
74
|
+
const lines = [
|
|
75
|
+
'# BYAN Knowledge',
|
|
76
|
+
'',
|
|
77
|
+
'Open Knowledge Format bundle generated from `_byan/connaissance`.',
|
|
78
|
+
'',
|
|
79
|
+
];
|
|
80
|
+
for (const t of [...byType.keys()].sort()) {
|
|
81
|
+
lines.push(`## ${t}`, '');
|
|
82
|
+
for (const e of byType.get(t).slice().sort((a, b) => a.path.localeCompare(b.path))) {
|
|
83
|
+
const title = (e.data && e.data.title) || e.path;
|
|
84
|
+
const desc = e.data && e.data.description ? ` — ${e.data.description}` : '';
|
|
85
|
+
lines.push(`- [${title}](${e.path})${desc}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push('');
|
|
88
|
+
}
|
|
89
|
+
return serializeFrontmatter({ okf_version: OKF_VERSION }, lines.join('\n'));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* buildLog(entries, timestamp) -> log.md content. Reserved file, no frontmatter,
|
|
94
|
+
* plain update history.
|
|
95
|
+
*/
|
|
96
|
+
export function buildLog(entries, timestamp) {
|
|
97
|
+
const stamp = timestamp || '(unstamped)';
|
|
98
|
+
return `# Update Log\n\n## ${stamp}\n\nGenerated OKF bundle from _byan/connaissance: ${entries.length} entries.\n`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* buildBundle(files, { timestamp }) -> { entries, index, log, errors }. `files`
|
|
103
|
+
* is [{ relPath, text }]. Reserved names (index.md/log.md) and non-markdown are
|
|
104
|
+
* skipped. `errors` lists entries that fail OKF validation (should be none, since
|
|
105
|
+
* we always set `type`).
|
|
106
|
+
*/
|
|
107
|
+
export function buildBundle(files, { timestamp } = {}) {
|
|
108
|
+
const entries = (files || [])
|
|
109
|
+
.filter((f) => f && typeof f.relPath === 'string' && /\.md$/.test(f.relPath))
|
|
110
|
+
.filter((f) => !OKF_RESERVED.includes(path.basename(f.relPath)))
|
|
111
|
+
.map((f) => buildEntry(f.relPath, f.text, { timestamp }));
|
|
112
|
+
const index = buildIndex(entries);
|
|
113
|
+
const log = buildLog(entries, timestamp);
|
|
114
|
+
const errors = entries
|
|
115
|
+
.filter((e) => !e.validation.ok)
|
|
116
|
+
.map((e) => ({ path: e.path, errors: e.validation.errors }));
|
|
117
|
+
return { entries, index, log, errors };
|
|
118
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// okf-format — Open Knowledge Format v0.1 primitives (parse / serialize /
|
|
2
|
+
// validate) + the BYAN knowledge type mapping.
|
|
3
|
+
//
|
|
4
|
+
// OKF (GoogleCloudPlatform/knowledge-catalog) represents knowledge as plain
|
|
5
|
+
// markdown + YAML frontmatter, vendor-neutral and dependency-free for consumers.
|
|
6
|
+
// We ADOPT the FORMAT for _byan/connaissance (the GCP reference AGENT, which needs
|
|
7
|
+
// BigQuery/Gemini, is deliberately NOT vendored — see docs). This module is the
|
|
8
|
+
// single source of truth for the frontmatter contract; the converter
|
|
9
|
+
// (okf-bundle.js) builds on it.
|
|
10
|
+
//
|
|
11
|
+
// Spec v0.1: required `type` (free string); recommended `title`, `description`,
|
|
12
|
+
// `resource`, `tags` (list), `timestamp` (ISO-8601); unknown keys MUST be
|
|
13
|
+
// preserved. Reserved filenames: index.md (listing, okf_version at bundle root)
|
|
14
|
+
// and log.md (history).
|
|
15
|
+
|
|
16
|
+
import yaml from 'js-yaml';
|
|
17
|
+
|
|
18
|
+
export const OKF_VERSION = '0.1';
|
|
19
|
+
export const OKF_RESERVED = Object.freeze(['index.md', 'log.md']);
|
|
20
|
+
|
|
21
|
+
const FM_RE = /^?---\n([\s\S]*?)\n---\n?/;
|
|
22
|
+
const ISO_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* parseFrontmatter(text) -> { data, body }. A leading `---\n...\n---` block is
|
|
26
|
+
* YAML-parsed into `data`; the rest is `body`. No block -> { data:{}, body:text }.
|
|
27
|
+
* Malformed YAML degrades to {} rather than throwing (a knowledge file is never
|
|
28
|
+
* worth crashing the tool over).
|
|
29
|
+
*/
|
|
30
|
+
export function parseFrontmatter(text) {
|
|
31
|
+
const s = String(text == null ? '' : text);
|
|
32
|
+
const m = s.match(FM_RE);
|
|
33
|
+
if (!m) return { data: {}, body: s.replace(/^/, '') };
|
|
34
|
+
let data;
|
|
35
|
+
try {
|
|
36
|
+
data = yaml.load(m[1]);
|
|
37
|
+
} catch {
|
|
38
|
+
data = {};
|
|
39
|
+
}
|
|
40
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
41
|
+
// Strip the blank line(s) the convention puts between `---` and the body, so
|
|
42
|
+
// body starts at real content and round-trips cleanly with serializeFrontmatter.
|
|
43
|
+
return { data, body: s.slice(m[0].length).replace(/^\n+/, '') };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* serializeFrontmatter(data, body) -> a markdown string with a clean YAML
|
|
48
|
+
* frontmatter block followed by the body. Round-trips with parseFrontmatter.
|
|
49
|
+
*/
|
|
50
|
+
export function serializeFrontmatter(data, body) {
|
|
51
|
+
const fm = yaml.dump(data || {}, { lineWidth: -1, noRefs: true, sortKeys: false }).trimEnd();
|
|
52
|
+
const b = String(body == null ? '' : body).replace(/^\n+/, '');
|
|
53
|
+
return `---\n${fm}\n---\n\n${b}`.replace(/\n*$/, '\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* isIsoTimestamp(v) -> true for an ISO-8601 date/datetime string OR a Date.
|
|
58
|
+
* js-yaml coerces an UNQUOTED YAML timestamp (`timestamp: 2026-06-23T10:00:00Z`)
|
|
59
|
+
* into a JS Date, so a valid hand-authored OKF file must not be rejected for it.
|
|
60
|
+
*/
|
|
61
|
+
export function isIsoTimestamp(v) {
|
|
62
|
+
if (v instanceof Date) return !Number.isNaN(v.getTime());
|
|
63
|
+
return typeof v === 'string' && ISO_RE.test(v);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* validateOkf(data) -> { ok, errors, warnings }. Enforces the v0.1 contract:
|
|
68
|
+
* `type` is required; title/description must be strings if present; tags must be
|
|
69
|
+
* a string list; timestamp must be ISO-8601. Missing recommended fields are
|
|
70
|
+
* WARNINGS, not errors (the spec only hard-requires `type`).
|
|
71
|
+
*/
|
|
72
|
+
export function validateOkf(data) {
|
|
73
|
+
const errors = [];
|
|
74
|
+
const warnings = [];
|
|
75
|
+
const d = data && typeof data === 'object' ? data : {};
|
|
76
|
+
|
|
77
|
+
if (typeof d.type !== 'string' || !d.type.trim()) {
|
|
78
|
+
errors.push('missing required field: type (non-empty string)');
|
|
79
|
+
}
|
|
80
|
+
for (const k of ['title', 'description', 'resource']) {
|
|
81
|
+
if (d[k] !== undefined && typeof d[k] !== 'string') errors.push(`${k} must be a string`);
|
|
82
|
+
}
|
|
83
|
+
if (d.tags !== undefined && (!Array.isArray(d.tags) || d.tags.some((t) => typeof t !== 'string'))) {
|
|
84
|
+
errors.push('tags must be a list of strings');
|
|
85
|
+
}
|
|
86
|
+
if (d.timestamp !== undefined && !isIsoTimestamp(d.timestamp)) {
|
|
87
|
+
errors.push('timestamp must be ISO-8601');
|
|
88
|
+
}
|
|
89
|
+
for (const k of ['title', 'description', 'timestamp']) {
|
|
90
|
+
if (d[k] === undefined) warnings.push(`recommended field absent: ${k}`);
|
|
91
|
+
}
|
|
92
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* byanTypeFor(relPath) -> a sensible OKF `type` for a _byan/connaissance file.
|
|
97
|
+
* Types are free strings in OKF; this keeps BYAN's vocabulary consistent.
|
|
98
|
+
*/
|
|
99
|
+
export function byanTypeFor(relPath) {
|
|
100
|
+
const p = String(relPath || '').replace(/\\/g, '/');
|
|
101
|
+
const base = p.split('/').pop();
|
|
102
|
+
if (base === 'sources.md') return 'Reference: Source Registry';
|
|
103
|
+
if (base === 'blacklisted-sources.md') return 'Reference: Blacklist';
|
|
104
|
+
if (base === 'mantras-sources.md') return 'Reference: Mantra Sources';
|
|
105
|
+
if (base === 'axioms.md') return 'Axiom';
|
|
106
|
+
// Match by path SEGMENT (not '/testarch/') so it works for relative paths
|
|
107
|
+
// ('testarch/...') the converter passes AND absolute ones.
|
|
108
|
+
const segs = p.split('/');
|
|
109
|
+
if (segs.includes('testarch')) return 'Knowledge: Test Architecture';
|
|
110
|
+
if (segs.includes('excalidraw')) return 'Knowledge: Excalidraw';
|
|
111
|
+
return 'Knowledge';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** deriveTitle(body) -> the first `# ` heading text, or null. */
|
|
115
|
+
export function deriveTitle(body) {
|
|
116
|
+
const m = String(body || '').match(/^#\s+(.+?)\s*$/m);
|
|
117
|
+
return m ? m[1].trim() : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* deriveDescription(body) -> the first prose sentence (skipping headings, the
|
|
122
|
+
* bold `**key:**` metadata lines, blockquotes and rules), capped at 200 chars,
|
|
123
|
+
* or null when the body has no prose. Best-effort only (description is a
|
|
124
|
+
* RECOMMENDED field): the sentence split is naive (an abbreviation like "e.g."
|
|
125
|
+
* truncates early) and a leading bullet is taken verbatim — acceptable for a
|
|
126
|
+
* derived default a human can override in the frontmatter.
|
|
127
|
+
*/
|
|
128
|
+
export function deriveDescription(body) {
|
|
129
|
+
for (const raw of String(body || '').split('\n')) {
|
|
130
|
+
const t = raw.trim();
|
|
131
|
+
if (!t || t.startsWith('#') || t.startsWith('---') || t.startsWith('**') || t.startsWith('>') || t.startsWith('|')) continue;
|
|
132
|
+
const sentence = t.split(/(?<=[.!?])\s/)[0];
|
|
133
|
+
return sentence.length > 200 ? `${sentence.slice(0, 197)}...` : sentence;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
@@ -151,9 +151,75 @@ export function modelRoutingViolations(src) {
|
|
|
151
151
|
return out;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// Positive tiering ADVISORY — the symmetric half of modelRoutingViolations.
|
|
155
|
+
//
|
|
156
|
+
// modelRoutingViolations stops a downgrade from landing on a PROTECTED leaf
|
|
157
|
+
// (the anti-downgrade direction, a HARD contract rule). This function surfaces the
|
|
158
|
+
// opposite signal: an EXPLORATION-labelled leaf (read/load/parse/detect) that does
|
|
159
|
+
// NOT downgrade and so runs on the session model (Opus) for cheap I/O — a possible
|
|
160
|
+
// token saving. It is NON-BLOCKING (the bin's --advise mode, not validateContract):
|
|
161
|
+
// classifyLeaf is permissive, so a flagged leaf may legitimately stay deep when it
|
|
162
|
+
// bears judgment (a gate, a classification, an exact conversion). The human owns
|
|
163
|
+
// the call. Same source of truth: classifyLeaf + isDowngradeModel from
|
|
164
|
+
// native-tiers.js; this only reports against the contract those encode.
|
|
165
|
+
//
|
|
166
|
+
// Membership: a model: token belongs to a label when it sits in the SAME opts
|
|
167
|
+
// object — between the brace that closes the previous object and the one that
|
|
168
|
+
// closes this label's object — so the rule is order-independent (model: may sit
|
|
169
|
+
// before OR after label: within { ... }). Comment-stripped, like the others.
|
|
170
|
+
//
|
|
171
|
+
// Conservative by construction: only STATIC quoted/backtick labels are seen, and
|
|
172
|
+
// only labels classifyLeaf rules EXPLORATION fire. A computed (unquoted) label or
|
|
173
|
+
// a non-exploration label is never flagged, so the rule never forces a downgrade
|
|
174
|
+
// onto protected or unclassifiable work — it only recovers the safe savings.
|
|
175
|
+
function sameOptsObjectText(code, labelStart, labelEnd) {
|
|
176
|
+
const prevClose = code.lastIndexOf('}', labelStart);
|
|
177
|
+
const left = prevClose === -1 ? 0 : prevClose + 1;
|
|
178
|
+
const nextClose = code.indexOf('}', labelEnd);
|
|
179
|
+
const right = nextClose === -1 ? code.length : nextClose;
|
|
180
|
+
return code.slice(left, right);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function objectHasDowngradeModel(objText) {
|
|
184
|
+
let mm;
|
|
185
|
+
MODEL_RE.lastIndex = 0;
|
|
186
|
+
while ((mm = MODEL_RE.exec(objText))) {
|
|
187
|
+
if (isDowngradeModel(mm[2])) return true;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function untieredExplorationViolations(src) {
|
|
193
|
+
const code = stripComments(src);
|
|
194
|
+
const out = [];
|
|
195
|
+
let m;
|
|
196
|
+
LABEL_RE.lastIndex = 0;
|
|
197
|
+
while ((m = LABEL_RE.exec(code))) {
|
|
198
|
+
const label = m[2];
|
|
199
|
+
if (classifyLeaf({ label }) !== LEAF_TYPES.EXPLORATION) continue;
|
|
200
|
+
const objText = sameOptsObjectText(code, m.index, m.index + m[0].length);
|
|
201
|
+
if (!objectHasDowngradeModel(objText)) {
|
|
202
|
+
out.push({
|
|
203
|
+
id: 'untiered-exploration',
|
|
204
|
+
msg: `exploration leaf '${label}' does not pin a downgrade model; add model: 'haiku' so cheap I/O does not run on the session model (token waste)`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
154
211
|
// Full native-workflow contract: state-coupling (comment-stripped) + clock/RNG
|
|
155
212
|
// (raw) + meta-literal-first + model-routing anti-downgrade. Returns the
|
|
156
213
|
// combined [{ id, msg }] violations.
|
|
214
|
+
//
|
|
215
|
+
// untieredExplorationViolations is DELIBERATELY NOT in the hard contract. The
|
|
216
|
+
// tiering rule is a FLOOR (forbid a downgrade on a protected leaf), not a CEILING
|
|
217
|
+
// (force every exploration-labelled leaf to downgrade). classifyLeaf is permissive
|
|
218
|
+
// — many exploration-labelled leaves bear judgment (a HALT/prereq gate, a
|
|
219
|
+
// classification, an exact conversion consumed verbatim downstream) and MUST stay
|
|
220
|
+
// deep. Forcing them to haiku would be the exact STRICT-2 No Downgrade regression.
|
|
221
|
+
// So the positive check ships as a non-blocking ADVISORY (the bin's --advise mode);
|
|
222
|
+
// the human keeps the per-leaf judgment. See docs/native-workflows-contract.md.
|
|
157
223
|
export function validateContract(src) {
|
|
158
224
|
return [
|
|
159
225
|
...lintSource(src),
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"name": "byan-byan",
|
|
95
95
|
"module": "core",
|
|
96
96
|
"tier": "connector-bound",
|
|
97
|
-
"sourceHash": "
|
|
97
|
+
"sourceHash": "88403d60531d9fb3e7cdb566429643f1f47baf9918593e8082ecc98eedebb878"
|
|
98
98
|
},
|
|
99
99
|
"byan-byan-v2": {
|
|
100
100
|
"name": "byan-byan-v2",
|
|
@@ -490,7 +490,7 @@ try {
|
|
|
490
490
|
|
|
491
491
|
**Plan:** `BYAN-V2-MANUAL-TEST-PLAN.md`
|
|
492
492
|
|
|
493
|
-
1. Run demo-byan-v2-simple.js
|
|
493
|
+
1. Run examples/demo-byan-v2-simple.js
|
|
494
494
|
2. Verify 12 questions asked
|
|
495
495
|
3. Check profile generated
|
|
496
496
|
4. Validate against GitHub Copilot CLI requirements
|
|
@@ -570,7 +570,7 @@ byan.setTemplate('my-custom-template');
|
|
|
570
570
|
### Tests
|
|
571
571
|
- `__tests__/byan-v2/orchestrator/` - Tests unitaires
|
|
572
572
|
- `__tests__/byan-v2/integration/` - Tests intégration
|
|
573
|
-
- `demo-byan-v2-simple.js` - Démo complète
|
|
573
|
+
- `examples/demo-byan-v2-simple.js` - Démo complète
|
|
574
574
|
|
|
575
575
|
---
|
|
576
576
|
|
|
@@ -271,7 +271,7 @@ Emojis not allowed in technical sections
|
|
|
271
271
|
### Integration Tests
|
|
272
272
|
|
|
273
273
|
**Scenarios:**
|
|
274
|
-
1. Validate demo agent (demo-byan-v2-simple.js output)
|
|
274
|
+
1. Validate demo agent (examples/demo-byan-v2-simple.js output)
|
|
275
275
|
2. Validate all BYAN agents in _byan/agents/
|
|
276
276
|
3. Detect invalid agents (negative tests)
|
|
277
277
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -153,6 +153,14 @@ Enforcement (because the in-session hooks do not fire inside a script):
|
|
|
153
153
|
- `test/native-routing-integration.test.js` pins the invariant on the SHIPPED
|
|
154
154
|
scripts: every script passes the contract, and every downgrade sits on an
|
|
155
155
|
exploration leaf.
|
|
156
|
+
- `workflows-lint.js` -> `untieredExplorationViolations` is the SYMMETRIC
|
|
157
|
+
advisory: it surfaces an exploration-labelled leaf that runs deep (a possible
|
|
158
|
+
saving). It is DELIBERATELY OUT of `validateContract` — forcing those leaves to
|
|
159
|
+
haiku is exactly the regression the curation above rejected, so it is a
|
|
160
|
+
non-blocking report (`byan-lint-workflows.js --advise`), not a gate. The
|
|
161
|
+
per-leaf deep-vs-cheap call stays with the author. There is no per-leaf effort
|
|
162
|
+
knob (the native `agent()` / Agent API exposes only `model`), so model tier is
|
|
163
|
+
the sole token lever and effort-by-complexity reduces to model-by-complexity.
|
|
156
164
|
|
|
157
165
|
If a future runtime needs full model ids instead of the `haiku`/`sonnet`
|
|
158
166
|
aliases, `TIER_MODEL` in `native-tiers.js` is the only edit; the linter then
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-byan-agent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.29.0",
|
|
4
4
|
"description": "BYAN v2.8 - Intelligent AI agent creator with ELO trust system + scientific fact-check + Hermes universal dispatcher + native Claude Code integration (hooks, skills, MCP server). Multi-platform (Claude Code, Codex). Merise Agile + TDD + 71 Mantras. ~54% LLM cost savings.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"test:watch": "jest --watch",
|
|
18
18
|
"test:e2e": "jest install/__tests__/post-install.e2e.test.js",
|
|
19
19
|
"setup-turbo-whisper": "node install/setup-turbo-whisper.js",
|
|
20
|
+
"setup-rtk": "node install/setup-rtk.js",
|
|
20
21
|
"byan": "echo \"BYAN agent installed. Open Claude Code (or Codex) in this project.\"",
|
|
21
22
|
"version": "node scripts/sync-install-version.js && git add install/package.json",
|
|
22
23
|
"app:dev": "npm --prefix app run dev",
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"install/packages/platform-config/package.json",
|
|
62
63
|
"install/setup-turbo-whisper.js",
|
|
63
64
|
"install/setup-parakeet.js",
|
|
65
|
+
"install/setup-rtk.js",
|
|
64
66
|
"install/install.sh",
|
|
65
67
|
"install/package.json",
|
|
66
68
|
"update-byan-agent/bin/",
|