@octanejs/mcp-server 0.2.6 → 0.2.9

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/README.md CHANGED
@@ -57,11 +57,12 @@ maintainer tools):
57
57
 
58
58
  ## Tools (always available)
59
59
 
60
- The server initialization instructions direct coding agents to call
61
- `octane_engineering_plan` and load `build-octane-software` before creating or
62
- materially changing Octane code. This makes the correctness, performance
63
- evidence, adversarial self-review, and handoff requirements available even when
64
- the host does not automatically discover skills.
60
+ The server initialization instructions are one orienting sentence plus a pointer
61
+ to `octane_engineering_plan`, because they are injected into every session
62
+ whether or not it touches Octane. The correctness, performance-evidence,
63
+ self-review, and handoff requirements live in that tool response, so they stay
64
+ reachable even on hosts that do not discover skills. Each tool description says
65
+ when to call it.
65
66
 
66
67
  ### `octane_engineering_plan`
67
68
 
@@ -112,24 +113,30 @@ without registering its React-package mapping fails CI.
112
113
 
113
114
  Returns a skill by name. Bundled skills (shipped with this package):
114
115
 
115
- - `bridge-react-package` the full workflow for porting a React library.
116
- - `build-octane-software` production engineering, performance, validation,
116
+ - `bridge-react-package`: the full workflow for porting a React library.
117
+ - `build-octane-software`: production engineering, performance, validation,
117
118
  and adversarial self-review gates for Octane code.
118
- - `migrate-react-component` React JSX to `.tsrx` conversion reference.
119
- - `react-divergences` Octane's intentional differences from React.
120
- - `setup-ssr` server rendering and hydration setup.
119
+ - `migrate-react-component`: React JSX to `.tsrx` conversion reference.
120
+ - `react-divergences`: Octane's intentional differences from React.
121
+ - `setup-ssr`: server rendering and hydration setup.
121
122
 
122
- When running inside the octane monorepo, the maintainer skills from
123
- `.ai/skills` are also available: `react-library-port`, `bug-hunter`,
123
+ When running inside the octane monorepo, the skills from `.rulesync/skills` are
124
+ also available: `authoring-tsrx`, `react-library-port`, `bug-hunter`,
124
125
  `create-a-pr`, `handle-issue`, `octane-core-extend`, `triage`,
125
- `performance-audit`.
126
+ `performance-audit`. A test compares this map against the directory in both
127
+ directions, so a new skill cannot stay unreachable here. This tool reads the RuleSync source, and
128
+ `pnpm rules:generate` writes the per-agent copies (`.claude/skills/`,
129
+ `.github/skills/`, `.cursor/skills/`, `.gemini/skills/`) from the same text, so
130
+ hosts that discover skills natively and hosts that call this tool see the same
131
+ thing.
126
132
 
127
133
  ## Tools (octane monorepo only)
128
134
 
129
135
  ### `octane_project_map`
130
136
 
131
- Returns `.ai/project-map.md` with package layout, authoritative sources,
132
- invariants, and validation commands.
137
+ Returns `AGENTS.md`: the RuleSync-generated root rule, covering what Octane is,
138
+ which source owns which behavior, the intentional divergences from React, and the
139
+ validation commands. CI fails if it drifts from its source in `.rulesync/rules/`.
133
140
 
134
141
  ### `octane_triage_paths`
135
142
 
@@ -152,11 +159,14 @@ optionally writes the generated Vitest skeleton to an output file.
152
159
 
153
160
  Runs benchmark suites through the unified runner (`node benchmarks/bench.mjs`):
154
161
  one manifest suite by name (`js-framework`, `todomvc`, `weather-app`,
155
- `weather-app-lighthouse`, `chat-stream`, `dbmon`, `news`, `react-hosted-islands`,
156
- `ssr-throughput`, `streaming-ssr`,
157
- `codegen-size`, `bundle-size`, `three-renderer`, `three-bundle-size`, …) or every
158
- suite with `all`; `quick` selects the reduced-iteration smoke pass. The suite
159
- list mirrors the runner manifest and `node benchmarks/bench.mjs --list`.
162
+ `hydration-interactivity`, `hydration-stress`, `lifecycle-memory`,
163
+ `controlled-form`, `external-store-fanout`, `external-store-integrations`,
164
+ `scheduler-responsiveness`, `suspense-recovery`, `event-delegation`,
165
+ `application-composition`, `scaling-curves`, `streaming-ssr`,
166
+ `streaming-backpressure`, `compiler-throughput`, `codegen-size`,
167
+ `bundle-size`, `three-renderer`, `three-bundle-size`, …) or every suite with
168
+ `all`; `quick` selects the reduced-iteration smoke pass. The suite list mirrors
169
+ the runner manifest and `node benchmarks/bench.mjs --list`.
160
170
 
161
171
  ### `octane_issue_context`
162
172
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/mcp-server",
3
- "version": "0.2.6",
3
+ "version": "0.2.9",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -36,7 +36,7 @@
36
36
  "zod": "^4.4.3"
37
37
  },
38
38
  "devDependencies": {
39
- "vitest": "^4.1.9"
39
+ "vitest": "^4.1.10"
40
40
  },
41
41
  "scripts": {
42
42
  "start": "node src/index.js",
@@ -38,7 +38,7 @@ locals, early returns) stays above it.
38
38
  | `items.map(x => <li key={x.id}>...` | `@for (const x of items; key x.id) { <li>... }` with optional `@empty { }` |
39
39
  | `cond ? <A/> : <B/>` in JSX | `@if (cond) { <A/> } @else { <B/> }` |
40
40
  | `{cond && <A/>}` | `@if (cond) { <A/> }` |
41
- | switch on a value | `@switch (v) { @case (a) { } @default { } }` |
41
+ | switch on a value | `@switch (v) { @case a: { } @default: { } }` |
42
42
  | `<Suspense fallback={...}>` | `<Suspense>` or `@try { } @pending { }` |
43
43
  | Error boundary class | `<ErrorBoundary>` or `@try { } @catch (e) { }` |
44
44
  | `forwardRef((props, ref) => ...)` | plain function; `ref` arrives as a prop |
package/src/bridge.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import { readdir, readFile } from 'node:fs/promises';
2
2
  import { join, resolve } from 'node:path';
3
3
 
4
- // React package → maintained @octanejs binding. bridge.test.js derives the
5
- // expected value set from the workspace manifests, so publishing a new binding
6
- // without registering it here fails the mcp-server tests.
4
+ // React package → maintained @octanejs binding. Bindings with no React-package
5
+ // equivalent live in KNOWN_NATIVE_BINDINGS below. bridge.test.js derives the
6
+ // expected union from the workspace manifests, so publishing a new binding
7
+ // without registering it in either catalog fails the mcp-server tests.
7
8
  export const KNOWN_BINDINGS = {
8
9
  zustand: '@octanejs/zustand',
10
+ valtio: '@octanejs/valtio',
9
11
  jotai: '@octanejs/jotai',
10
12
  '@apollo/client': '@octanejs/apollo-client',
11
13
  '@tanstack/ai-react': '@octanejs/tanstack-ai',
@@ -14,6 +16,9 @@ export const KNOWN_BINDINGS = {
14
16
  '@tanstack/react-query': '@octanejs/tanstack-query',
15
17
  '@tanstack/react-router': '@octanejs/tanstack-router',
16
18
  '@tanstack/react-store': '@octanejs/tanstack-store',
19
+ '@tanstack/react-router-ssr-query': '@octanejs/tanstack-router-ssr-query',
20
+ '@tanstack/react-hotkeys': '@octanejs/tanstack-hotkeys',
21
+ '@tanstack/react-pacer': '@octanejs/tanstack-pacer',
17
22
  '@tanstack/react-table': '@octanejs/tanstack-table',
18
23
  '@tanstack/react-virtual': '@octanejs/tanstack-virtual',
19
24
  'framer-motion': '@octanejs/motion',
@@ -23,6 +28,7 @@ export const KNOWN_BINDINGS = {
23
28
  'react-router': '@octanejs/remix-router',
24
29
  'react-router-dom': '@octanejs/remix-router',
25
30
  nuqs: '@octanejs/nuqs',
31
+ cmdk: '@octanejs/cmdk',
26
32
  '@lexical/react': '@octanejs/lexical',
27
33
  '@tiptap/react': '@octanejs/tiptap',
28
34
  'lucide-react': '@octanejs/lucide',
@@ -35,6 +41,7 @@ export const KNOWN_BINDINGS = {
35
41
  '@base-ui-components/react': '@octanejs/base-ui',
36
42
  '@dnd-kit/react': '@octanejs/dnd-kit',
37
43
  sonner: '@octanejs/sonner',
44
+ shadcn: '@octanejs/shadcn',
38
45
  recharts: '@octanejs/recharts',
39
46
  '@react-three/fiber': '@octanejs/three',
40
47
  '@visx/visx': '@octanejs/visx',
@@ -94,11 +101,18 @@ export const KNOWN_BINDINGS = {
94
101
  'dexie-react-hooks': '@octanejs/dexie',
95
102
  };
96
103
 
104
+ // Octane-specific ecosystem packages that have no React import to rewrite.
105
+ // Keep these out of KNOWN_BINDINGS so the React bridge never invents a source
106
+ // package mapping for native tooling.
107
+ export const KNOWN_NATIVE_BINDINGS = new Set(['@octanejs/devtools', '@octanejs/tauri']);
108
+
97
109
  // Workspace directory names for the maintained bindings. Keep this derived
98
- // from KNOWN_BINDINGS so repository path routing cannot drift from the public
110
+ // from both catalogs so repository path routing cannot drift from the public
99
111
  // binding list (aliases such as `motion` intentionally collapse to one dir).
100
112
  export const KNOWN_BINDING_PACKAGE_DIRS = new Set(
101
- Object.values(KNOWN_BINDINGS).map((name) => name.slice('@octanejs/'.length)),
113
+ [...Object.values(KNOWN_BINDINGS), ...KNOWN_NATIVE_BINDINGS].map((name) =>
114
+ name.slice('@octanejs/'.length),
115
+ ),
102
116
  );
103
117
 
104
118
  export const KNOWN_VANILLA_CORES = {
@@ -10,6 +10,7 @@ import {
10
10
  detectVanillaCore,
11
11
  scanSource,
12
12
  KNOWN_BINDINGS,
13
+ KNOWN_NATIVE_BINDINGS,
13
14
  KNOWN_BINDING_PACKAGE_DIRS,
14
15
  } from './bridge.js';
15
16
 
@@ -267,7 +268,7 @@ describe('KNOWN_BINDINGS', () => {
267
268
  // metaframeworks and infrastructure cannot drift into the binding catalog.
268
269
  const bindings = getBindingPackages();
269
270
  expect(bindings.length).toBeGreaterThan(0);
270
- expect(new Set(Object.values(KNOWN_BINDINGS))).toEqual(
271
+ expect(new Set([...Object.values(KNOWN_BINDINGS), ...KNOWN_NATIVE_BINDINGS])).toEqual(
271
272
  new Set(bindings.map(({ name }) => name)),
272
273
  );
273
274
  expect(KNOWN_BINDING_PACKAGE_DIRS).toEqual(new Set(bindings.map(({ dir }) => dir)));
package/src/index.js CHANGED
@@ -13,9 +13,10 @@ const __filename = fileURLToPath(import.meta.url);
13
13
  const PACKAGE_ROOT = resolve(dirname(__filename), '..');
14
14
 
15
15
  // Bundled skills ship with the npm package and work in ANY project using
16
- // octane. Repo skills live in the octane monorepo's .ai/skills and are only
17
- // available when the server runs against a checkout (they cover maintainer
18
- // workflows: triage, PRs, core changes).
16
+ // octane. Repo skills are read from the RuleSync source, not a generated copy,
17
+ // so this server and every per-agent output (.claude/skills, .github/skills,
18
+ // …) serve the same text. They cover maintainer workflows: triage, PRs, core
19
+ // changes, and are only available when the server runs against a checkout.
19
20
  export const BUNDLED_SKILLS = {
20
21
  'bridge-react-package': 'skills/bridge-react-package.md',
21
22
  'build-octane-software': 'skills/build-octane-software.md',
@@ -25,13 +26,14 @@ export const BUNDLED_SKILLS = {
25
26
  };
26
27
 
27
28
  export const REPO_SKILLS = {
28
- 'bug-hunter': '.ai/skills/bug-hunter.md',
29
- 'create-a-pr': '.ai/skills/create-a-pr.md',
30
- 'handle-issue': '.ai/skills/handle-issue.md',
31
- 'octane-core-extend': '.ai/skills/octane-core-extend.md',
32
- 'performance-audit': '.ai/skills/performance-audit.md',
33
- 'react-library-port': '.ai/skills/react-library-port.md',
34
- triage: '.ai/skills/triage.md',
29
+ 'authoring-tsrx': '.rulesync/skills/authoring-tsrx/SKILL.md',
30
+ 'bug-hunter': '.rulesync/skills/bug-hunter/SKILL.md',
31
+ 'create-a-pr': '.rulesync/skills/create-a-pr/SKILL.md',
32
+ 'handle-issue': '.rulesync/skills/handle-issue/SKILL.md',
33
+ 'octane-core-extend': '.rulesync/skills/octane-core-extend/SKILL.md',
34
+ 'performance-audit': '.rulesync/skills/performance-audit/SKILL.md',
35
+ 'react-library-port': '.rulesync/skills/react-library-port/SKILL.md',
36
+ triage: '.rulesync/skills/triage/SKILL.md',
35
37
  };
36
38
 
37
39
  // Suite names from the unified runner manifest (`SUITES` in
@@ -48,13 +50,27 @@ export const BENCHMARK_SUITES = [
48
50
  'recursive-context',
49
51
  'signal-favoring',
50
52
  'news',
53
+ 'hydration-interactivity',
54
+ 'hydration-stress',
55
+ 'lifecycle-memory',
56
+ 'controlled-form',
57
+ 'external-store-fanout',
58
+ 'external-store-integrations',
59
+ 'scheduler-responsiveness',
60
+ 'suspense-recovery',
61
+ 'event-delegation',
62
+ 'application-composition',
63
+ 'scaling-curves',
64
+ 'store-selector-fanout',
51
65
  'effectful-list',
66
+ 'list-clear',
52
67
  'memo-wall',
53
68
  'portal-swarm',
54
69
  'react-hosted-islands',
55
70
  'ssr-throughput',
56
71
  'streaming-ssr',
57
72
  'ssr-http',
73
+ 'streaming-backpressure',
58
74
  'ssr-workerd',
59
75
  'tanstack-start',
60
76
  'dbmon-deopt',
@@ -62,7 +78,10 @@ export const BENCHMARK_SUITES = [
62
78
  'async-waterfall',
63
79
  'async-composition',
64
80
  'lynx-list',
81
+ 'lynx-render',
82
+ 'lynx-bundle-size',
65
83
  'codegen-size',
84
+ 'compiler-throughput',
66
85
  'bundle-size',
67
86
  'three-renderer',
68
87
  'three-bundle-size',
@@ -71,10 +90,15 @@ export const BENCHMARK_SUITES = [
71
90
  const DEFAULT_TIMEOUT_MS = 120_000;
72
91
 
73
92
  export function instructionsFor(repoMode) {
93
+ // This text is injected into every session of every connected agent, whether
94
+ // or not it touches Octane, so it stays one orienting sentence plus one
95
+ // pointer. The gates themselves live in octane_engineering_plan, and each
96
+ // tool's own description says when to call it: that is where a model looks
97
+ // when it is actually deciding.
74
98
  const common =
75
- '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.';
99
+ "Octane is a compiler-first UI framework with React's programming model, so React habits are the main source of wrong changes here. Before creating or materially changing Octane code, call octane_engineering_plan: it returns the gates that apply and names the skills to load.";
76
100
  return repoMode
77
- ? `${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.`
101
+ ? `${common} Inside this monorepo checkout it also returns the repository validation commands for the paths you changed.`
78
102
  : common;
79
103
  }
80
104
 
@@ -108,7 +132,7 @@ export function areaForPath(path) {
108
132
  if (path.startsWith('benchmarks/')) return 'benchmark';
109
133
  if (path.startsWith('website/')) return 'website';
110
134
  if (path.startsWith('.rulesync/')) return 'rulesync-source';
111
- if (path.startsWith('.ai/') || path.startsWith('.codex/') || path.startsWith('.claude/')) {
135
+ if (path.startsWith('.codex/') || path.startsWith('.claude/')) {
112
136
  return 'agent-instructions';
113
137
  }
114
138
  if (path.startsWith('docs/') || path.endsWith('.md')) return 'docs';
@@ -390,7 +414,7 @@ function registerUserTools(server, repoRoot, repoMode) {
390
414
  {
391
415
  title: 'Octane skill',
392
416
  description:
393
- '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.' +
417
+ 'Fetch an Octane agent skill by name. Call when starting work the skill covers: build-octane-software for engineering gates, bridge-react-package for porting a React library, migrate-react-component for JSX to .tsrx, react-divergences before assuming React behavior, setup-ssr for server rendering.' +
394
418
  (repoMode ? ' Repo skills cover octane maintainer workflows.' : ''),
395
419
  inputSchema: {
396
420
  name: z.enum(Object.keys(skills)),
@@ -476,12 +500,19 @@ function registerRepoTools(server, repoRoot) {
476
500
  {
477
501
  title: 'Octane project map',
478
502
  description:
479
- 'Return Octane repository map, source ownership, validation commands, and skill paths.',
503
+ 'Call when you need Octane repo orientation: what the framework is, which source owns which behavior, the intentional divergences from React, the validation commands, and the full package inventory.',
480
504
  inputSchema: {},
481
505
  },
482
506
  async () => {
483
- const projectMap = await readFile(resolve(repoRoot, '.ai/project-map.md'), 'utf8');
484
- return text(projectMap);
507
+ // Composed from the two generated sources rather than a hand-written
508
+ // summary, because both are CI-gated: AGENTS.md against .rulesync/rules,
509
+ // and packages.md against the workspace manifests. A third restatement
510
+ // would be the one free to drift.
511
+ const [rootRule, packages] = await Promise.all([
512
+ readFile(resolve(repoRoot, 'AGENTS.md'), 'utf8'),
513
+ readFile(resolve(repoRoot, 'docs/packages.md'), 'utf8'),
514
+ ]);
515
+ return text(`${rootRule.trimEnd()}\n\n---\n\n${packages.trimStart()}`);
485
516
  },
486
517
  );
487
518
 
@@ -489,7 +520,8 @@ function registerRepoTools(server, repoRoot) {
489
520
  'octane_triage_paths',
490
521
  {
491
522
  title: 'Triage Octane paths',
492
- description: 'Classify changed paths by Octane repo area.',
523
+ description:
524
+ 'Call before editing unfamiliar paths in this monorepo to learn which area owns them.',
493
525
  inputSchema: {
494
526
  paths: z.array(z.string()).describe('Repository-relative paths'),
495
527
  },
@@ -504,7 +536,8 @@ function registerRepoTools(server, repoRoot) {
504
536
  'octane_validate_plan',
505
537
  {
506
538
  title: 'Octane validation plan',
507
- description: 'Recommend validation commands for changed paths and task kind.',
539
+ description:
540
+ 'Call when a change is finished to get the validation commands that cover the paths you touched.',
508
541
  inputSchema: {
509
542
  paths: z.array(z.string()).default([]).describe('Repository-relative changed paths'),
510
543
  taskKind: z
@@ -534,7 +567,8 @@ function registerRepoTools(server, repoRoot) {
534
567
  'octane_scaffold_react_port',
535
568
  {
536
569
  title: 'Scaffold React test port',
537
- description: 'Run scripts/scaffold-react-port.mjs for a React upstream test file.',
570
+ description:
571
+ 'Call when porting a React test file into the conformance suite, returns a triage skeleton of in-scope cases and out-of-scope reasons.',
538
572
  inputSchema: {
539
573
  reactTestFile: z
540
574
  .string()
package/src/index.test.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
3
  import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
4
- import { existsSync } from 'node:fs';
4
+ import { existsSync, readdirSync } from 'node:fs';
5
5
  import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { dirname, join, resolve } from 'node:path';
@@ -10,6 +10,7 @@ import {
10
10
  areaForPath,
11
11
  BENCHMARK_SUITES,
12
12
  BUNDLED_SKILLS,
13
+ REPO_SKILLS,
13
14
  createServer,
14
15
  engineeringPlanFor,
15
16
  isOctaneRepo,
@@ -173,20 +174,50 @@ describe('@octanejs/mcp-server helpers', () => {
173
174
  }
174
175
  });
175
176
 
176
- it('advertises the engineering gates during MCP initialization', async () => {
177
+ it('routes to the engineering gates without restating them at initialization', async () => {
178
+ // Initialization instructions are injected into every session, so they stay
179
+ // short and point at the tool. The gates themselves must still be reachable
180
+ //: that is the contract, not any particular wording.
177
181
  const server = createServer({ repoRoot: resolve(PACKAGE_ROOT, '../..') });
178
182
  const client = new Client({ name: 'octane-mcp-test', version: '1.0.0' });
179
183
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
180
184
 
181
185
  try {
182
186
  await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
183
- expect(client.getInstructions()).toContain(
184
- 'Before creating or materially changing Octane software',
185
- );
186
- expect(client.getInstructions()).toContain('establish a relevant baseline before editing');
187
+ const instructions = client.getInstructions();
188
+ expect(instructions).toContain('octane_engineering_plan');
189
+ expect(instructions.length).toBeLessThan(600);
187
190
 
188
191
  const tools = await client.listTools();
189
192
  expect(tools.tools.map((tool) => tool.name)).toContain('octane_engineering_plan');
193
+
194
+ const plan = await client.callTool({
195
+ name: 'octane_engineering_plan',
196
+ arguments: { scope: 'framework-core', changeKind: 'performance' },
197
+ });
198
+ const body = plan.content[0].text;
199
+ expect(body).toContain('Identify hot paths and record a relevant baseline before editing.');
200
+ expect(body).toContain('build-octane-software');
201
+ } finally {
202
+ await client.close();
203
+ await server.close();
204
+ }
205
+ });
206
+
207
+ it('serves the project map from both generated, CI-gated sources', async () => {
208
+ // A hand-written map is the copy free to drift, which is why the old one
209
+ // did. Both halves here are regenerated and checked by CI.
210
+ const server = createServer({ repoRoot: resolve(PACKAGE_ROOT, '../..') });
211
+ const client = new Client({ name: 'octane-mcp-test', version: '1.0.0' });
212
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
213
+
214
+ try {
215
+ await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
216
+ const map = (await client.callTool({ name: 'octane_project_map', arguments: {} })).content[0]
217
+ .text;
218
+ expect(map).toContain('Your React instincts are the main failure mode here');
219
+ expect(map).toContain('Package inventory (generated)');
220
+ expect(map).toContain('framework binding');
190
221
  } finally {
191
222
  await client.close();
192
223
  await server.close();
@@ -207,6 +238,26 @@ describe('@octanejs/mcp-server helpers', () => {
207
238
  }
208
239
  });
209
240
 
241
+ it('serves every RuleSync skill, and only skills that exist', async () => {
242
+ // Checking that each mapped path resolves catches a dangling entry but not
243
+ // a missing one, which is how authoring-tsrx was added to .rulesync and
244
+ // stayed unreachable through octane_skill. Compare both directions.
245
+ const repoRoot = resolve(PACKAGE_ROOT, '../..');
246
+ const onDisk = readdirSync(resolve(repoRoot, '.rulesync/skills'), { withFileTypes: true })
247
+ .filter((entry) => entry.isDirectory())
248
+ .map((entry) => entry.name)
249
+ .sort();
250
+
251
+ expect(Object.keys(REPO_SKILLS).sort()).toEqual(onDisk);
252
+
253
+ for (const file of Object.values(REPO_SKILLS)) {
254
+ expect(file.startsWith('.rulesync/skills/')).toBe(true);
255
+ expect(existsSync(resolve(repoRoot, file))).toBe(true);
256
+ const body = await readFile(resolve(repoRoot, file), 'utf8');
257
+ expect(body).toMatch(/^---\n[\s\S]*?\ndescription: /);
258
+ }
259
+ });
260
+
210
261
  it('runs the React port scaffolder wrapper', async () => {
211
262
  const repoRoot = await mkdtemp(join(tmpdir(), 'octane-mcp-test-'));
212
263
  await mkdir(join(repoRoot, 'scripts'), { recursive: true });