create-byan-agent 2.9.4 → 2.9.5

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 (92) hide show
  1. package/install/bin/byan-cleanup.js +156 -0
  2. package/install/bin/byan-kanban.js +159 -0
  3. package/install/bin/byan-ledger.js +45 -0
  4. package/install/lib/cleanup/detector.js +154 -0
  5. package/install/lib/cleanup/executor.js +72 -0
  6. package/install/lib/subagent-generator.js +208 -0
  7. package/install/lib/token-ledger.js +131 -0
  8. package/install/templates/.claude/agents/bmad-bmad-master.md +14 -0
  9. package/install/templates/.claude/agents/bmad-bmb-agent-builder.md +14 -0
  10. package/install/templates/.claude/agents/bmad-bmb-module-builder.md +14 -0
  11. package/install/templates/.claude/agents/bmad-bmb-workflow-builder.md +14 -0
  12. package/install/templates/.claude/agents/bmad-bmm-analyst.md +14 -0
  13. package/install/templates/.claude/agents/bmad-bmm-architect.md +14 -0
  14. package/install/templates/.claude/agents/bmad-bmm-dev.md +14 -0
  15. package/install/templates/.claude/agents/bmad-bmm-pm.md +14 -0
  16. package/install/templates/.claude/agents/bmad-bmm-quick-flow-solo-dev.md +14 -0
  17. package/install/templates/.claude/agents/bmad-bmm-quinn.md +14 -0
  18. package/install/templates/.claude/agents/bmad-bmm-sm.md +14 -0
  19. package/install/templates/.claude/agents/bmad-bmm-tech-writer.md +14 -0
  20. package/install/templates/.claude/agents/bmad-bmm-ux-designer.md +14 -0
  21. package/install/templates/.claude/agents/bmad-byan-v2.md +14 -0
  22. package/install/templates/.claude/agents/bmad-byan.md +152 -0
  23. package/install/templates/.claude/agents/bmad-carmack.md +14 -0
  24. package/install/templates/.claude/agents/bmad-cis-brainstorming-coach.md +14 -0
  25. package/install/templates/.claude/agents/bmad-cis-creative-problem-solver.md +14 -0
  26. package/install/templates/.claude/agents/bmad-cis-design-thinking-coach.md +14 -0
  27. package/install/templates/.claude/agents/bmad-cis-innovation-strategist.md +14 -0
  28. package/install/templates/.claude/agents/bmad-cis-presentation-master.md +14 -0
  29. package/install/templates/.claude/agents/bmad-cis-storyteller.md +14 -0
  30. package/install/templates/.claude/agents/bmad-claude.md +26 -0
  31. package/install/templates/.claude/agents/bmad-codex.md +26 -0
  32. package/install/templates/.claude/agents/bmad-compliance.md +68 -0
  33. package/install/templates/.claude/agents/bmad-drawio.md +25 -0
  34. package/install/templates/.claude/agents/bmad-expert-merise-agile.md +54 -0
  35. package/install/templates/.claude/agents/bmad-fact-checker.md +14 -0
  36. package/install/templates/.claude/agents/bmad-forgeron.md +14 -0
  37. package/install/templates/.claude/agents/bmad-hermes.md +59 -0
  38. package/install/templates/.claude/agents/bmad-marc.md +25 -0
  39. package/install/templates/.claude/agents/bmad-patnote.md +26 -0
  40. package/install/templates/.claude/agents/bmad-rachid.md +25 -0
  41. package/install/templates/.claude/agents/bmad-tao.md +14 -0
  42. package/install/templates/.claude/agents/bmad-tea-tea.md +14 -0
  43. package/install/templates/.claude/agents/bmad-yanstaller.md +47 -0
  44. package/install/templates/.claude/hooks/fact-check-absolutes.js +185 -0
  45. package/install/templates/.claude/hooks/fd-phase-guard.js +87 -0
  46. package/install/templates/.claude/hooks/fd-response-check.js +92 -0
  47. package/install/templates/.claude/hooks/lib/failure-detector.js +14 -0
  48. package/install/templates/.claude/hooks/pre-compact-save.js +148 -0
  49. package/install/templates/.claude/hooks/tool-failure-guard.js +6 -0
  50. package/install/templates/.claude/hooks/tool-transparency.js +4 -0
  51. package/install/templates/.claude/settings.json +23 -0
  52. package/install/templates/.claude/skills/byan-byan/SKILL.md +115 -163
  53. package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +100 -0
  54. package/install/templates/.githooks/pre-commit +75 -0
  55. package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +148 -0
  56. package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +163 -0
  57. package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +226 -0
  58. package/install/templates/_byan/mcp/byan-mcp-server/lib/peer-review.js +187 -0
  59. package/install/templates/_byan/mcp/byan-mcp-server/server.js +463 -0
  60. package/install/templates/detector.js +154 -0
  61. package/package.json +6 -7
  62. package/src/loadbalancer/capability-matrix.js +157 -0
  63. package/src/loadbalancer/config.js +141 -0
  64. package/src/loadbalancer/graceful-degradation.js +212 -0
  65. package/src/loadbalancer/health-probe.js +151 -0
  66. package/src/loadbalancer/hooks/claude-hooks.js +53 -0
  67. package/src/loadbalancer/hooks/copilot-hooks.js +74 -0
  68. package/src/loadbalancer/index.js +81 -0
  69. package/src/loadbalancer/loadbalancer.default.yaml +65 -0
  70. package/src/loadbalancer/loadbalancer.js +324 -0
  71. package/src/loadbalancer/mcp-server.js +304 -0
  72. package/src/loadbalancer/metrics.js +146 -0
  73. package/src/loadbalancer/native/claude-integration.js +64 -0
  74. package/src/loadbalancer/native/copilot-integration.js +59 -0
  75. package/src/loadbalancer/pressure-score.js +102 -0
  76. package/src/loadbalancer/providers/base-provider.js +80 -0
  77. package/src/loadbalancer/providers/byan-api-provider.js +132 -0
  78. package/src/loadbalancer/providers/claude-provider.js +113 -0
  79. package/src/loadbalancer/providers/copilot-provider.js +104 -0
  80. package/src/loadbalancer/rate-limit-tracker.js +216 -0
  81. package/src/loadbalancer/session-bridge.js +179 -0
  82. package/src/loadbalancer/state/db.js +211 -0
  83. package/src/loadbalancer/state/migrations/001-initial.sql +50 -0
  84. package/src/loadbalancer/tools/index.js +123 -0
  85. package/src/loadbalancer/velocity-estimator.js +147 -0
  86. package/update-byan-agent/bin/update-byan-agent.js +27 -2
  87. package/API-BYAN-V2.md +0 -741
  88. package/BMAD-QUICK-REFERENCE.md +0 -370
  89. package/CHANGELOG-v2.1.0.md +0 -371
  90. package/MIGRATION-v2.0-to-v2.1.md +0 -430
  91. package/README-BYAN-V2.md +0 -446
  92. package/TEST-GUIDE-v2.3.2.md +0 -161
@@ -0,0 +1,304 @@
1
+ /**
2
+ * BYAN LoadBalancer — MCP Server
3
+ *
4
+ * Standalone MCP server exposing loadbalancer tools to Claude Code and
5
+ * Copilot CLI. HTTP transport for persistence across sessions.
6
+ *
7
+ * Start: node src/loadbalancer/mcp-server.js
8
+ * Connect: add to .mcp.json or claude settings
9
+ *
10
+ * Architecture:
11
+ * CLI (Claude/Copilot) --MCP--> this server --SDK--> providers
12
+ */
13
+
14
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
15
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
16
+ const {
17
+ ListToolsRequestSchema,
18
+ CallToolRequestSchema,
19
+ } = require('@modelcontextprotocol/sdk/types.js');
20
+
21
+ const { loadConfig } = require('./config');
22
+ const { createTools } = require('./tools/index');
23
+ const { RateLimitTracker } = require('./rate-limit-tracker');
24
+ const { Metrics } = require('./metrics');
25
+ const { VelocityEstimator } = require('./velocity-estimator');
26
+ const { calculatePressure, formatPressureSummary } = require('./pressure-score');
27
+ const { EventEmitter } = require('events');
28
+
29
+ const VERSION = '0.2.0';
30
+
31
+ class LoadBalancerLive extends EventEmitter {
32
+ constructor(config) {
33
+ super();
34
+ this.config = config;
35
+ this.activeProvider = config.primary;
36
+ this.startedAt = new Date().toISOString();
37
+ this.switchHistory = [];
38
+
39
+ this.trackers = {};
40
+ this.velocities = {};
41
+ this.metrics = new Metrics();
42
+
43
+ const rlOpts = config.rate_limits || {};
44
+ const quotaOpts = config.quota || {};
45
+ this.preemptiveThreshold = quotaOpts.preemptive_threshold || 75;
46
+ this.preemptiveEnabled = quotaOpts.preemptive_enabled !== false;
47
+
48
+ for (const [name, prov] of Object.entries(config.providers)) {
49
+ if (prov.enabled !== false) {
50
+ this.trackers[name] = new RateLimitTracker(name, rlOpts);
51
+ this.trackers[name].on('state_change', (evt) => {
52
+ this.emit('rate_limit_change', evt);
53
+ });
54
+
55
+ this.velocities[name] = new VelocityEstimator(name, {
56
+ windowMs: quotaOpts.velocity_window_ms || 120000,
57
+ warningThresholdPerMin: quotaOpts.warning_threshold_per_min || 10,
58
+ maxRequestsBeforeLimit: quotaOpts.max_requests_before_limit || 30,
59
+ });
60
+ this.velocities[name].on('threshold_warning', (evt) => {
61
+ this.emit('velocity_warning', evt);
62
+ });
63
+ }
64
+ }
65
+
66
+ this.metrics.attachToLoadBalancer(this);
67
+ }
68
+
69
+ getTracker(provider) {
70
+ return this.trackers[provider] || null;
71
+ }
72
+
73
+ getVelocity(provider) {
74
+ return this.velocities[provider] || null;
75
+ }
76
+
77
+ getStatus() {
78
+ const providers = {};
79
+ for (const [name, prov] of Object.entries(this.config.providers)) {
80
+ const tracker = this.trackers[name];
81
+ providers[name] = {
82
+ enabled: prov.enabled !== false,
83
+ state: tracker ? tracker.state : 'DISABLED',
84
+ };
85
+ }
86
+
87
+ return {
88
+ version: VERSION,
89
+ activeProvider: this.activeProvider,
90
+ primary: this.config.primary,
91
+ fallbackOrder: this.config.fallback_order,
92
+ providers,
93
+ uptime: Date.now() - new Date(this.startedAt).getTime(),
94
+ };
95
+ }
96
+
97
+ getRateLimitDetails() {
98
+ const result = {};
99
+ for (const [name] of Object.entries(this.config.providers)) {
100
+ const tracker = this.trackers[name];
101
+ if (tracker) {
102
+ const s = tracker.getState();
103
+ result[name] = {
104
+ state: s.state,
105
+ count429InWindow: s.count429InWindow,
106
+ totalRequests: s.totalRequests,
107
+ total429s: s.total429s,
108
+ windowMs: s.windowMs,
109
+ canAcceptRequest: s.canAcceptRequest,
110
+ lastStateChange: s.lastStateChange,
111
+ };
112
+ } else {
113
+ result[name] = { state: 'DISABLED', message: 'Provider not enabled' };
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+
119
+ getSwitchoverHistory(limit = 20) {
120
+ const events = this.switchHistory.slice(-limit);
121
+ return { events, total: this.switchHistory.length, limit };
122
+ }
123
+
124
+ recordSuccess(provider) {
125
+ const tracker = this.trackers[provider];
126
+ if (tracker) tracker.recordSuccess();
127
+ const ve = this.velocities[provider];
128
+ if (ve) ve.recordRequest();
129
+ this._checkPreemptive(provider);
130
+ }
131
+
132
+ record429(provider, meta) {
133
+ const tracker = this.trackers[provider];
134
+ if (tracker) tracker.record429(meta);
135
+ const ve = this.velocities[provider];
136
+ if (ve) ve.recordRequest();
137
+ this._checkPreemptive(provider);
138
+ }
139
+
140
+ _checkPreemptive(provider) {
141
+ if (!this.preemptiveEnabled) return;
142
+ if (provider !== this.activeProvider) return;
143
+
144
+ const tracker = this.trackers[provider];
145
+ const ve = this.velocities[provider];
146
+ if (!tracker || !ve) return;
147
+
148
+ const trackerState = tracker.getState();
149
+ const velocitySnap = ve.getSnapshot();
150
+ const pressure = calculatePressure(trackerState, velocitySnap, {
151
+ blockThreshold: this.config.rate_limits?.block_threshold,
152
+ });
153
+
154
+ if (pressure.score >= this.preemptiveThreshold) {
155
+ const fallback = this._findBestFallback(provider);
156
+ this.emit('preemptive_switch', {
157
+ provider,
158
+ pressureScore: pressure.score,
159
+ recommendation: pressure.recommendation,
160
+ suggestedTarget: fallback,
161
+ timestamp: new Date().toISOString(),
162
+ });
163
+ }
164
+ }
165
+
166
+ _findBestFallback(excludeProvider) {
167
+ const order = [this.config.primary, ...(this.config.fallback_order || [])];
168
+ for (const name of order) {
169
+ if (name === excludeProvider) continue;
170
+ const tracker = this.trackers[name];
171
+ if (tracker && tracker.canAcceptRequest()) return name;
172
+ }
173
+ return null;
174
+ }
175
+
176
+ getQuota() {
177
+ const result = {};
178
+ for (const [name] of Object.entries(this.config.providers)) {
179
+ const tracker = this.trackers[name];
180
+ const ve = this.velocities[name];
181
+ if (tracker && ve) {
182
+ const trackerState = tracker.getState();
183
+ const velocitySnap = ve.getSnapshot();
184
+ const pressure = calculatePressure(trackerState, velocitySnap, {
185
+ blockThreshold: this.config.rate_limits?.block_threshold,
186
+ });
187
+ result[name] = {
188
+ pressureScore: pressure.score,
189
+ recommendation: pressure.recommendation,
190
+ components: pressure.components,
191
+ velocity: velocitySnap.velocity,
192
+ trend: velocitySnap.trend,
193
+ etaMinutes: velocitySnap.etaMinutes === Infinity ? null : velocitySnap.etaMinutes,
194
+ circuitBreakerState: trackerState.state,
195
+ summary: formatPressureSummary(name, pressure, velocitySnap),
196
+ };
197
+ } else {
198
+ result[name] = { pressureScore: null, recommendation: 'disabled', summary: `${name}: DISABLED` };
199
+ }
200
+ }
201
+ return result;
202
+ }
203
+
204
+ async send(opts) {
205
+ return {
206
+ provider: this.activeProvider,
207
+ status: 'stub',
208
+ message: 'ProviderAdapter not yet integrated (LB-01). Use lb_status/lb_quota for monitoring.',
209
+ prompt: opts.prompt?.substring(0, 100),
210
+ };
211
+ }
212
+
213
+ async switchProvider(opts) {
214
+ const prev = this.activeProvider;
215
+ this.activeProvider = opts.target;
216
+ const entry = {
217
+ timestamp: new Date().toISOString(),
218
+ from: prev,
219
+ to: opts.target,
220
+ reason: opts.reason || 'manual_switch',
221
+ };
222
+ this.switchHistory.push(entry);
223
+ this.emit('switch', entry);
224
+ return {
225
+ switched: true,
226
+ from: prev,
227
+ to: opts.target,
228
+ reason: opts.reason,
229
+ contextTransferred: false,
230
+ message: 'SessionBridge not yet integrated (LB-04). No context transfer.',
231
+ };
232
+ }
233
+
234
+ async getSessionContext(sessionId) {
235
+ return {
236
+ sessionId: sessionId || 'current',
237
+ provider: this.activeProvider,
238
+ context: null,
239
+ message: 'SharedStateStore not yet integrated (LB-STATE).',
240
+ };
241
+ }
242
+
243
+ destroy() {
244
+ for (const tracker of Object.values(this.trackers)) {
245
+ tracker.destroy();
246
+ }
247
+ for (const ve of Object.values(this.velocities)) {
248
+ ve.destroy();
249
+ }
250
+ this.removeAllListeners();
251
+ }
252
+ }
253
+
254
+ async function startServer(projectRoot) {
255
+ const resolvedRoot = projectRoot || process.env.BYAN_PROJECT_ROOT || process.cwd();
256
+ const config = loadConfig(resolvedRoot);
257
+ const lb = new LoadBalancerLive(config);
258
+ const tools = createTools(lb);
259
+
260
+ const server = new Server(
261
+ { name: 'byan-loadbalancer', version: VERSION },
262
+ { capabilities: { tools: {} } }
263
+ );
264
+
265
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
266
+ tools: tools.map(t => ({
267
+ name: t.name,
268
+ description: t.description,
269
+ inputSchema: t.inputSchema,
270
+ })),
271
+ }));
272
+
273
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
274
+ const tool = tools.find(t => t.name === request.params.name);
275
+ if (!tool) {
276
+ return {
277
+ content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }],
278
+ isError: true,
279
+ };
280
+ }
281
+ try {
282
+ return await tool.handler(request.params.arguments || {});
283
+ } catch (err) {
284
+ return {
285
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
286
+ isError: true,
287
+ };
288
+ }
289
+ });
290
+
291
+ const transport = new StdioServerTransport();
292
+ await server.connect(transport);
293
+
294
+ return { server, lb, config };
295
+ }
296
+
297
+ if (require.main === module) {
298
+ startServer().catch(err => {
299
+ process.stderr.write(`LoadBalancer MCP server failed to start: ${err.message}\n`);
300
+ process.exit(1);
301
+ });
302
+ }
303
+
304
+ module.exports = { startServer, LoadBalancerLive, VERSION };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Metrics — LoadBalancer Observability
3
+ *
4
+ * Collects and exposes metrics for the loadbalancer:
5
+ * - Provider usage percentages
6
+ * - Switchover count and history
7
+ * - Average latency per provider
8
+ * - Rate limit hit totals
9
+ * - Queue depth (graceful degradation)
10
+ *
11
+ * Integrates with LoadBalancer events. Queryable via lb_status MCP tool.
12
+ */
13
+
14
+ class Metrics {
15
+ constructor() {
16
+ this.counters = {
17
+ requests_total: 0,
18
+ requests_by_provider: {},
19
+ switchovers_total: 0,
20
+ auto_failovers_total: 0,
21
+ rate_limit_hits: {},
22
+ queue_enqueued_total: 0,
23
+ queue_processed_total: 0,
24
+ queue_dropped_total: 0,
25
+ };
26
+
27
+ this.latency = {};
28
+ this.startedAt = Date.now();
29
+ }
30
+
31
+ /**
32
+ * Attach event listeners to a LoadBalancer instance.
33
+ * @param {import('./loadbalancer').LoadBalancer} lb
34
+ */
35
+ attachToLoadBalancer(lb) {
36
+ lb.on('rate_limit_change', (event) => {
37
+ if (event.to === 'BLOCKED' || event.to === 'THROTTLED') {
38
+ this._incrementCounter('rate_limit_hits', event.provider);
39
+ }
40
+ });
41
+
42
+ lb.on('switch', (event) => {
43
+ this.counters.switchovers_total++;
44
+ });
45
+
46
+ lb.on('auto_failover', (event) => {
47
+ this.counters.auto_failovers_total++;
48
+ this.counters.switchovers_total++;
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Attach event listeners to a GracefulDegradation instance.
54
+ * @param {import('./graceful-degradation').GracefulDegradation} gd
55
+ */
56
+ attachToGracefulDegradation(gd) {
57
+ gd.on('enqueued', () => { this.counters.queue_enqueued_total++; });
58
+ gd.on('processed', () => { this.counters.queue_processed_total++; });
59
+ }
60
+
61
+ /**
62
+ * Record a provider request with latency.
63
+ * @param {string} provider
64
+ * @param {number} latencyMs
65
+ */
66
+ recordRequest(provider, latencyMs) {
67
+ this.counters.requests_total++;
68
+ this._incrementCounter('requests_by_provider', provider);
69
+
70
+ if (!this.latency[provider]) {
71
+ this.latency[provider] = { total: 0, count: 0, min: Infinity, max: 0 };
72
+ }
73
+
74
+ const l = this.latency[provider];
75
+ l.total += latencyMs;
76
+ l.count++;
77
+ l.min = Math.min(l.min, latencyMs);
78
+ l.max = Math.max(l.max, latencyMs);
79
+ }
80
+
81
+ /**
82
+ * Get all metrics as a snapshot.
83
+ */
84
+ getSnapshot() {
85
+ const uptimeMs = Date.now() - this.startedAt;
86
+
87
+ const providerUsage = {};
88
+ const total = this.counters.requests_total || 1;
89
+ for (const [provider, count] of Object.entries(this.counters.requests_by_provider)) {
90
+ providerUsage[provider] = {
91
+ count,
92
+ percentage: Math.round((count / total) * 100),
93
+ };
94
+ }
95
+
96
+ const latencyStats = {};
97
+ for (const [provider, l] of Object.entries(this.latency)) {
98
+ latencyStats[provider] = {
99
+ avg: l.count > 0 ? Math.round(l.total / l.count) : 0,
100
+ min: l.min === Infinity ? 0 : l.min,
101
+ max: l.max,
102
+ count: l.count,
103
+ };
104
+ }
105
+
106
+ return {
107
+ uptime_ms: uptimeMs,
108
+ requests_total: this.counters.requests_total,
109
+ provider_usage: providerUsage,
110
+ latency: latencyStats,
111
+ switchovers_total: this.counters.switchovers_total,
112
+ auto_failovers_total: this.counters.auto_failovers_total,
113
+ rate_limit_hits: { ...this.counters.rate_limit_hits },
114
+ queue: {
115
+ enqueued_total: this.counters.queue_enqueued_total,
116
+ processed_total: this.counters.queue_processed_total,
117
+ dropped_total: this.counters.queue_dropped_total,
118
+ },
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Reset all metrics.
124
+ */
125
+ reset() {
126
+ this.counters = {
127
+ requests_total: 0,
128
+ requests_by_provider: {},
129
+ switchovers_total: 0,
130
+ auto_failovers_total: 0,
131
+ rate_limit_hits: {},
132
+ queue_enqueued_total: 0,
133
+ queue_processed_total: 0,
134
+ queue_dropped_total: 0,
135
+ };
136
+ this.latency = {};
137
+ this.startedAt = Date.now();
138
+ }
139
+
140
+ _incrementCounter(counterName, key) {
141
+ if (!this.counters[counterName]) this.counters[counterName] = {};
142
+ this.counters[counterName][key] = (this.counters[counterName][key] || 0) + 1;
143
+ }
144
+ }
145
+
146
+ module.exports = { Metrics };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Claude Native Integration
3
+ *
4
+ * Wires LoadBalancer into Claude Code's agent system.
5
+ * Ensures Claude executes BYAN logic natively via Claude Agent SDK,
6
+ * with hooks feeding the LoadBalancer tracker.
7
+ */
8
+
9
+ const { createClaudeHooks } = require('../hooks/claude-hooks');
10
+
11
+ class ClaudeIntegration {
12
+ /**
13
+ * @param {import('../loadbalancer').LoadBalancer} lb
14
+ */
15
+ constructor(lb) {
16
+ this.lb = lb;
17
+ this.tracker = lb.getTracker('claude');
18
+ }
19
+
20
+ /**
21
+ * Get Claude Agent SDK hooks wired to the rate limit tracker.
22
+ * @returns {object} Hook configuration for Claude Agent SDK
23
+ */
24
+ getHooks() {
25
+ if (!this.tracker) return {};
26
+ return createClaudeHooks(this.tracker);
27
+ }
28
+
29
+ /**
30
+ * Generate .mcp.json entry for connecting Claude to the LB MCP server.
31
+ * @returns {object} MCP configuration fragment
32
+ */
33
+ getMcpConfig() {
34
+ return {
35
+ 'byan-loadbalancer': {
36
+ command: 'node',
37
+ args: ['src/loadbalancer/mcp-server.js'],
38
+ env: {
39
+ BYAN_PROJECT_ROOT: '{project-root}',
40
+ },
41
+ },
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Generate CLAUDE.md directive for LoadBalancer awareness.
47
+ * @returns {string} Markdown directive
48
+ */
49
+ getClaudeMdDirective() {
50
+ return [
51
+ '## LoadBalancer Integration',
52
+ '',
53
+ 'This project uses BYAN LoadBalancer for automatic failover between',
54
+ 'GitHub Copilot CLI and Claude Code. The MCP tool `byan-loadbalancer`',
55
+ 'provides: lb_status, lb_send, lb_switch, lb_get_context, lb_rate_limits,',
56
+ 'lb_history.',
57
+ '',
58
+ 'When rate-limited, the LoadBalancer automatically switches to the',
59
+ 'alternate provider with context transfer.',
60
+ ].join('\n');
61
+ }
62
+ }
63
+
64
+ module.exports = { ClaudeIntegration };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Copilot Native Integration
3
+ *
4
+ * Wires LoadBalancer into GitHub Copilot CLI's agent system.
5
+ * Ensures Copilot executes BYAN logic natively via its own SDK,
6
+ * with rate limit hooks feeding the LoadBalancer tracker.
7
+ */
8
+
9
+ const { attachCopilotHooks, wrapCopilotSend } = require('../hooks/copilot-hooks');
10
+
11
+ class CopilotIntegration {
12
+ /**
13
+ * @param {import('../loadbalancer').LoadBalancer} lb
14
+ */
15
+ constructor(lb) {
16
+ this.lb = lb;
17
+ this.tracker = lb.getTracker('copilot');
18
+ }
19
+
20
+ /**
21
+ * Enhance a Copilot SDK session with rate limit tracking.
22
+ * @param {object} session - Copilot SDK session object
23
+ * @returns {object} Enhanced session
24
+ */
25
+ enhanceSession(session) {
26
+ if (!this.tracker) return session;
27
+ attachCopilotHooks(session, this.tracker);
28
+ return session;
29
+ }
30
+
31
+ /**
32
+ * Wrap a sendAndWait function with rate limit tracking.
33
+ * @param {Function} sendFn
34
+ * @returns {Function}
35
+ */
36
+ wrapSend(sendFn) {
37
+ if (!this.tracker) return sendFn;
38
+ return wrapCopilotSend(sendFn, this.tracker);
39
+ }
40
+
41
+ /**
42
+ * Generate .mcp.json entry for connecting Copilot to the LB MCP server.
43
+ * @returns {object} MCP configuration fragment
44
+ */
45
+ getMcpConfig() {
46
+ const mcpConfig = this.lb.config.mcp_server || {};
47
+ return {
48
+ 'byan-loadbalancer': {
49
+ command: 'node',
50
+ args: ['src/loadbalancer/mcp-server.js'],
51
+ env: {
52
+ BYAN_PROJECT_ROOT: '{project-root}',
53
+ },
54
+ },
55
+ };
56
+ }
57
+ }
58
+
59
+ module.exports = { CopilotIntegration };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * PressureScore — Composite Rate Limit Pressure (0-100)
3
+ *
4
+ * Single number per provider answering: "How close am I to hitting the wall?"
5
+ *
6
+ * Components (weights configurable):
7
+ * - 429 ratio (40%) — total429s / totalRequests in circuit breaker
8
+ * - Proximity (30%) — count429InWindow / blockThreshold
9
+ * - Velocity (20%) — current req/min / warningThreshold
10
+ * - State bonus (10%) — HEALTHY=0, THROTTLED=50, RECOVERING=70, BLOCKED=100
11
+ *
12
+ * Pure computation — takes tracker + velocity snapshots, returns score.
13
+ */
14
+
15
+ const STATE_SCORES = {
16
+ HEALTHY: 0,
17
+ THROTTLED: 50,
18
+ RECOVERING: 70,
19
+ BLOCKED: 100,
20
+ };
21
+
22
+ const DEFAULT_WEIGHTS = {
23
+ ratio429: 0.40,
24
+ proximity: 0.30,
25
+ velocity: 0.20,
26
+ statePenalty: 0.10,
27
+ };
28
+
29
+ /**
30
+ * Calculate pressure score for a single provider.
31
+ *
32
+ * @param {object} trackerState — from RateLimitTracker.getState()
33
+ * @param {object} velocitySnap — from VelocityEstimator.getSnapshot()
34
+ * @param {object} [opts] — override weights and thresholds
35
+ * @returns {object} { score, components, recommendation }
36
+ */
37
+ function calculatePressure(trackerState, velocitySnap, opts = {}) {
38
+ const weights = { ...DEFAULT_WEIGHTS, ...opts.weights };
39
+ const blockThreshold = opts.blockThreshold || trackerState.windowMs ? 3 : 3;
40
+
41
+ const ratio429 = trackerState.totalRequests > 0
42
+ ? Math.min(1, trackerState.total429s / trackerState.totalRequests)
43
+ : 0;
44
+
45
+ const proximity = Math.min(1, trackerState.count429InWindow / (blockThreshold || 3));
46
+
47
+ const velocityRatio = velocitySnap.warningThreshold > 0
48
+ ? Math.min(1, velocitySnap.velocity / velocitySnap.warningThreshold)
49
+ : 0;
50
+
51
+ const stateScore = (STATE_SCORES[trackerState.state] || 0) / 100;
52
+
53
+ const raw = (
54
+ ratio429 * weights.ratio429 +
55
+ proximity * weights.proximity +
56
+ velocityRatio * weights.velocity +
57
+ stateScore * weights.statePenalty
58
+ );
59
+
60
+ const score = Math.round(Math.min(100, raw * 100));
61
+
62
+ let recommendation;
63
+ if (score >= 80) recommendation = 'switch_now';
64
+ else if (score >= 50) recommendation = 'caution';
65
+ else recommendation = 'ok';
66
+
67
+ return {
68
+ score,
69
+ components: {
70
+ ratio429: Math.round(ratio429 * 100),
71
+ proximity: Math.round(proximity * 100),
72
+ velocity: Math.round(velocityRatio * 100),
73
+ statePenalty: Math.round(stateScore * 100),
74
+ },
75
+ recommendation,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Human-readable summary for lb_quota tool output.
81
+ */
82
+ function formatPressureSummary(provider, pressure, velocitySnap) {
83
+ const etaStr = velocitySnap.etaMinutes === Infinity
84
+ ? 'no limit in sight'
85
+ : `~${velocitySnap.etaMinutes} min before limit`;
86
+
87
+ const lines = [
88
+ `${provider}: ${pressure.score}/100 [${pressure.recommendation.toUpperCase()}]`,
89
+ ` velocity: ${velocitySnap.velocity} req/min (${velocitySnap.trend})`,
90
+ ` eta: ${etaStr}`,
91
+ ` breakdown: 429_ratio=${pressure.components.ratio429}% proximity=${pressure.components.proximity}% velocity=${pressure.components.velocity}% state=${pressure.components.statePenalty}%`,
92
+ ];
93
+
94
+ return lines.join('\n');
95
+ }
96
+
97
+ module.exports = {
98
+ calculatePressure,
99
+ formatPressureSummary,
100
+ STATE_SCORES,
101
+ DEFAULT_WEIGHTS,
102
+ };