@openora/mcp 0.4.1-canary.33 → 0.4.1-canary.331

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.
@@ -0,0 +1,131 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
3
+ import { join, posix } from 'node:path';
4
+ const WORKSPACE_ROOTS = ['apps', 'packages'];
5
+ const CONFIG = '.dependency-cruiser.cjs';
6
+ function cruise(cwd, targets) {
7
+ const out = execFileSync('pnpm', ['exec', 'depcruise', ...targets, '--config', CONFIG, '--output-type', 'json'], { cwd, maxBuffer: 512 * 1024 * 1024 });
8
+ return JSON.parse(out.toString()).modules;
9
+ }
10
+ // A consumer repo keeps one config per workspace; the platform repo keeps one at the root.
11
+ function cruiseRuns(root) {
12
+ const workspaces = WORKSPACE_ROOTS.filter((r) => existsSync(join(root, r))).flatMap((r) => readdirSync(join(root, r))
13
+ .map((name) => `${r}/${name}`)
14
+ .filter((dir) => existsSync(join(root, dir, CONFIG))));
15
+ if (workspaces.length > 0) {
16
+ return workspaces.map((dir) => ({
17
+ dir,
18
+ targets: [existsSync(join(root, dir, 'src')) ? 'src' : '.'],
19
+ }));
20
+ }
21
+ if (!existsSync(join(root, CONFIG))) {
22
+ throw new Error(`No ${CONFIG} at ${root} or in any of ${WORKSPACE_ROOTS.join('/')}/*`);
23
+ }
24
+ return [{ dir: '.', targets: WORKSPACE_ROOTS.filter((r) => existsSync(join(root, r))) }];
25
+ }
26
+ // `exports` point at dist; the graph speaks source, so map each entry back to its src file.
27
+ function publishedEntryPoints(root) {
28
+ const entries = {};
29
+ const packagesDir = join(root, 'packages');
30
+ if (!existsSync(packagesDir)) {
31
+ return entries;
32
+ }
33
+ for (const name of readdirSync(packagesDir)) {
34
+ const manifestPath = join(packagesDir, name, 'package.json');
35
+ if (!existsSync(manifestPath)) {
36
+ continue;
37
+ }
38
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
39
+ if (!manifest.name?.startsWith('@openora/') || manifest.private) {
40
+ continue;
41
+ }
42
+ for (const [key, target] of Object.entries(manifest.exports ?? {})) {
43
+ const importPath = typeof target === 'string' ? target : target?.import;
44
+ const dist = importPath?.match(/^\.\/dist\/(.+)\.js$/)?.[1];
45
+ if (!dist) {
46
+ continue;
47
+ }
48
+ const source = ['ts', 'tsx']
49
+ .map((ext) => `packages/${name}/src/${dist}.${ext}`)
50
+ .find((file) => existsSync(join(root, file)));
51
+ if (source) {
52
+ entries[source] = manifest.name + key.slice(1);
53
+ }
54
+ }
55
+ }
56
+ return entries;
57
+ }
58
+ export function buildDepGraph(root) {
59
+ const dependents = {};
60
+ const platform = {};
61
+ for (const { dir, targets } of cruiseRuns(root)) {
62
+ const toRepoPath = (source) => posix.normalize(`${dir}/${source}`);
63
+ // Linked or installed @openora/* resolve outside the repo; the graph covers the repo's own files.
64
+ const isInternal = (d, path) => !d.coreModule &&
65
+ !d.couldNotResolve &&
66
+ !path.includes('node_modules/') &&
67
+ !toRepoPath(path).startsWith('../');
68
+ for (const module of cruise(join(root, dir), targets)) {
69
+ if (!isInternal(module, module.source)) {
70
+ continue;
71
+ }
72
+ const file = toRepoPath(module.source);
73
+ for (const dependency of module.dependencies) {
74
+ if (isInternal(dependency, dependency.resolved)) {
75
+ (dependents[toRepoPath(dependency.resolved)] ??= new Set()).add(file);
76
+ }
77
+ else if (dependency.module.startsWith('@openora/')) {
78
+ (platform[file] ??= new Set()).add(dependency.module);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ const sorted = (map) => Object.fromEntries(Object.entries(map)
84
+ .sort(([a], [b]) => a.localeCompare(b))
85
+ .map(([file, values]) => [file, [...values].sort()]));
86
+ return {
87
+ generatedAt: new Date().toISOString(),
88
+ commit: process.env['CI_COMMIT_SHA'] ?? process.env['GITHUB_SHA'] ?? null,
89
+ dependents: sorted(dependents),
90
+ platform: sorted(platform),
91
+ entryPoints: publishedEntryPoints(root),
92
+ };
93
+ }
94
+ /** Transitive importers of `file`, up to `maxDepth` hops, nearest first. */
95
+ export function importersOf(graph, file, maxDepth) {
96
+ const depth = new Map([[file, 0]]);
97
+ const queue = [file];
98
+ for (let current = queue.shift(); current !== undefined; current = queue.shift()) {
99
+ const level = depth.get(current) ?? 0;
100
+ if (level >= maxDepth) {
101
+ continue;
102
+ }
103
+ for (const importer of graph.dependents[current] ?? []) {
104
+ if (!depth.has(importer)) {
105
+ depth.set(importer, level + 1);
106
+ queue.push(importer);
107
+ }
108
+ }
109
+ }
110
+ depth.delete(file);
111
+ return [...depth.keys()];
112
+ }
113
+ /**
114
+ * What a change to `file` can break. With a consumer graph, a platform file is followed through
115
+ * the public entry points it reaches into the consumer files that import them - the cross-repo
116
+ * half that no single-repo graph shows.
117
+ */
118
+ export function impactOf(graph, file, maxDepth, consumer) {
119
+ const importers = importersOf(graph, file, maxDepth);
120
+ // Nearest first: a change reaches its own entry point before the barrels that re-export it.
121
+ const specifiers = [...new Set([file, ...importers].flatMap((f) => graph.entryPoints[f] ?? []))];
122
+ const rank = (specs) => Math.min(...specs.map((s) => specifiers.indexOf(s)).filter((i) => i >= 0));
123
+ const consumerFiles = consumer
124
+ ? Object.entries(consumer.platform)
125
+ .filter(([, specs]) => specs.some((s) => specifiers.includes(s)))
126
+ .sort(([a, x], [b, y]) => rank(x) - rank(y) || a.localeCompare(b))
127
+ .map(([f]) => f)
128
+ : [];
129
+ return { file, importers, specifiers, consumerFiles };
130
+ }
131
+ //# sourceMappingURL=dep-graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dep-graph.js","sourceRoot":"","sources":["../src/dep-graph.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export type IntentKind = 'feature' | 'adapter' | 'ui-page' | 'route' | 'unsure';
2
+ export declare function classifyIntent(ask: string): IntentKind;
3
+ //# sourceMappingURL=intent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intent.d.ts","sourceRoot":"","sources":["../src/intent.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEhF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAoBtD"}
package/dist/intent.js ADDED
@@ -0,0 +1,18 @@
1
+ export function classifyIntent(ask) {
2
+ const a = ` ${ask.toLowerCase()} `;
3
+ const has = (re) => re.test(a);
4
+ if (has(/\b(payment|psp|stripe|adyen|kyc|onfido|sms|email|notification|vendor|adapter|gateway)\b/)) {
5
+ return 'adapter';
6
+ }
7
+ if (has(/\b(page|screen|dashboard|view|frontend|admin panel|backoffice page|player page)\b/)) {
8
+ return 'ui-page';
9
+ }
10
+ if (has(/\b(endpoint|route|procedure|api method|rpc)\b/)) {
11
+ return 'route';
12
+ }
13
+ if (has(/\b(feature|module|tournament|leaderboard|jackpot|loyalty|bonus|cashback|mission|reward)\b/)) {
14
+ return 'feature';
15
+ }
16
+ return 'unsure';
17
+ }
18
+ //# sourceMappingURL=intent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intent.js","sourceRoot":"","sources":["../src/intent.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC;IACnC,MAAM,GAAG,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvC,IACE,GAAG,CAAC,yFAAyF,CAAC,EAC9F,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,GAAG,CAAC,mFAAmF,CAAC,EAAE,CAAC;QAC7F,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,GAAG,CAAC,+CAA+C,CAAC,EAAE,CAAC;QACzD,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IACE,GAAG,CAAC,2FAA2F,CAAC,EAChG,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
package/dist/main.js CHANGED
@@ -5,7 +5,36 @@ import { existsSync, readFileSync } from 'node:fs';
5
5
  import { execSync } from 'node:child_process';
6
6
  import { dirname, join, parse } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { buildDepGraph, impactOf } from './dep-graph.js';
9
+ import { classifyIntent } from './intent.js';
8
10
  const here = dirname(fileURLToPath(import.meta.url));
11
+ const ownPackage = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
12
+ const formatIam = (iam) => iam ? `${iam.resource}:${iam.action}` : '(non-literal)';
13
+ function formatAgentTool(t) {
14
+ return [
15
+ `- ${t.id} [${t.class ?? '?'}, v${t.schemaVersion ?? '?'}, iam ${formatIam(t.iam)}, module ${t.module}]`,
16
+ ...(t.title || t.description
17
+ ? [` ${[t.title, t.description].filter(Boolean).join(' - ')}`]
18
+ : []),
19
+ ];
20
+ }
21
+ function formatAgentAction(a) {
22
+ const reversibility = a.reversible === null ? 'reversible ?' : a.reversible ? 'reversible' : 'irreversible';
23
+ const benefit = a.benefitsPlayer === null
24
+ ? ', benefits player ?'
25
+ : a.benefitsPlayer
26
+ ? ', benefits player'
27
+ : a.benefitsPlayerWhen
28
+ ? `, benefits player when ${a.benefitsPlayerWhen.field} is ${a.benefitsPlayerWhen.values.join(' or ')}`
29
+ : '';
30
+ const distinct = a.distinctBy ? `, distinct by ${a.distinctBy}` : '';
31
+ return [
32
+ `- ${a.id} [${reversibility}${benefit}${distinct}, v${a.schemaVersion ?? '?'}, iam ${formatIam(a.iam)}, module ${a.module}]`,
33
+ ...(a.title || a.description
34
+ ? [` ${[a.title, a.description].filter(Boolean).join(' - ')}`]
35
+ : []),
36
+ ];
37
+ }
9
38
  const NOT_FOUND_MESSAGE = 'catalog.json not found - run `pnpm regen` in the platform repo, or set OSS_CATALOG to its path.';
10
39
  function catalogCandidates() {
11
40
  const candidates = [];
@@ -54,16 +83,16 @@ function withCatalog(fn) {
54
83
  }
55
84
  const server = new McpServer({
56
85
  name: 'oss',
57
- version: '0.1.0',
86
+ version: ownPackage.version,
58
87
  });
59
88
  server.registerTool('catalog-overview', {
60
- description: 'START HERE. A concise "what can I extend" summary of the OSS igaming platform you are consuming: counts of modules/events/slots/schemas, the adapter swap-seam table (category/interface/token/status), and the igaming-config fields. Read this first to orient before drilling into the other tools.',
89
+ description: 'START HERE. A concise "what can I extend" summary of the OSS igaming platform you are consuming: counts of modules/events/schemas, the adapter swap-seam table (category/interface/token/status), and the igaming-config fields. Read this first to orient before drilling into the other tools.',
61
90
  inputSchema: {},
62
91
  }, withCatalog((c) => {
63
92
  const lines = ['=== OSS igaming platform catalog ==='];
64
93
  lines.push(`modules: ${c.modules.length} adapters: ${c.adapters.length} events: ${c.events.length} ` +
65
- `uiSlots: ${c.uiSlots.length} schemas: ${c.schemas.length} ` +
66
- `httpRoutes: ${c.httpRoutes.length}`);
94
+ `schemas: ${c.schemas.length} agent tools: ${c.agentTools?.length ?? 0} ` +
95
+ `action types: ${c.agentActions?.length ?? 0}`);
67
96
  lines.push('\n--- Adapter seams (implement an interface, bind to the token) ---');
68
97
  for (const a of c.adapters) {
69
98
  lines.push(`- ${a.category}: ${a.interface} -> ${a.token} [${a.status}]`);
@@ -72,7 +101,7 @@ server.registerTool('catalog-overview', {
72
101
  for (const f of c.config.fields) {
73
102
  lines.push(`- ${f.key}${f.note ? `: ${f.note}` : ''}`);
74
103
  }
75
- lines.push('\nNext: list-adapters | list-routes | list-events | list-slots | describe-module <name> | schema-get <name> | get-config-schema');
104
+ lines.push('\nNext: list-adapters | list-routes | list-events | list-agent-tools | describe-module <name> | schema-get <name> | get-config-schema');
76
105
  return lines.join('\n');
77
106
  }));
78
107
  server.registerTool('list-adapters', {
@@ -97,7 +126,7 @@ server.registerTool('list-adapters', {
97
126
  return lines.join('\n');
98
127
  }));
99
128
  server.registerTool('list-routes', {
100
- description: 'List oRPC route namespaces exposed by the platform modules. Pass `module` to scope to a single module. Includes top-level httpRoutes when present.',
129
+ description: 'List oRPC route namespaces exposed by the platform modules. Pass `module` to scope to a single module.',
101
130
  inputSchema: {
102
131
  module: z.string().optional().describe('Module id to filter by (e.g. "wallet")'),
103
132
  },
@@ -124,9 +153,6 @@ server.registerTool('list-routes', {
124
153
  lines.push(`${m.id}:\n${m.routes.map((r) => ` ${r}`).join('\n')}`);
125
154
  }
126
155
  }
127
- if (!mod && c.httpRoutes.length > 0) {
128
- lines.push(`httpRoutes:\n${c.httpRoutes.map((r) => ` ${r}`).join('\n')}`);
129
- }
130
156
  const text = lines.join('\n\n') || 'No routes defined in the catalog yet.';
131
157
  return { content: [{ type: 'text', text }] };
132
158
  });
@@ -139,19 +165,25 @@ server.registerTool('list-events', {
139
165
  }
140
166
  return `=== Domain events (${c.events.length}) ===\n${c.events.map((e) => `- ${e}`).join('\n')}`;
141
167
  }));
142
- server.registerTool('list-slots', {
143
- description: 'List the named UI slots you can fill from a client-side UI plugin (ctx.<slot>.add(...)) to extend the backoffice without forking: nav items, table columns, dashboard tiles, detail sections. Includes each slot description and subject type.',
144
- inputSchema: {},
145
- }, withCatalog((c) => {
146
- if (c.uiSlots.length === 0) {
147
- return 'No UI slots in the catalog.';
148
- }
149
- const lines = ['=== UI slots ==='];
150
- for (const s of c.uiSlots) {
151
- lines.push(`- ${s.name}${s.description ? ` # ${s.description}` : ''}`);
168
+ server.registerTool('list-agent-tools', {
169
+ description: 'List the agent surface: MCP tools (read calls a model may make during a run) and action types (state changes a model may only propose; an idempotent executor runs them after approval). Each entry shows its IAM resource:action, schema version and owning module. Modules register them in plugin.ts with ctx.mcp.tool(defineMcpTool({...}), factory) and ctx.actions.register(defineActionType({...}), factory); consumers reach them only through the MCP_KERNEL token, which validates input, checks IAM, applies the output allow-list and audits every call. Pass `module` to scope to one module.',
170
+ inputSchema: {
171
+ module: z.string().optional().describe('Module id to filter by (e.g. "wallet")'),
172
+ },
173
+ }, async ({ module: mod }) => {
174
+ const loaded = loadCatalog();
175
+ if (!loaded) {
176
+ return { content: [{ type: 'text', text: NOT_FOUND_MESSAGE }] };
152
177
  }
153
- return lines.join('\n');
154
- }));
178
+ const c = loaded.catalog;
179
+ const tools = (c.agentTools ?? []).filter((t) => !mod || t.module === mod);
180
+ const actions = (c.agentActions ?? []).filter((a) => !mod || a.module === mod);
181
+ const lines = [`=== Agent tools (${tools.length}) ===`];
182
+ lines.push(...(tools.length > 0 ? tools.flatMap(formatAgentTool) : ['(none)']));
183
+ lines.push(`\n=== Agent action types (${actions.length}) ===`);
184
+ lines.push(...(actions.length > 0 ? actions.flatMap(formatAgentAction) : ['(none)']));
185
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
186
+ });
155
187
  server.registerTool('describe-module', {
156
188
  description: "Describe one platform module from the catalog: its group (player/backoffice/platform), Drizzle tables, and oRPC routes. Use this to understand a module's surface before extending or integrating with it.",
157
189
  inputSchema: { name: z.string().describe('Module id (e.g. "wallet", "gaming")') },
@@ -176,6 +208,14 @@ server.registerTool('describe-module', {
176
208
  const lines = [`=== Module: ${m.id} (${m.group}) ===`];
177
209
  lines.push(`\n--- Tables ---\n${m.tables.length > 0 ? m.tables.map((t) => `- ${t}`).join('\n') : '(none)'}`);
178
210
  lines.push(`\n--- Routes ---\n${m.routes.length > 0 ? m.routes.map((r) => `- ${r}`).join('\n') : '(none)'}`);
211
+ const tools = (c.agentTools ?? []).filter((t) => t.module === m.id);
212
+ const actions = (c.agentActions ?? []).filter((a) => a.module === m.id);
213
+ if (tools.length > 0) {
214
+ lines.push(`\n--- Agent tools ---\n${tools.flatMap(formatAgentTool).join('\n')}`);
215
+ }
216
+ if (actions.length > 0) {
217
+ lines.push(`\n--- Agent action types ---\n${actions.flatMap(formatAgentAction).join('\n')}`);
218
+ }
179
219
  return { content: [{ type: 'text', text: lines.join('\n') }] };
180
220
  });
181
221
  server.registerTool('schema-get', {
@@ -220,23 +260,6 @@ server.registerTool('get-config-schema', {
220
260
  }
221
261
  return lines.join('\n');
222
262
  }));
223
- function classifyIntent(ask) {
224
- const a = ` ${ask.toLowerCase()} `;
225
- const has = (re) => re.test(a);
226
- if (has(/\b(payment|psp|stripe|adyen|kyc|onfido|sms|email|notification|vendor|adapter|gateway)\b/)) {
227
- return 'adapter';
228
- }
229
- if (has(/\b(page|screen|dashboard|view|frontend|admin panel|backoffice page|player page)\b/)) {
230
- return 'ui-page';
231
- }
232
- if (has(/\b(endpoint|route|procedure|api method|rpc)\b/)) {
233
- return 'route';
234
- }
235
- if (has(/\b(feature|module|tournament|leaderboard|jackpot|loyalty|bonus|cashback|mission|reward)\b/)) {
236
- return 'feature';
237
- }
238
- return 'unsure';
239
- }
240
263
  function buildConsumerPlaybook(kind, ctx) {
241
264
  const moduleList = ctx.modules.length
242
265
  ? ctx.modules.map((m) => `- ${m}`).join('\n')
@@ -245,7 +268,7 @@ function buildConsumerPlaybook(kind, ctx) {
245
268
  case 'feature':
246
269
  return [
247
270
  '## Where it goes',
248
- 'New behavior in a consumer repo -> an overlay plugin (`pnpm gen plugin`). It can add routes, subscribe to events, rebind adapters, and fill UI slots without touching OSS core.',
271
+ 'New behavior in a consumer repo -> an overlay plugin (`pnpm gen plugin`). It can add routes, subscribe to events, and rebind adapters without touching OSS core.',
249
272
  'If this is a new business domain that should be in the OSS platform itself, open an issue on the OSS repo instead.',
250
273
  '',
251
274
  '## Existing platform modules you can hook into',
@@ -253,15 +276,15 @@ function buildConsumerPlaybook(kind, ctx) {
253
276
  '',
254
277
  '## Playbook',
255
278
  '1. Spawn the `expert` agent (Task tool) to turn this ask into requirements + acceptance criteria (player journey, jurisdiction rules, edge cases). Skip only for a trivial change.',
256
- '2. Spawn the `builder` agent to implement: `pnpm gen plugin`, then in `register(ctx)` add routes (`ctx.routers.add`), subscribe to events (`ctx.events.on`), fill slots (`ctx.slots.fill`).',
279
+ '2. Spawn the `builder` agent to implement: `pnpm gen plugin`, then in `register(ctx)` add routes (`ctx.routers.add`) and subscribe to events (`ctx.events.on`).',
257
280
  '3. Register the plugin in `extensions.config.ts` at the repo root.',
258
- '4. Spawn the `qa` agent to write/run a Playwright E2E test for the acceptance criteria.',
259
- '5. Run `pnpm typecheck && pnpm lint`.',
281
+ '4. Spawn the `tester` agent to write/run a Playwright E2E test for the acceptance criteria.',
282
+ '5. Run `pnpm check:types && pnpm check:lint`.',
260
283
  '',
261
284
  '## Agents',
262
285
  '- `expert` - requirements + AC (advisory, no code)',
263
286
  '- `builder` - implements the overlay',
264
- '- `qa` - E2E test + bug triage (OSS-core vs overlay)',
287
+ '- `tester` - E2E test + bug triage (OSS-core vs overlay)',
265
288
  ].join('\n');
266
289
  case 'adapter':
267
290
  return [
@@ -277,7 +300,7 @@ function buildConsumerPlaybook(kind, ctx) {
277
300
  '1. If the adapter is compliance-sensitive (KYC/AML, PSP, geo), spawn the `expert` agent first to confirm jurisdiction requirements.',
278
301
  '2. Spawn the `builder` agent to implement: `pnpm gen adapter` (prompts for name + token), implement the interface, bind it in the generated plugin.',
279
302
  '3. Ensure the plugin is listed AFTER the module that owns the default binding in `extensions.config.ts` (last registration wins).',
280
- '4. Run `pnpm typecheck && pnpm lint`.',
303
+ '4. Run `pnpm check:types && pnpm check:lint`.',
281
304
  '',
282
305
  '## Agents',
283
306
  '- `expert` - jurisdiction/compliance requirements (advisory)',
@@ -286,21 +309,16 @@ function buildConsumerPlaybook(kind, ctx) {
286
309
  case 'ui-page':
287
310
  return [
288
311
  '## Where it goes',
289
- 'The platform is headless - pages live in your own frontend repo and consume the api via `@openora/core/react`. Fill named UI slots from a client-side UI plugin.',
290
- '',
291
- '## Named UI slots you can fill (via defineUIPlugin)',
292
- ctx.slots.length
293
- ? ctx.slots.map((s) => `- ${s}`).join('\n')
294
- : '- (run list-slots for details)',
312
+ 'The platform is headless - pages live in your own frontend repo and consume the api via `@openora/core/react`.',
295
313
  '',
296
314
  '## Playbook',
297
- '1. Spawn the `builder` agent to implement the page in your frontend repo, or extend an existing surface via `defineUIPlugin` into a slot above.',
298
- '2. Spawn the `qa` agent to verify the page renders and behaves in a browser.',
299
- '3. Run `pnpm typecheck`.',
315
+ '1. Spawn the `builder` agent to implement the page in your frontend repo.',
316
+ '2. Spawn the `tester` agent to verify the page renders and behaves in a browser.',
317
+ '3. Run `pnpm check:types`.',
300
318
  '',
301
319
  '## Agents',
302
320
  '- `builder` - mounts/extends the page',
303
- '- `qa` - browser verification',
321
+ '- `tester` - browser verification',
304
322
  ].join('\n');
305
323
  case 'route':
306
324
  return [
@@ -312,7 +330,7 @@ function buildConsumerPlaybook(kind, ctx) {
312
330
  '',
313
331
  '## Playbook',
314
332
  '1. Spawn the `builder` agent to implement: `pnpm gen plugin` if no overlay exists, then add the oRPC route in `register(ctx)`. Admin routes must assert AdminGuard first.',
315
- '2. Run `pnpm typecheck && pnpm lint`.',
333
+ '2. Run `pnpm check:types && pnpm check:lint`.',
316
334
  '',
317
335
  '## Agents',
318
336
  '- `builder` - implements the route',
@@ -329,7 +347,7 @@ function buildConsumerPlaybook(kind, ctx) {
329
347
  '## Agents available in this repo',
330
348
  '- `expert` - domain/product requirements (advisory)',
331
349
  '- `builder` - fullstack implementation',
332
- '- `qa` - E2E testing',
350
+ '- `tester` - E2E testing',
333
351
  ].join('\n');
334
352
  }
335
353
  }
@@ -365,7 +383,7 @@ const REQUIREMENTS_INTERVIEW = [
365
383
  'Then summarize the requirements back to the user as a short structured list and get an explicit yes before any building starts.',
366
384
  ].join('\n');
367
385
  server.registerTool('enhance-intent', {
368
- description: 'Turn a fuzzy "I want to build X" ask into a grounded, consumer-context brief. Uses the platform catalog (live module/adapter/slot list) to return a classified intent, a requirements checklist to collect from the user, a step-by-step playbook using the consumer pnpm gen commands, and an acceptance-criteria stub.',
386
+ description: 'Turn a fuzzy "I want to build X" ask into a grounded, consumer-context brief. Uses the platform catalog (live module and adapter list) to return a classified intent, a requirements checklist to collect from the user, a step-by-step playbook using the consumer pnpm gen commands, and an acceptance-criteria stub.',
369
387
  inputSchema: {
370
388
  ask: z
371
389
  .string()
@@ -382,7 +400,6 @@ server.registerTool('enhance-intent', {
382
400
  const ctx = {
383
401
  modules: catalog?.modules.map((m) => `${m.group}/${m.id}`) ?? [],
384
402
  tokens: catalog?.adapters.map((a) => a.token) ?? [],
385
- slots: catalog?.uiSlots.map((s) => s.name) ?? [],
386
403
  };
387
404
  const detected = kind && kind !== 'unsure' ? '' : ' (auto-detected)';
388
405
  const text = [
@@ -403,7 +420,7 @@ server.registerTool('enhance-intent', {
403
420
  '## Acceptance criteria (derive from the requirements above)',
404
421
  '- [ ] Happy path works end to end',
405
422
  '- [ ] Edge cases handled',
406
- '- [ ] `pnpm typecheck && pnpm lint` is green',
423
+ '- [ ] `pnpm check:types && pnpm check:lint` is green',
407
424
  ].join('\n');
408
425
  return { content: [{ type: 'text', text }] };
409
426
  });
@@ -422,14 +439,13 @@ server.registerTool('start', {
422
439
  const tokens = catalog?.adapters.map((a) => a.token) ?? [];
423
440
  const role = [
424
441
  '## Your role',
425
- 'You are a requirements interviewer and orchestrator, NOT the implementer. Your one human-facing job is to collect thorough requirements from the user. Everything after that - scaffolding, code, tests - you delegate to the agents (`expert`, `builder`, `qa`) via the Task tool. Keep the user out of the loop after requirements are confirmed, except to resolve genuine decisions only they can make.',
442
+ 'You are a requirements interviewer and orchestrator, NOT the implementer. Your one human-facing job is to collect thorough requirements from the user. Everything after that - scaffolding, code, tests - you delegate to the agents (`expert`, `builder`, `tester`) via the Task tool. Keep the user out of the loop after requirements are confirmed, except to resolve genuine decisions only they can make.',
426
443
  ].join('\n');
427
444
  if (ask) {
428
445
  const resolved = classifyIntent(ask);
429
446
  const playbook = buildConsumerPlaybook(resolved, {
430
447
  modules,
431
448
  tokens,
432
- slots: catalog?.uiSlots.map((s) => s.name) ?? [],
433
449
  });
434
450
  return {
435
451
  content: [
@@ -480,15 +496,95 @@ server.registerTool('start', {
480
496
  'Call `enhance-intent` (ask = the confirmed requirements summary, kind = step 1). Then follow its playbook, which delegates to the agents:',
481
497
  '- `expert` formalizes requirements + acceptance criteria (and may surface gaps - if so, ask the user those, then continue).',
482
498
  '- `builder` implements (`pnpm gen ...`, code).',
483
- '- `qa` writes/runs the E2E test.',
499
+ '- `tester` writes/runs the E2E test.',
484
500
  'You orchestrate; you do not write feature code yourself.',
485
501
  '',
486
- `Platform modules: ${modules.join(', ') || '(catalog not loaded - run pnpm build:oss first)'}`,
502
+ `Platform modules: ${modules.join(', ') || '(catalog not loaded)'}`,
487
503
  ].join('\n'),
488
504
  },
489
505
  ],
490
506
  };
491
507
  });
508
+ let builtGraph = null;
509
+ // The repo's own graph: a prebuilt dep-graph.json at the root wins (CI writes one), else built
510
+ // once per server process - a full cruise takes seconds, not milliseconds.
511
+ function localGraph(rebuild) {
512
+ const prebuilt = join(process.cwd(), 'dep-graph.json');
513
+ if (!rebuild && existsSync(prebuilt)) {
514
+ return JSON.parse(readFileSync(prebuilt, 'utf8'));
515
+ }
516
+ if (rebuild || !builtGraph) {
517
+ builtGraph = buildDepGraph(process.cwd());
518
+ }
519
+ return builtGraph;
520
+ }
521
+ // Shipped with the package (built by the platform's publish job), so a consumer repo can follow
522
+ // a platform file into its own code without a platform checkout.
523
+ function platformGraph() {
524
+ const shipped = join(here, '..', 'docs', 'dep-graph.json');
525
+ return existsSync(shipped) ? JSON.parse(readFileSync(shipped, 'utf8')) : null;
526
+ }
527
+ server.registerTool('impact', {
528
+ description: 'Blast radius of a change: for each repo-relative file, the files that import it (transitively), and for a platform file (packages/core/src/...) the public @openora/* entry points it reaches plus the consumer files importing them. Use before editing a shared file, or to decide what to re-test after a change.',
529
+ inputSchema: {
530
+ files: z
531
+ .array(z.string())
532
+ .min(1)
533
+ .describe('Repo-relative paths, e.g. "apps/web/src/lib/x.ts"'),
534
+ depth: z
535
+ .number()
536
+ .int()
537
+ .min(1)
538
+ .max(10)
539
+ .optional()
540
+ .describe('Import hops to follow (default 4)'),
541
+ rebuild: z
542
+ .boolean()
543
+ .optional()
544
+ .describe('Re-cruise the repo instead of using a cached graph'),
545
+ },
546
+ }, async ({ files, depth = 4, rebuild = false }) => {
547
+ let local;
548
+ try {
549
+ local = localGraph(rebuild);
550
+ }
551
+ catch (error) {
552
+ return {
553
+ content: [{ type: 'text', text: `Could not build the graph: ${String(error)}` }],
554
+ };
555
+ }
556
+ const platform = platformGraph();
557
+ const list = (items, max = 40) => items.length === 0
558
+ ? ' (none)'
559
+ : items
560
+ .slice(0, max)
561
+ .map((i) => ` - ${i}`)
562
+ .join('\n') + (items.length > max ? `\n (+${items.length - max} more)` : '');
563
+ const sections = files.map((file) => {
564
+ const known = (g) => file in g.dependents || file in g.entryPoints || file in g.platform;
565
+ const onPlatform = platform && !known(local) && known(platform);
566
+ const impact = onPlatform
567
+ ? impactOf(platform, file, depth, local)
568
+ : impactOf(local, file, depth);
569
+ const lines = [
570
+ `=== ${file} ===`,
571
+ `Importers (${impact.importers.length}):`,
572
+ list(impact.importers),
573
+ ];
574
+ if (impact.specifiers.length > 0) {
575
+ lines.push(`Public entry points reached:`, list(impact.specifiers));
576
+ }
577
+ if (onPlatform) {
578
+ lines.push(`Files in this repo importing them:`, list(impact.consumerFiles));
579
+ }
580
+ const imports = local.platform[file];
581
+ if (imports?.length) {
582
+ lines.push(`Platform imports of this file:`, list(imports));
583
+ }
584
+ return lines.join('\n');
585
+ });
586
+ return { content: [{ type: 'text', text: sections.join('\n\n') }] };
587
+ });
492
588
  server.registerTool('dev:infra', {
493
589
  description: 'Start / stop / status local dev infrastructure via docker compose (postgres on :5432). Call before pnpm db:migrate or pnpm dev when the database is not reachable.',
494
590
  inputSchema: {