@selesai/code 0.13.29 → 0.13.30

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 (45) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +8 -2
  3. package/dist/core/model-registry.d.ts +13 -1
  4. package/dist/core/model-registry.js +16 -0
  5. package/dist/defaults/settings.json +8 -13
  6. package/dist/extensions/capability-gateway/catalog.ts +2 -2
  7. package/dist/extensions/capability-gateway/index.ts +103 -18
  8. package/dist/extensions/capability-gateway/integration.test.ts +394 -8
  9. package/dist/extensions/capability-gateway/routing.test.ts +415 -0
  10. package/dist/extensions/capability-gateway/routing.ts +221 -0
  11. package/dist/extensions/grep-app/index.ts +10 -0
  12. package/dist/extensions/jev/decisions.test.ts +316 -0
  13. package/dist/extensions/jev/decisions.ts +527 -0
  14. package/dist/extensions/jev/test-support.ts +233 -0
  15. package/dist/extensions/jev-advisory-lifecycle.test.ts +206 -0
  16. package/dist/extensions/jev-advisory-memory.test.ts +191 -0
  17. package/dist/extensions/jev-advisory-recommendations.test.ts +240 -0
  18. package/dist/extensions/jev-advisory-routing.ts +539 -0
  19. package/dist/extensions/package.json +2 -2
  20. package/dist/extensions/pi-hermes-memory/src/memory-search-bridge.ts +40 -0
  21. package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +57 -46
  22. package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +17 -0
  23. package/dist/extensions/pi-hermes-memory/src/tools/session-search-tool.ts +10 -0
  24. package/dist/extensions/pi-hermes-memory/src/tools/skill-tool.ts +5 -0
  25. package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +25 -0
  26. package/dist/extensions/pi-intercom/index.ts +10 -0
  27. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +5 -0
  28. package/dist/extensions/pi-subagents/src/extension/index.ts +5 -0
  29. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +10 -0
  30. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +10 -0
  31. package/dist/extensions/pi-web-agent/src/extension.ts +5 -0
  32. package/dist/extensions/question/index.ts +5 -0
  33. package/dist/extensions/tokenin-onboarding.ts +185 -0
  34. package/dist/skills/code-review-and-quality/SKILL.md +396 -0
  35. package/dist/skills/code-simplification/SKILL.md +331 -0
  36. package/dist/skills/incremental-implementation/SKILL.md +249 -0
  37. package/dist/skills/planning-and-task-breakdown/SKILL.md +257 -0
  38. package/dist/skills/references/agent-skills-LICENSE +21 -0
  39. package/dist/skills/references/definition-of-done.md +67 -0
  40. package/dist/skills/references/performance-checklist.md +236 -0
  41. package/dist/skills/references/security-checklist.md +248 -0
  42. package/docs/settings.md +68 -31
  43. package/package.json +3 -3
  44. package/dist/extensions/auto-model.test.ts +0 -438
  45. package/dist/extensions/auto-model.ts +0 -357
@@ -2,16 +2,19 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { afterEach, describe, expect, it } from "vitest";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
6
  import { fauxProvider } from "@earendil-works/pi-ai/providers/faux";
7
7
  import {
8
8
  createAgentSession,
9
+ createEventBus,
9
10
  DefaultResourceLoader,
10
11
  ModelRuntime,
11
12
  SessionManager,
12
13
  SettingsManager,
13
14
  type AgentSession,
15
+ type EventBus,
14
16
  } from "@selesai/code";
17
+ import { allowNetwork } from "../../../test/test-network-env.ts";
15
18
 
16
19
  const EXTENSIONS_DIR = fileURLToPath(new URL("../../", import.meta.url));
17
20
  const GATEWAY_DIR = fileURLToPath(new URL(".", import.meta.url));
@@ -21,14 +24,34 @@ const INLINE_SKILLS_FILE = fileURLToPath(new URL("../inline-skills.ts", import.m
21
24
 
22
25
  interface Harness {
23
26
  session: AgentSession;
27
+ /** Everything the gateway emitted on its telemetry channel. */
28
+ telemetry: Array<Record<string, unknown>>;
24
29
  dispose: () => Promise<void>;
25
30
  }
26
31
 
27
- async function createGatewaySession(options: {
32
+ interface HarnessOptions {
28
33
  enabled: boolean;
29
34
  withSkill?: boolean;
30
35
  extensions?: string[];
31
- }): Promise<Harness> {
36
+ /** Written to `capabilityGateway.routing.jev` in the session's settings.json. */
37
+ jev?: Record<string, unknown>;
38
+ /** Make the gateway's telemetry channel throw on every emit. */
39
+ telemetryDown?: boolean;
40
+ }
41
+
42
+ /** A bus that fails only on the gateway's own telemetry channel. */
43
+ function telemetryDownBus(): EventBus {
44
+ const bus = createEventBus();
45
+ return {
46
+ emit: (channel, data) => {
47
+ if (channel === "capability-gateway") throw new Error("telemetry channel down");
48
+ bus.emit(channel, data);
49
+ },
50
+ on: bus.on,
51
+ };
52
+ }
53
+
54
+ async function createGatewaySession(options: HarnessOptions): Promise<Harness> {
32
55
  const extensionPaths = options.extensions ?? [EXTENSIONS_DIR];
33
56
  const cwd = mkdtempSync(join(tmpdir(), "gw-cwd-"));
34
57
  const home = mkdtempSync(join(tmpdir(), "gw-home-"));
@@ -45,6 +68,13 @@ async function createGatewaySession(options: {
45
68
  if (options.enabled) delete process.env.SELESAI_CAPABILITY_GATEWAY;
46
69
  else process.env.SELESAI_CAPABILITY_GATEWAY = "0";
47
70
 
71
+ if (options.jev) {
72
+ writeFileSync(
73
+ join(home, "settings.json"),
74
+ JSON.stringify({ capabilityGateway: { routing: { jev: options.jev } } }),
75
+ );
76
+ }
77
+
48
78
  if (options.withSkill) {
49
79
  mkdirSync(join(home, "skills", "research"), { recursive: true });
50
80
  writeFileSync(
@@ -71,11 +101,20 @@ async function createGatewaySession(options: {
71
101
  if (!model) throw new Error("faux model not registered");
72
102
 
73
103
  const settingsManager = SettingsManager.inMemory({ compaction: { enabled: false }, retry: { enabled: false } });
104
+ // Subscribe to the gateway's telemetry channel before the extension can emit.
105
+ const eventBus = options.telemetryDown ? telemetryDownBus() : createEventBus();
106
+ const telemetry: Array<Record<string, unknown>> = [];
107
+ if (!options.telemetryDown) {
108
+ eventBus.on("capability-gateway", (data) => {
109
+ telemetry.push(data as Record<string, unknown>);
110
+ });
111
+ }
74
112
  const loader = new DefaultResourceLoader({
75
113
  cwd,
76
114
  agentDir: home,
77
115
  settingsManager,
78
116
  additionalExtensionPaths: extensionPaths,
117
+ eventBus,
79
118
  noPromptTemplates: true,
80
119
  noThemes: true,
81
120
  noContextFiles: true,
@@ -114,12 +153,13 @@ async function createGatewaySession(options: {
114
153
  rmSync(home, { recursive: true, force: true });
115
154
  };
116
155
 
117
- return { session, dispose };
156
+ return { session, telemetry, dispose };
118
157
  }
119
158
 
120
159
  const harnesses: Harness[] = [];
121
160
  afterEach(async () => {
122
161
  for (const h of harnesses.splice(0)) await h.dispose();
162
+ vi.unstubAllGlobals();
123
163
  });
124
164
 
125
165
  describe("capability gateway integration", () => {
@@ -236,7 +276,7 @@ describe("capability gateway integration", () => {
236
276
  h.session.systemPrompt,
237
277
  { cwd: process.cwd() } as never,
238
278
  );
239
- expect(result).toBeUndefined();
279
+ expect(result.messages).toEqual([]);
240
280
  expect(h.session.getActiveToolNames()).toContain("grep_app_search");
241
281
  });
242
282
 
@@ -253,7 +293,7 @@ describe("capability gateway integration", () => {
253
293
  h.session.systemPrompt,
254
294
  { cwd: process.cwd() } as never,
255
295
  );
256
- expect(result).toBeUndefined();
296
+ expect(result.messages).toEqual([]);
257
297
  expect(h.session.systemPrompt).not.toContain("Research instructions body.");
258
298
  });
259
299
 
@@ -266,7 +306,7 @@ describe("capability gateway integration", () => {
266
306
  h.session.systemPrompt,
267
307
  { cwd: process.cwd() } as never,
268
308
  );
269
- expect(result).toBeUndefined();
309
+ expect(result.messages).toEqual([]);
270
310
  });
271
311
 
272
312
  it("does not recommend capability_skill_show for an inline-loaded $skill", async () => {
@@ -288,6 +328,352 @@ describe("capability gateway integration", () => {
288
328
  h.session.systemPrompt,
289
329
  { cwd: process.cwd() } as never,
290
330
  );
291
- expect(result).toBeUndefined();
331
+ expect(result.messages).toEqual([]);
332
+ });
333
+ });
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // Jev-assisted routing (opt-in): capabilityGateway.routing.jev.
337
+ // ---------------------------------------------------------------------------
338
+
339
+ describe("capability gateway Jev routing", () => {
340
+ // "...github..." only weakly suggests both grep-app tools: the deterministic router returns a
341
+ // two-candidate hint, which is the only Jev trigger.
342
+ const HINT_PROMPT = "look at this github repo";
343
+ // No tool name, alias, or summary token matches: the router has no lexical signal and Jev
344
+ // must never be consulted.
345
+ const NO_SIGNAL_PROMPT = "continue where we left off last time";
346
+
347
+ const jevSettings = (overrides: Record<string, unknown> = {}): Record<string, unknown> => ({
348
+ enabled: true,
349
+ provider: "faux-gw",
350
+ baseUrl: "http://jev.test/v1",
351
+ minConfidence: 0.5,
352
+ timeoutMs: 2_000,
353
+ ...overrides,
354
+ });
355
+
356
+ function jevSseResponse(choice: string, confidence: number): Response {
357
+ const body = JSON.stringify({ answers: { capability: { choice, confidence } } });
358
+ const chunk = (delta: unknown, finish: string | null) =>
359
+ `data: ${JSON.stringify({
360
+ id: "1",
361
+ object: "chat.completion.chunk",
362
+ created: 0,
363
+ model: "jev-1.13",
364
+ choices: [{ index: 0, delta, finish_reason: finish }],
365
+ })}\n\n`;
366
+ return new Response(chunk({ role: "assistant", content: body }, null) + chunk({}, "stop") + "data: [DONE]\n\n", {
367
+ status: 200,
368
+ headers: { "content-type": "text/event-stream" },
369
+ });
370
+ }
371
+
372
+ function stubJev(choice: string, confidence = 0.95): { requests: string[]; fetchMock: ReturnType<typeof vi.fn> } {
373
+ const requests: string[] = [];
374
+ const fetchMock = vi.fn(async (_url: unknown, init?: { body?: unknown }) => {
375
+ requests.push(typeof init?.body === "string" ? init.body : "");
376
+ return jevSseResponse(choice, confidence);
377
+ });
378
+ vi.stubGlobal("fetch", fetchMock);
379
+ return { requests, fetchMock };
380
+ }
381
+
382
+ async function route(h: Harness, prompt: string): Promise<void> {
383
+ const result = await h.session.extensionRunner.emitBeforeAgentStart(
384
+ prompt,
385
+ undefined,
386
+ h.session.systemPrompt,
387
+ { cwd: process.cwd() } as never,
388
+ );
389
+ expect(result.messages).toEqual([]);
390
+ }
391
+
392
+ function routeEvents(h: Harness): Array<Record<string, unknown>> {
393
+ return h.telemetry.filter((event) => event.event === "route");
394
+ }
395
+
396
+ // Telemetry may carry route metadata and canonical tool names, never prompt
397
+ // text, conversation turns, credentials, raw Jev output, or tool arguments.
398
+ const TELEMETRY_KEYS = new Set([
399
+ "event",
400
+ "source",
401
+ "outcome",
402
+ "reason",
403
+ "tool",
404
+ "confidence",
405
+ "candidates",
406
+ "durationMs",
407
+ "baselineCount",
408
+ "dormantCount",
409
+ "activeCount",
410
+ "activated",
411
+ "used",
412
+ "results",
413
+ "skill",
414
+ ]);
415
+
416
+ it("activates the tool Jev selects for the run, then restores the baseline", async () => {
417
+ allowNetwork();
418
+ const { requests } = stubJev("grep_app_search", 0.91);
419
+ const h = await createGatewaySession({
420
+ enabled: true,
421
+ extensions: [GATEWAY_DIR, GREP_APP_DIR, GRAFT_DIR],
422
+ jev: jevSettings(),
423
+ });
424
+ harnesses.push(h);
425
+ const baseline = h.session.getActiveToolNames();
426
+ expect(baseline).not.toContain("grep_app_search");
427
+
428
+ await route(h, HINT_PROMPT);
429
+
430
+ expect(h.session.getActiveToolNames()).toContain("grep_app_search");
431
+ // Built-ins and always-active code-context tools stay active.
432
+ for (const name of ["read", "bash", "capability_catalog", "graft_find_code"]) {
433
+ expect(h.session.getActiveToolNames()).toContain(name);
434
+ }
435
+ expect(routeEvents(h)).toContainEqual(
436
+ expect.objectContaining({ source: "jev", outcome: "attempt", candidates: 2 }),
437
+ );
438
+ expect(routeEvents(h)).toContainEqual(
439
+ expect.objectContaining({
440
+ source: "jev",
441
+ outcome: "activated",
442
+ tool: "grep_app_search",
443
+ confidence: "high",
444
+ candidates: 2,
445
+ durationMs: expect.any(Number),
446
+ }),
447
+ );
448
+ expect(requests).toHaveLength(1);
449
+
450
+ // The activation was temporary: settlement restores the baseline set.
451
+ await h.session.extensionRunner.emit({ type: "agent_settled" });
452
+ expect(h.session.getActiveToolNames()).toEqual(baseline);
453
+ expect(h.telemetry).toContainEqual(
454
+ expect.objectContaining({ event: "reset", activated: 1, used: 0 }),
455
+ );
456
+ });
457
+
458
+ it("exposes the selected schema for the same turn and keeps the other hinted candidate dormant", async () => {
459
+ allowNetwork();
460
+ stubJev("grep_app_search", 0.9);
461
+ const h = await createGatewaySession({
462
+ enabled: true,
463
+ extensions: [GATEWAY_DIR, GREP_APP_DIR],
464
+ jev: jevSettings(),
465
+ });
466
+ harnesses.push(h);
467
+
468
+ await route(h, HINT_PROMPT);
469
+
470
+ // The live loadout is what the resulting turn uses, and the selected tool's
471
+ // schema is registered for it.
472
+ expect(h.session.getActiveToolNames()).toContain("grep_app_search");
473
+ const definition = h.session.getToolDefinition("grep_app_search");
474
+ expect(definition).toBeDefined();
475
+ expect(definition?.parameters).toBeDefined();
476
+ // The other hinted candidate (and every nonselected tool) stays dormant.
477
+ expect(h.session.getActiveToolNames()).not.toContain("grep_app_fetch");
478
+ });
479
+
480
+ it("does not call Jev without a lexical tool signal or for a skill-only match", async () => {
481
+ allowNetwork();
482
+ const { fetchMock } = stubJev("grep_app_search");
483
+ const h = await createGatewaySession({
484
+ enabled: true,
485
+ withSkill: true,
486
+ extensions: [GATEWAY_DIR, GREP_APP_DIR],
487
+ jev: jevSettings(),
488
+ });
489
+ harnesses.push(h);
490
+
491
+ await route(h, NO_SIGNAL_PROMPT);
492
+ expect(fetchMock).not.toHaveBeenCalled();
493
+ expect(routeEvents(h)).toEqual([]);
494
+ expect(h.session.getActiveToolNames()).not.toContain("grep_app_search");
495
+
496
+ // A unique skill match is a recommendation, never a Jev auto-selection.
497
+ await route(h, "do research on this topic");
498
+ expect(fetchMock).not.toHaveBeenCalled();
499
+ expect(routeEvents(h)).toEqual([]);
500
+ expect(h.session.getActiveToolNames()).not.toContain("research");
501
+ });
502
+
503
+ it("does not call Jev when the deterministic router already activated a tool", async () => {
504
+ allowNetwork();
505
+ const deterministic = stubJev("grep_app_search");
506
+ const h = await createGatewaySession({
507
+ enabled: true,
508
+ extensions: [GATEWAY_DIR, GREP_APP_DIR],
509
+ jev: jevSettings(),
510
+ });
511
+ harnesses.push(h);
512
+
513
+ await route(h, "search github code with grep_app_search");
514
+ expect(h.session.getActiveToolNames()).toContain("grep_app_search");
515
+ expect(deterministic.fetchMock).not.toHaveBeenCalled();
516
+ expect(routeEvents(h)).toContainEqual(
517
+ expect.objectContaining({ source: "deterministic", outcome: "activated", tool: "grep_app_search" }),
518
+ );
519
+ });
520
+
521
+ it("leaves routing deterministic when the Jev route is not configured", async () => {
522
+ allowNetwork();
523
+ const disabled = stubJev("grep_app_search");
524
+ const h = await createGatewaySession({ enabled: true, extensions: [GATEWAY_DIR, GREP_APP_DIR] });
525
+ harnesses.push(h);
526
+
527
+ await route(h, HINT_PROMPT);
528
+
529
+ expect(disabled.fetchMock).not.toHaveBeenCalled();
530
+ expect(h.session.getActiveToolNames()).not.toContain("grep_app_search");
531
+ expect(routeEvents(h)).toEqual([]);
532
+ });
533
+
534
+ it("offers Jev only the current prompt and the hinted candidates, never the full catalog", async () => {
535
+ allowNetwork();
536
+ const { requests } = stubJev("none", 1);
537
+ const h = await createGatewaySession({
538
+ enabled: true,
539
+ withSkill: true,
540
+ extensions: [GATEWAY_DIR, GREP_APP_DIR, GRAFT_DIR],
541
+ jev: jevSettings(),
542
+ });
543
+ harnesses.push(h);
544
+
545
+ await route(h, HINT_PROMPT);
546
+
547
+ const sent = JSON.parse(requests[0]!) as { messages: Array<{ content: string }> };
548
+ const payload = JSON.parse(sent.messages[0]!.content) as {
549
+ state: { conversation: unknown[]; system_prompt?: string };
550
+ questions: Record<string, { criteria: Record<string, string> }>;
551
+ };
552
+ const criteria = Object.keys(payload.questions.capability!.criteria);
553
+ expect(criteria).toEqual(["none", "grep_app_search", "grep_app_fetch"]);
554
+ // Built-ins, gateway tools, always-active Graft tools, and skills are not selectable.
555
+ for (const name of ["read", "capability_catalog", "capability_skill_show", "graft_find_code", "research"]) {
556
+ expect(criteria).not.toContain(name);
557
+ }
558
+ // Only the current prompt travels; never conversation history or a system prompt.
559
+ expect(payload.state.conversation).toEqual([{ role: "user", text: HINT_PROMPT }]);
560
+ expect(payload.state.system_prompt).toBeUndefined();
561
+ });
562
+
563
+ it("abstains on `none` and stays non-blocking when Jev is unavailable", async () => {
564
+ allowNetwork();
565
+ const { requests } = stubJev("none", 1);
566
+ const h = await createGatewaySession({
567
+ enabled: true,
568
+ extensions: [GATEWAY_DIR, GREP_APP_DIR],
569
+ jev: jevSettings(),
570
+ });
571
+ harnesses.push(h);
572
+
573
+ await route(h, HINT_PROMPT);
574
+ expect(h.session.getActiveToolNames()).not.toContain("grep_app_search");
575
+ expect(routeEvents(h)).toContainEqual(
576
+ expect.objectContaining({ source: "jev", outcome: "abstained", reason: "none", candidates: 2 }),
577
+ );
578
+ expect(requests).toHaveLength(1);
579
+
580
+ // A provider outage is an ordinary fallback: nothing activates, and the
581
+ // telemetry reports the route without recording any content.
582
+ vi.stubGlobal(
583
+ "fetch",
584
+ vi.fn(async () => {
585
+ throw new Error("connection reset");
586
+ }),
587
+ );
588
+ await route(h, HINT_PROMPT);
589
+ expect(h.session.getActiveToolNames()).not.toContain("grep_app_search");
590
+ expect(routeEvents(h)).toContainEqual(
591
+ expect.objectContaining({ source: "jev", outcome: "unavailable", reason: "transport" }),
592
+ );
593
+
594
+ const serialized = JSON.stringify(h.telemetry);
595
+ expect(serialized).not.toContain(HINT_PROMPT);
596
+ expect(serialized).not.toContain(NO_SIGNAL_PROMPT);
597
+ expect(serialized).not.toContain("answers");
598
+ expect(serialized).not.toContain("faux");
599
+ for (const event of h.telemetry) {
600
+ for (const key of Object.keys(event)) expect(TELEMETRY_KEYS.has(key)).toBe(true);
601
+ }
602
+ });
603
+
604
+ it("is invisible without a Jev provider template or subscription", async () => {
605
+ allowNetwork();
606
+ const { fetchMock } = stubJev("grep_app_search");
607
+ const h = await createGatewaySession({
608
+ enabled: true,
609
+ extensions: [GATEWAY_DIR, GREP_APP_DIR],
610
+ jev: jevSettings({ provider: "no-such-provider", baseUrl: undefined }),
611
+ });
612
+ harnesses.push(h);
613
+
614
+ await route(h, HINT_PROMPT);
615
+
616
+ expect(fetchMock).not.toHaveBeenCalled();
617
+ expect(h.session.getActiveToolNames()).not.toContain("grep_app_search");
618
+ expect(routeEvents(h)).toContainEqual(
619
+ expect.objectContaining({ source: "jev", outcome: "unavailable", reason: "no-template" }),
620
+ );
621
+ });
622
+
623
+ it("records whether an activated tool was actually used, then resets", async () => {
624
+ allowNetwork();
625
+ stubJev("grep_app_search", 0.9);
626
+ const h = await createGatewaySession({
627
+ enabled: true,
628
+ extensions: [GATEWAY_DIR, GREP_APP_DIR],
629
+ jev: jevSettings(),
630
+ });
631
+ harnesses.push(h);
632
+
633
+ await route(h, HINT_PROMPT);
634
+ expect(h.session.getActiveToolNames()).toContain("grep_app_search");
635
+
636
+ // An unrelated tool produces no use telemetry.
637
+ await h.session.extensionRunner.emit({
638
+ type: "tool_execution_start",
639
+ toolCallId: "t0",
640
+ toolName: "read",
641
+ args: {},
642
+ });
643
+ expect(h.telemetry.filter((event) => event.event === "use")).toEqual([]);
644
+
645
+ // The selected tool is called (twice): one use event per activation.
646
+ for (const toolCallId of ["t1", "t2"]) {
647
+ await h.session.extensionRunner.emit({
648
+ type: "tool_execution_start",
649
+ toolCallId,
650
+ toolName: "grep_app_search",
651
+ args: {},
652
+ });
653
+ }
654
+ const uses = h.telemetry.filter((event) => event.event === "use");
655
+ expect(uses).toEqual([{ event: "use", tool: "grep_app_search", source: "jev" }]);
656
+
657
+ await h.session.extensionRunner.emit({ type: "agent_settled" });
658
+ expect(h.telemetry).toContainEqual(
659
+ expect.objectContaining({ event: "reset", activated: 1, used: 1 }),
660
+ );
661
+ });
662
+
663
+ it("completes the run when telemetry cannot be emitted", async () => {
664
+ allowNetwork();
665
+ stubJev("grep_app_search", 0.95);
666
+ const h = await createGatewaySession({
667
+ enabled: true,
668
+ extensions: [GATEWAY_DIR, GREP_APP_DIR],
669
+ jev: jevSettings(),
670
+ telemetryDown: true,
671
+ });
672
+ harnesses.push(h);
673
+
674
+ await route(h, HINT_PROMPT);
675
+
676
+ expect(h.telemetry).toEqual([]);
677
+ expect(h.session.getActiveToolNames()).toContain("grep_app_search");
292
678
  });
293
679
  });