@octanejs/mcp-server 0.2.2 → 0.2.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.
package/src/index.js CHANGED
@@ -7,7 +7,7 @@ import { readFile } from 'node:fs/promises';
7
7
  import { dirname, join, resolve } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { z } from 'zod';
10
- import { bridgeReport, KNOWN_BINDINGS } from './bridge.js';
10
+ import { bridgeReport, KNOWN_BINDINGS, KNOWN_BINDING_PACKAGE_DIRS } from './bridge.js';
11
11
 
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const PACKAGE_ROOT = resolve(dirname(__filename), '..');
@@ -18,6 +18,7 @@ const PACKAGE_ROOT = resolve(dirname(__filename), '..');
18
18
  // workflows: triage, PRs, core changes).
19
19
  export const BUNDLED_SKILLS = {
20
20
  'bridge-react-package': 'skills/bridge-react-package.md',
21
+ 'build-octane-software': 'skills/build-octane-software.md',
21
22
  'migrate-react-component': 'skills/migrate-react-component.md',
22
23
  'react-divergences': 'skills/react-divergences.md',
23
24
  'setup-ssr': 'skills/setup-ssr.md',
@@ -33,16 +34,46 @@ export const REPO_SKILLS = {
33
34
  triage: '.ai/skills/triage.md',
34
35
  };
35
36
 
36
- const BENCHMARKS = {
37
- dbmon: '@benchmarks/dbmon',
38
- 'js-framework': 'octane-js-framework-benchmarks',
39
- news: '@benchmarks/news',
40
- 'recursive-context': '@benchmarks/recursive-context',
41
- 'signal-favoring': '@benchmarks/signal-favoring',
42
- };
37
+ // Suite names from the unified runner manifest (`SUITES` in
38
+ // benchmarks/bench.mjs; `node benchmarks/bench.mjs --list` prints the same
39
+ // set). index.test.js keeps this list in sync with the runner.
40
+ export const BENCHMARK_SUITES = [
41
+ 'js-framework',
42
+ 'js-framework-reorder',
43
+ 'todomvc',
44
+ 'weather-app',
45
+ 'weather-app-lighthouse',
46
+ 'chat-stream',
47
+ 'dbmon',
48
+ 'recursive-context',
49
+ 'signal-favoring',
50
+ 'news',
51
+ 'effectful-list',
52
+ 'memo-wall',
53
+ 'portal-swarm',
54
+ 'react-hosted-islands',
55
+ 'ssr-throughput',
56
+ 'streaming-ssr',
57
+ 'dbmon-deopt',
58
+ 'js-framework-deopt',
59
+ 'async-waterfall',
60
+ 'async-composition',
61
+ 'codegen-size',
62
+ 'bundle-size',
63
+ 'three-renderer',
64
+ 'three-bundle-size',
65
+ ];
43
66
 
44
67
  const DEFAULT_TIMEOUT_MS = 120_000;
45
68
 
69
+ export function instructionsFor(repoMode) {
70
+ const common =
71
+ 'Before creating or materially changing Octane software, call octane_engineering_plan and load the build-octane-software skill. Treat its correctness, performance evidence, and adversarial self-review gates as required. Load the task-specific migration, binding, divergence, or SSR skill in addition when relevant. Do not claim a performance improvement without comparable measurements.';
72
+ return repoMode
73
+ ? `${common} For Octane framework-fundamental work, also load octane-core-extend and performance-audit, establish a relevant baseline before editing, and use octane_validate_plan for the final changed paths.`
74
+ : common;
75
+ }
76
+
46
77
  export function text(content) {
47
78
  return { content: [{ type: 'text', text: content }] };
48
79
  }
@@ -59,12 +90,19 @@ export function areaForPath(path) {
59
90
  return 'core-runtime';
60
91
  }
61
92
  if (path.startsWith('packages/octane/tests/')) return 'core-tests';
93
+ if (path.startsWith('packages/app-core/')) return 'metaframework-core';
94
+ if (path.startsWith('packages/rspack-plugin-octane/')) return 'rspack-plugin';
95
+ if (path.startsWith('packages/rsbuild-plugin-octane/')) return 'rsbuild-plugin';
62
96
  if (path.startsWith('packages/vite-plugin-octane/')) return 'vite-plugin';
97
+ if (path.startsWith('packages/adapter-vercel/')) return 'deploy-adapter';
98
+ if (path.startsWith('packages/octane-evals/')) return 'evals';
63
99
  if (path.startsWith('packages/octane-mcp-server/')) return 'mcp-server';
64
- if (/^packages\/(zustand|query|motion|stylex|router|lexical|floating-ui|radix)\//.test(path)) {
100
+ const packageMatch = path.match(/^packages\/([^/]+)\//);
101
+ if (packageMatch && KNOWN_BINDING_PACKAGE_DIRS.has(packageMatch[1])) {
65
102
  return 'ecosystem-binding';
66
103
  }
67
104
  if (path.startsWith('benchmarks/')) return 'benchmark';
105
+ if (path.startsWith('website/')) return 'website';
68
106
  if (path.startsWith('.rulesync/')) return 'rulesync-source';
69
107
  if (path.startsWith('.ai/') || path.startsWith('.codex/') || path.startsWith('.claude/')) {
70
108
  return 'agent-instructions';
@@ -88,13 +126,12 @@ export function validationFor(paths, taskKind) {
88
126
  }
89
127
  if (areas.has('ecosystem-binding')) {
90
128
  for (const path of paths) {
91
- const match = path.match(
92
- /^packages\/(zustand|query|motion|stylex|router|lexical|floating-ui|radix)\//,
93
- );
94
- if (match)
129
+ const match = path.match(/^packages\/([^/]+)\//);
130
+ if (match && KNOWN_BINDING_PACKAGE_DIRS.has(match[1])) {
95
131
  commands.add(
96
132
  `./node_modules/.bin/vitest run packages/${match[1]}/tests --project ${match[1]}`,
97
133
  );
134
+ }
98
135
  }
99
136
  }
100
137
  if (areas.has('mcp-server')) {
@@ -102,8 +139,49 @@ export function validationFor(paths, taskKind) {
102
139
  './node_modules/.bin/vitest run packages/octane-mcp-server --project octane-mcp-server',
103
140
  );
104
141
  }
105
- if (areas.has('vite-plugin')) commands.add('pnpm typecheck');
106
- if (areas.has('benchmark') || taskKind === 'performance') commands.add('pnpm bench');
142
+ if (areas.has('evals')) {
143
+ commands.add(
144
+ './node_modules/.bin/vitest run packages/octane-evals/tests --project octane-evals',
145
+ );
146
+ }
147
+ if (areas.has('website')) {
148
+ commands.add('./node_modules/.bin/vitest run website/tests --project website');
149
+ }
150
+ if (areas.has('metaframework-core')) {
151
+ commands.add('./node_modules/.bin/vitest run packages/app-core/tests --project app-core');
152
+ }
153
+ if (areas.has('rspack-plugin')) {
154
+ commands.add(
155
+ './node_modules/.bin/vitest run packages/rspack-plugin-octane/tests --project rspack-plugin',
156
+ );
157
+ }
158
+ if (areas.has('rsbuild-plugin')) {
159
+ commands.add(
160
+ './node_modules/.bin/vitest run packages/rsbuild-plugin-octane/tests --project rsbuild-plugin',
161
+ );
162
+ }
163
+ if (areas.has('vite-plugin')) {
164
+ commands.add(
165
+ './node_modules/.bin/vitest run packages/vite-plugin-octane/tests --project vite-plugin',
166
+ );
167
+ }
168
+ if (areas.has('deploy-adapter')) {
169
+ commands.add(
170
+ './node_modules/.bin/vitest run packages/adapter-vercel/tests --project adapter-vercel',
171
+ );
172
+ }
173
+ if (
174
+ areas.has('metaframework-core') ||
175
+ areas.has('rspack-plugin') ||
176
+ areas.has('rsbuild-plugin') ||
177
+ areas.has('vite-plugin') ||
178
+ areas.has('deploy-adapter')
179
+ ) {
180
+ commands.add('pnpm typecheck');
181
+ }
182
+ if (areas.has('benchmark') || taskKind === 'performance' || taskKind === 'core') {
183
+ commands.add('node benchmarks/bench.mjs --quick --ratios');
184
+ }
107
185
  if (taskKind === 'api' || taskKind === 'core' || taskKind === 'package')
108
186
  commands.add('pnpm typecheck');
109
187
  commands.add('pnpm format:check');
@@ -111,6 +189,74 @@ export function validationFor(paths, taskKind) {
111
189
  return [...commands];
112
190
  }
113
191
 
192
+ export function engineeringPlanFor(input, repoMode = false) {
193
+ const paths = input.paths ?? [];
194
+ const scope = input.scope;
195
+ const changeKind = input.changeKind ?? 'feature';
196
+ const performanceSensitive =
197
+ scope === 'framework-core' ||
198
+ changeKind === 'performance' ||
199
+ input.performanceSensitive === true;
200
+ const correctnessGate =
201
+ changeKind === 'bug'
202
+ ? 'Reproduce the bug through a realistic public boundary and verify that the test has a credible pre-fix failure.'
203
+ : 'Protect the new or changed behavior through a realistic public boundary with an assertion that would fail if the contract were absent.';
204
+ const plan = {
205
+ scope,
206
+ changeKind,
207
+ performanceSensitive,
208
+ areas: paths.map((path) => ({ path, area: areaForPath(path) })),
209
+ requiredSkills: ['build-octane-software'],
210
+ gates: {
211
+ contract: [
212
+ 'State the consumer-observable behavior, invariants, failure states, and supported execution modes.',
213
+ 'Inspect current source, callers, tests, configuration, and documented Octane divergences before editing.',
214
+ ],
215
+ correctness: [
216
+ correctnessGate,
217
+ 'Exercise applicable empty, large, repeated, nested, error, abort, cleanup, production, SSR, and hydration cases.',
218
+ ],
219
+ performance: performanceSensitive
220
+ ? [
221
+ 'Identify hot paths and record a relevant baseline before editing.',
222
+ 'Compare baseline and candidate with the same environment, warmup, iterations, and semantic controls.',
223
+ 'Inspect allocations, retained memory, DOM work, compiler and generated-code cost, SSR/hydration work, and bundle size as applicable.',
224
+ 'Do not claim improvement when the delta is within noise or no trustworthy measurement exists.',
225
+ ]
226
+ : [
227
+ 'Check that the change does not add unnecessary reactive work, retained state, dependencies, or common-path cost.',
228
+ 'Measure the important user journey when the change can materially affect it.',
229
+ ],
230
+ selfReview: [
231
+ 'Read the complete diff adversarially and try to falsify the solution with boundary and lifecycle cases.',
232
+ 'Trace new state and allocations through invalidation, cleanup, errors, and aborts.',
233
+ 'Compare with a simpler design and remove complexity that does not justify its permanent cost.',
234
+ 'Resolve findings, rerun affected checks, and repeat the review on the final diff.',
235
+ ],
236
+ handoff: [
237
+ 'Report the protected contract, validation commands and results, and applicable baseline/candidate measurements.',
238
+ 'Report improvements made during self-review, untested modes, inconclusive evidence, and residual risk.',
239
+ ],
240
+ },
241
+ };
242
+
243
+ if (scope === 'framework-core' && repoMode) {
244
+ plan.requiredSkills.push('octane-core-extend', 'performance-audit');
245
+ }
246
+ if (scope === 'framework-core' && !repoMode) {
247
+ plan.blockingConditions = [
248
+ 'Framework-core work requires the MCP server to run against an Octane monorepo checkout. Set OCTANE_REPO_ROOT, reconnect, and request this plan again so maintainer skills and repository validation are available.',
249
+ ];
250
+ }
251
+ if (repoMode) {
252
+ const taskKind =
253
+ scope === 'framework-core' ? 'core' : performanceSensitive ? 'performance' : changeKind;
254
+ plan.validationCommands = validationFor(paths, taskKind);
255
+ }
256
+
257
+ return plan;
258
+ }
259
+
114
260
  export function runCommand(command, args, options = {}) {
115
261
  const cwd = options.cwd ?? process.cwd();
116
262
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -172,15 +318,14 @@ export async function scaffoldReactPort(repoRoot, input) {
172
318
  }
173
319
 
174
320
  export async function runBenchmark(repoRoot, input) {
175
- const args =
176
- input.benchmark === 'all'
177
- ? ['bench']
178
- : ['--filter', BENCHMARKS[input.benchmark], 'run', 'bench'];
179
- const result = await runCommand('pnpm', args, {
321
+ const args = ['benchmarks/bench.mjs'];
322
+ if (input.benchmark && input.benchmark !== 'all') args.push(input.benchmark);
323
+ if (input.quick) args.push('--quick');
324
+ const result = await runCommand(process.execPath, args, {
180
325
  cwd: repoRoot,
181
- timeoutMs: input.timeoutMs ?? 300_000,
326
+ timeoutMs: input.timeoutMs ?? 600_000,
182
327
  });
183
- return { command: ['pnpm', ...args], benchmark: input.benchmark, ...result };
328
+ return { command: [process.execPath, ...args], benchmark: input.benchmark, ...result };
184
329
  }
185
330
 
186
331
  export async function issueContext(repoRoot, input) {
@@ -236,7 +381,7 @@ function registerUserTools(server, repoRoot, repoMode) {
236
381
  {
237
382
  title: 'Octane skill',
238
383
  description:
239
- 'Return an Octane agent skill by name. Bundled skills cover working WITH octane in any project: bridging React packages, migrating React components to .tsrx, intentional React divergences, and SSR setup.' +
384
+ 'Return an Octane agent skill by name. Load build-octane-software before creating or materially changing Octane code; other bundled skills cover React package bridges, component migration, intentional divergences, and SSR setup.' +
240
385
  (repoMode ? ' Repo skills cover octane maintainer workflows.' : ''),
241
386
  inputSchema: {
242
387
  name: z.enum(Object.keys(skills)),
@@ -249,12 +394,33 @@ function registerUserTools(server, repoRoot, repoMode) {
249
394
  },
250
395
  );
251
396
 
397
+ server.registerTool(
398
+ 'octane_engineering_plan',
399
+ {
400
+ title: 'Plan high-quality Octane engineering work',
401
+ description:
402
+ 'Return required correctness, performance-evidence, adversarial self-review, and handoff gates before creating or materially changing Octane software. Framework-core scope always requires baseline/candidate performance evidence and the maintainer core/performance skills.',
403
+ inputSchema: {
404
+ scope: z.enum(['application', 'library', 'framework-core']),
405
+ changeKind: z
406
+ .enum(['bug', 'feature', 'performance', 'refactor', 'api', 'docs', 'test', 'unknown'])
407
+ .default('feature'),
408
+ paths: z.array(z.string()).default([]).describe('Repository-relative changed paths.'),
409
+ performanceSensitive: z
410
+ .boolean()
411
+ .optional()
412
+ .describe('Force performance evidence for application or library work.'),
413
+ },
414
+ },
415
+ async (input) => text(JSON.stringify(engineeringPlanFor(input, repoMode), null, 2)),
416
+ );
417
+
252
418
  server.registerTool(
253
419
  'octane_bridge_react_package',
254
420
  {
255
421
  title: 'Bridge a React package to Octane',
256
422
  description:
257
- 'Scan a React package (from node_modules by name, or any source directory by path) for React API usage and return an Octane compatibility report: which APIs map 1:1, which need rewrites (forwardRef, useDebugValue, lazy, class components), whether a framework-agnostic core can be reused verbatim, whether an official @octanejs binding already exists, and a step-by-step bridge plan. Follow up with the bridge-react-package skill for the full workflow.',
423
+ 'Scan a React package (from node_modules by name, or any source directory by path) for React API usage and return an Octane compatibility report: which APIs map 1:1, which need rewrites (forwardRef, class components, React-style text-host onChange, react-dom/server imports), whether a framework-agnostic core can be reused verbatim, whether an official @octanejs binding already exists, and a step-by-step bridge plan. Follow up with the bridge-react-package skill for the full workflow.',
258
424
  inputSchema: {
259
425
  package: z
260
426
  .string()
@@ -379,9 +545,10 @@ function registerRepoTools(server, repoRoot) {
379
545
  {
380
546
  title: 'Run Octane benchmark',
381
547
  description:
382
- 'Run one of the known Octane benchmark workspaces, or all benchmarks via pnpm bench.',
548
+ 'Run benchmark suites through the unified runner (node benchmarks/bench.mjs): one manifest suite by name, or every suite with "all". Set quick for the reduced-iteration smoke pass.',
383
549
  inputSchema: {
384
- benchmark: z.enum(['all', ...Object.keys(BENCHMARKS)]).default('all'),
550
+ benchmark: z.enum(['all', ...BENCHMARK_SUITES]).default('all'),
551
+ quick: z.boolean().default(false),
385
552
  timeoutMs: z.number().int().positive().optional(),
386
553
  },
387
554
  },
@@ -408,7 +575,10 @@ function registerRepoTools(server, repoRoot) {
408
575
  export function createServer(options = {}) {
409
576
  const repoRoot = resolve(options.repoRoot || process.env.OCTANE_REPO_ROOT || process.cwd());
410
577
  const repoMode = isOctaneRepo(repoRoot);
411
- const server = new McpServer({ name: 'octane', version: '0.2.0' });
578
+ const server = new McpServer(
579
+ { name: 'octane', version: '0.2.0' },
580
+ { instructions: instructionsFor(repoMode) },
581
+ );
412
582
  registerUserTools(server, repoRoot, repoMode);
413
583
  if (repoMode) registerRepoTools(server, repoRoot);
414
584
  return server;
package/src/index.test.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
2
4
  import { existsSync } from 'node:fs';
3
5
  import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
4
6
  import { tmpdir } from 'node:os';
@@ -6,11 +8,16 @@ import { dirname, join, resolve } from 'node:path';
6
8
  import { fileURLToPath } from 'node:url';
7
9
  import {
8
10
  areaForPath,
11
+ BENCHMARK_SUITES,
9
12
  BUNDLED_SKILLS,
13
+ createServer,
14
+ engineeringPlanFor,
10
15
  isOctaneRepo,
16
+ runCommand,
11
17
  scaffoldReactPort,
12
18
  validationFor,
13
19
  } from './index.js';
20
+ import { KNOWN_BINDING_PACKAGE_DIRS } from './bridge.js';
14
21
 
15
22
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
16
23
 
@@ -22,10 +29,63 @@ describe('@octanejs/mcp-server helpers', () => {
22
29
  expect(areaForPath('packages/zustand/src/index.ts')).toBe('ecosystem-binding');
23
30
  expect(areaForPath('packages/radix/src/index.ts')).toBe('ecosystem-binding');
24
31
  expect(areaForPath('packages/octane-mcp-server/src/index.js')).toBe('mcp-server');
32
+ expect(areaForPath('packages/adapter-vercel/src/index.ts')).toBe('deploy-adapter');
33
+ expect(areaForPath('packages/octane-evals/tools/run.mjs')).toBe('evals');
34
+ expect(areaForPath('website/src/pages/index.tsrx')).toBe('website');
25
35
  expect(areaForPath('benchmarks/news/run.mjs')).toBe('benchmark');
26
36
  expect(areaForPath('.rulesync/rules/project.md')).toBe('rulesync-source');
27
37
  });
28
38
 
39
+ it('recommends the adapter, evals, and website test projects', () => {
40
+ const commands = validationFor(
41
+ [
42
+ 'packages/adapter-vercel/src/index.ts',
43
+ 'packages/octane-evals/tools/run.mjs',
44
+ 'website/src/pages/index.tsrx',
45
+ ],
46
+ 'feature',
47
+ );
48
+
49
+ expect(commands).toContain(
50
+ './node_modules/.bin/vitest run packages/adapter-vercel/tests --project adapter-vercel',
51
+ );
52
+ expect(commands).toContain(
53
+ './node_modules/.bin/vitest run packages/octane-evals/tests --project octane-evals',
54
+ );
55
+ expect(commands).toContain('./node_modules/.bin/vitest run website/tests --project website');
56
+ expect(commands).toContain('pnpm typecheck');
57
+ });
58
+
59
+ it('keeps the benchmark suite list in sync with the unified runner manifest', async () => {
60
+ // BENCHMARK_SUITES is hand-maintained in index.js; the runner manifest in
61
+ // benchmarks/bench.mjs is the source of truth. --list prints one suite
62
+ // name per line, in manifest order.
63
+ const repoRoot = resolve(PACKAGE_ROOT, '../..');
64
+ const result = await runCommand(process.execPath, ['benchmarks/bench.mjs', '--list'], {
65
+ cwd: repoRoot,
66
+ });
67
+ expect(result.code).toBe(0);
68
+ const suites = result.stdout
69
+ .split('\n')
70
+ .map((line) => line.trim())
71
+ .filter((line) => line && line !== 'Available suites:');
72
+ expect(suites).toEqual(BENCHMARK_SUITES);
73
+ });
74
+
75
+ it('classifies every maintained binding and recommends its test project', () => {
76
+ const paths = [...KNOWN_BINDING_PACKAGE_DIRS].map(
77
+ (directory) => `packages/${directory}/src/index.ts`,
78
+ );
79
+ const commands = validationFor(paths, 'binding');
80
+
81
+ for (const directory of KNOWN_BINDING_PACKAGE_DIRS) {
82
+ expect(areaForPath(`packages/${directory}/src/index.ts`)).toBe('ecosystem-binding');
83
+ expect(commands).toContain(
84
+ `./node_modules/.bin/vitest run packages/${directory}/tests --project ${directory}`,
85
+ );
86
+ }
87
+ });
88
+
29
89
  it('recommends validation commands from changed paths', () => {
30
90
  const commands = validationFor(
31
91
  [
@@ -48,9 +108,86 @@ describe('@octanejs/mcp-server helpers', () => {
48
108
  './node_modules/.bin/vitest run packages/radix/tests --project radix',
49
109
  );
50
110
  expect(commands).toContain('pnpm typecheck');
111
+ expect(commands).toContain('node benchmarks/bench.mjs --quick --ratios');
51
112
  expect(commands).toContain('pnpm format:check');
52
113
  });
53
114
 
115
+ it('requires performance evidence and adversarial review for framework fundamentals', () => {
116
+ const plan = engineeringPlanFor(
117
+ {
118
+ scope: 'framework-core',
119
+ changeKind: 'refactor',
120
+ paths: ['packages/octane/src/runtime.ts'],
121
+ },
122
+ true,
123
+ );
124
+
125
+ expect(plan.performanceSensitive).toBe(true);
126
+ expect(plan.requiredSkills).toEqual([
127
+ 'build-octane-software',
128
+ 'octane-core-extend',
129
+ 'performance-audit',
130
+ ]);
131
+ expect(plan.gates.performance).toContain(
132
+ 'Identify hot paths and record a relevant baseline before editing.',
133
+ );
134
+ expect(plan.gates.selfReview).toContain(
135
+ 'Resolve findings, rerun affected checks, and repeat the review on the final diff.',
136
+ );
137
+ expect(plan.validationCommands).toContain('node benchmarks/bench.mjs --quick --ratios');
138
+ });
139
+
140
+ it('blocks framework-core plans when maintainer tools are unavailable', () => {
141
+ const plan = engineeringPlanFor({ scope: 'framework-core', changeKind: 'bug' });
142
+
143
+ expect(plan.requiredSkills).toEqual(['build-octane-software']);
144
+ expect(plan.blockingConditions).toContain(
145
+ 'Framework-core work requires the MCP server to run against an Octane monorepo checkout. Set OCTANE_REPO_ROOT, reconnect, and request this plan again so maintainer skills and repository validation are available.',
146
+ );
147
+ expect(plan.gates.correctness).toContain(
148
+ 'Reproduce the bug through a realistic public boundary and verify that the test has a credible pre-fix failure.',
149
+ );
150
+ });
151
+
152
+ it('keeps performance gates and validation commands aligned', () => {
153
+ const performancePlan = engineeringPlanFor(
154
+ { scope: 'application', changeKind: 'performance' },
155
+ true,
156
+ );
157
+ const flaggedPlan = engineeringPlanFor(
158
+ { scope: 'library', changeKind: 'feature', performanceSensitive: true },
159
+ true,
160
+ );
161
+
162
+ for (const plan of [performancePlan, flaggedPlan]) {
163
+ expect(plan.performanceSensitive).toBe(true);
164
+ expect(plan.gates.performance).toContain(
165
+ 'Identify hot paths and record a relevant baseline before editing.',
166
+ );
167
+ expect(plan.validationCommands).toContain('node benchmarks/bench.mjs --quick --ratios');
168
+ }
169
+ });
170
+
171
+ it('advertises the engineering gates during MCP initialization', async () => {
172
+ const server = createServer({ repoRoot: resolve(PACKAGE_ROOT, '../..') });
173
+ const client = new Client({ name: 'octane-mcp-test', version: '1.0.0' });
174
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
175
+
176
+ try {
177
+ await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
178
+ expect(client.getInstructions()).toContain(
179
+ 'Before creating or materially changing Octane software',
180
+ );
181
+ expect(client.getInstructions()).toContain('establish a relevant baseline before editing');
182
+
183
+ const tools = await client.listTools();
184
+ expect(tools.tools.map((tool) => tool.name)).toContain('octane_engineering_plan');
185
+ } finally {
186
+ await client.close();
187
+ await server.close();
188
+ }
189
+ });
190
+
54
191
  it('detects the octane monorepo for repo-mode tools', async () => {
55
192
  expect(isOctaneRepo(resolve(PACKAGE_ROOT, '../..'))).toBe(true);
56
193
  const elsewhere = await mkdtemp(join(tmpdir(), 'octane-mcp-test-'));