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.
Files changed (62) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/install/bin/create-byan-agent-v2.js +28 -0
  3. package/install/lib/native-helper.js +50 -0
  4. package/install/lib/rtk-integration.js +182 -0
  5. package/install/package.json +1 -1
  6. package/install/setup-rtk.js +41 -0
  7. package/install/templates/.claude/rules/native-workflows.md +25 -0
  8. package/install/templates/.claude/skills/byan-byan/SKILL.md +1 -0
  9. package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-lint-workflows.js +30 -4
  10. package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-okf.js +99 -0
  11. package/install/templates/_byan/mcp/byan-mcp-server/lib/okf-bundle.js +118 -0
  12. package/install/templates/_byan/mcp/byan-mcp-server/lib/okf-format.js +136 -0
  13. package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-lint.js +66 -0
  14. package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +1 -1
  15. package/install/templates/_byan/workflow/simple/interview-workflow.md +2 -2
  16. package/install/templates/_byan/workflow/simple/validate-agent-workflow.md +1 -1
  17. package/install/templates/dist/skill-bundles/byan-benchmark.zip +0 -0
  18. package/install/templates/dist/skill-bundles/byan-bmad-agent-tao.zip +0 -0
  19. package/install/templates/dist/skill-bundles/byan-bmad-master.zip +0 -0
  20. package/install/templates/dist/skill-bundles/byan-bmb-agent-builder.zip +0 -0
  21. package/install/templates/dist/skill-bundles/byan-bmb-module-builder.zip +0 -0
  22. package/install/templates/dist/skill-bundles/byan-bmb-workflow-builder.zip +0 -0
  23. package/install/templates/dist/skill-bundles/byan-bmm-analyst.zip +0 -0
  24. package/install/templates/dist/skill-bundles/byan-bmm-architect.zip +0 -0
  25. package/install/templates/dist/skill-bundles/byan-bmm-dev.zip +0 -0
  26. package/install/templates/dist/skill-bundles/byan-bmm-pm.zip +0 -0
  27. package/install/templates/dist/skill-bundles/byan-bmm-quick-flow-solo-dev.zip +0 -0
  28. package/install/templates/dist/skill-bundles/byan-bmm-quinn.zip +0 -0
  29. package/install/templates/dist/skill-bundles/byan-bmm-sm.zip +0 -0
  30. package/install/templates/dist/skill-bundles/byan-bmm-tech-writer.zip +0 -0
  31. package/install/templates/dist/skill-bundles/byan-bmm-ux-designer.zip +0 -0
  32. package/install/templates/dist/skill-bundles/byan-byan-v2.zip +0 -0
  33. package/install/templates/dist/skill-bundles/byan-byan.zip +0 -0
  34. package/install/templates/dist/skill-bundles/byan-carmack.zip +0 -0
  35. package/install/templates/dist/skill-bundles/byan-cis-brainstorming-coach.zip +0 -0
  36. package/install/templates/dist/skill-bundles/byan-cis-creative-problem-solver.zip +0 -0
  37. package/install/templates/dist/skill-bundles/byan-cis-design-thinking-coach.zip +0 -0
  38. package/install/templates/dist/skill-bundles/byan-cis-innovation-strategist.zip +0 -0
  39. package/install/templates/dist/skill-bundles/byan-cis-presentation-master.zip +0 -0
  40. package/install/templates/dist/skill-bundles/byan-cis-storyteller.zip +0 -0
  41. package/install/templates/dist/skill-bundles/byan-claude.zip +0 -0
  42. package/install/templates/dist/skill-bundles/byan-codex.zip +0 -0
  43. package/install/templates/dist/skill-bundles/byan-drawio.zip +0 -0
  44. package/install/templates/dist/skill-bundles/byan-elo-trust.zip +0 -0
  45. package/install/templates/dist/skill-bundles/byan-fact-check.zip +0 -0
  46. package/install/templates/dist/skill-bundles/byan-forge.zip +0 -0
  47. package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
  48. package/install/templates/dist/skill-bundles/byan-insight.zip +0 -0
  49. package/install/templates/dist/skill-bundles/byan-mantra-audit.zip +0 -0
  50. package/install/templates/dist/skill-bundles/byan-merise-agile.zip +0 -0
  51. package/install/templates/dist/skill-bundles/byan-native-dev-story.zip +0 -0
  52. package/install/templates/dist/skill-bundles/byan-orchestrate.zip +0 -0
  53. package/install/templates/dist/skill-bundles/byan-patnote.zip +0 -0
  54. package/install/templates/dist/skill-bundles/byan-project.zip +0 -0
  55. package/install/templates/dist/skill-bundles/byan-rachid.zip +0 -0
  56. package/install/templates/dist/skill-bundles/byan-skeptic.zip +0 -0
  57. package/install/templates/dist/skill-bundles/byan-strict.zip +0 -0
  58. package/install/templates/dist/skill-bundles/byan-suitability.zip +0 -0
  59. package/install/templates/dist/skill-bundles/byan-tea-tea.zip +0 -0
  60. package/install/templates/dist/skill-bundles/byan-yanstaller.zip +0 -0
  61. package/install/templates/docs/native-workflows-contract.md +8 -0
  62. 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
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.27.0",
3
+ "version": "2.29.0",
4
4
  "description": "BYAN - Intelligent AI agent installer with multi-platform native support (Claude Code, Codex/OpenCode)",
5
5
  "bin": {
6
6
  "create-byan-agent": "bin/create-byan-agent-v2.js"
@@ -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
- // Usage: node bin/byan-lint-workflows.js [--root <dir>]
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 violations = validateContract(fs.readFileSync(file, 'utf8'));
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": "0a1f4b1333f7f42a43b1a8ad57b0d7d687aec95f149ae13ba98b78970ed3bd09"
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
 
@@ -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.27.0",
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/",