@selesai/code 0.13.37 → 0.13.39

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/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to `@selesai/code` will be documented in this file.
4
4
 
5
+ ## [0.13.39] - 2026-10-02
6
+
7
+ ### Changed
8
+ - **`/tokenin usage` shows the account balance.** It queries `/tokenin/account/me/balance` and shows available credit (plan, PAYG, debt), active plan credit used, and plan expiry; a 404 falls back to the LiteLLM `/key/info` quota view.
9
+ - **The `researcher` subagent uses `web_explore`** from the bundled `pi-web-agent` extension instead of `pi-web-access` tools; `pi-web-agent` registers a builtin-agent augmentation so foreground `researcher` children load it.
10
+ - **Graft CLI floor raised to 0.21** (`MIN_GRAFT_VERSION` 0.21.0, install spec `@nanonets/graft@^0.21`).
11
+
12
+ ## [0.13.38] - 2026-10-01
13
+
14
+ ### Changed
15
+ - **`jev_find` returns source evidence, not just file pointers.** Large trees are narrowed through directory, file, and source-unit judging; results include ranked files, reading leads, and accepted source verbatim with original line numbers, capped at 12 Jev requests and 16 KiB of source per call. JavaScript and TypeScript are split into declarations when the parser is available; other files and installs without it use bounded text chunks.
16
+
17
+ ### Fixed
18
+ - **Jev Find remains useful when Jev cannot judge.** It returns ripgrep-ranked candidates with local keyword-window source, and provider diagnostics expose only a failure category and optional HTTP status, never raw provider error text.
19
+
5
20
  ## [0.13.37] - 2026-09-30
6
21
 
7
22
  ### Changed
@@ -17,6 +17,7 @@ const state = vi.hoisted(() => ({ settingsPath: "" }));
17
17
  // layer owns the non-streaming decisions request), so a suite stubs that.
18
18
  import {
19
19
  askJev,
20
+ askJevAnswers,
20
21
  buildConversation,
21
22
  buildJevPayload,
22
23
  confidenceBucket,
@@ -278,6 +279,23 @@ describe("askJev", () => {
278
279
  expect(await ask()).toMatchObject({ failure: "malformed" });
279
280
  });
280
281
 
282
+ it("reports safe transport diagnostics without returning raw provider errors", async () => {
283
+ const error = Object.assign(new Error("secret=must-not-escape"), { status: 503 });
284
+ completeMock.mockRejectedValue(error);
285
+
286
+ const result = await askJevAnswers(runtime() as never, CONNECTION, { payload: {}, maxBytes: 1_024 });
287
+ expect(result).toMatchObject({ failure: "transport", diagnostic: { kind: "provider-error", httpStatus: 503 } });
288
+ expect(JSON.stringify(result)).not.toContain("must-not-escape");
289
+ });
290
+
291
+ it("extracts only the HTTP status from TokenIn's error completion", async () => {
292
+ completeMock.mockResolvedValue(jevResponse("", { stopReason: "error", errorMessage: "HTTP 429: private response body" }));
293
+
294
+ const result = await askJevAnswers(runtime() as never, CONNECTION, { payload: {}, maxBytes: 1_024 });
295
+ expect(result).toMatchObject({ failure: "transport", diagnostic: { kind: "provider-error", httpStatus: 429 } });
296
+ expect(JSON.stringify(result)).not.toContain("private response body");
297
+ });
298
+
281
299
  it("abstains per question on an unlisted, low-confidence, or missing answer", async () => {
282
300
  completeMock.mockResolvedValue(jevResponse(jevAnswers({ repository_context: { choice: "maybe", confidence: 1 } })));
283
301
  expect(await ask()).toMatchObject({ choices: {}, failure: "unknown-choice" });
@@ -426,10 +426,41 @@ function responseText(response: { content: readonly { type: string; text?: strin
426
426
  }
427
427
 
428
428
  function failureReason(error: unknown): JevAbstainReason {
429
- if (isRecord(error) && (error.name === "TimeoutError" || error.name === "AbortError")) return "timeout";
429
+ try {
430
+ if (isRecord(error) && (error.name === "TimeoutError" || error.name === "AbortError")) return "timeout";
431
+ } catch {
432
+ // A hostile error object is still just a transport failure.
433
+ }
430
434
  return "transport";
431
435
  }
432
436
 
437
+ function failureDiagnostic(error: unknown, failure: JevAbstainReason): JevFailureDiagnostic {
438
+ if (failure === "timeout") return { kind: "timeout" };
439
+ try {
440
+ const response = isRecord(error) && isRecord(error.response) ? error.response : undefined;
441
+ const message =
442
+ typeof error === "string"
443
+ ? error
444
+ : error instanceof Error
445
+ ? error.message
446
+ : isRecord(error) && typeof error.errorMessage === "string"
447
+ ? error.errorMessage
448
+ : "";
449
+ const messageStatus = /^HTTP (\d{3}):/.exec(message)?.[1];
450
+ const httpStatus =
451
+ (isRecord(error) ? (error.status ?? error.statusCode ?? response?.status) : undefined) ??
452
+ (messageStatus ? Number(messageStatus) : undefined);
453
+ return {
454
+ kind: "provider-error",
455
+ ...(typeof httpStatus === "number" && Number.isInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599
456
+ ? { httpStatus }
457
+ : {}),
458
+ };
459
+ } catch {
460
+ return { kind: "provider-error" };
461
+ }
462
+ }
463
+
433
464
  interface JevAuth {
434
465
  ok: boolean;
435
466
  apiKey?: string;
@@ -494,10 +525,17 @@ export async function askJev(
494
525
  return { choices, rejected, elapsedMs: result.elapsedMs };
495
526
  }
496
527
 
528
+ /** Safe, content-free context for a failed Jev request. Never contains provider error text. */
529
+ export interface JevFailureDiagnostic {
530
+ kind: "timeout" | "provider-error" | "malformed-response";
531
+ httpStatus?: number;
532
+ }
533
+
497
534
  /** Jev's raw `answers` envelope, or why the request produced none. */
498
535
  export interface JevAnswers {
499
536
  answers?: Record<string, unknown>;
500
537
  failure?: JevAbstainReason;
538
+ diagnostic?: JevFailureDiagnostic;
501
539
  elapsedMs: number;
502
540
  }
503
541
 
@@ -510,7 +548,8 @@ export interface JevAnswers {
510
548
  * was asked. Nothing beyond the envelope is validated here, so a caller that
511
549
  * expects typed answers owns interpreting them. Every failure — no provider
512
550
  * template, no credential, an oversized request, a timeout, a rejected call, or
513
- * malformed JSON — is an absent decision, and this never throws.
551
+ * malformed JSON — is an absent decision, and this never throws. Failure diagnostics
552
+ * contain only a category and optional HTTP status, never provider error text.
514
553
  */
515
554
  export async function askJevAnswers(
516
555
  ctx: JevRuntime,
@@ -518,7 +557,11 @@ export async function askJevAnswers(
518
557
  request: { payload: unknown; maxBytes: number },
519
558
  ): Promise<JevAnswers> {
520
559
  const started = Date.now();
521
- const abstained = (failure: JevAbstainReason): JevAnswers => ({ failure, elapsedMs: Date.now() - started });
560
+ const abstained = (failure: JevAbstainReason, diagnostic?: JevFailureDiagnostic): JevAnswers => ({
561
+ failure,
562
+ ...(diagnostic ? { diagnostic } : {}),
563
+ elapsedMs: Date.now() - started,
564
+ });
522
565
 
523
566
  const access = await jevAccess(ctx, connection);
524
567
  if ("failure" in access) return abstained(access.failure);
@@ -539,16 +582,21 @@ export async function askJevAnswers(
539
582
  { apiKey: auth.apiKey, headers: auth.headers, maxTokens: JEV_MAX_TOKENS, signal },
540
583
  );
541
584
  } catch (error) {
542
- return abstained(failureReason(error));
585
+ const failure = failureReason(error);
586
+ return abstained(failure, failureDiagnostic(error, failure));
543
587
  }
544
588
  // A provider failure or a deadline arrives as a message with an error stop
545
589
  // reason rather than as a thrown error.
546
590
  if (completion.stopReason === "error" || completion.stopReason === "aborted") {
547
- return abstained(signal.aborted ? "timeout" : "transport");
591
+ const failure = signal.aborted ? "timeout" : "transport";
592
+ return abstained(
593
+ failure,
594
+ failure === "timeout" ? { kind: "timeout" } : failureDiagnostic(completion.errorMessage, failure),
595
+ );
548
596
  }
549
597
 
550
598
  const answers = parseAnswers(responseText(completion));
551
- if (!answers) return abstained("malformed");
599
+ if (!answers) return abstained("malformed", { kind: "malformed-response" });
552
600
  return { answers, elapsedMs: Date.now() - started };
553
601
  }
554
602
 
@@ -6,7 +6,7 @@
6
6
  * assert what the agent observes — the state Jev was sent, the answers that come
7
7
  * back, and what never leaves the process — never private helper order.
8
8
  */
9
- import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
9
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@@ -32,8 +32,26 @@ vi.mock("@selesai/code", () => ({
32
32
  }),
33
33
  }));
34
34
 
35
- import jevAskToolExtension, { askPath, FIND_BATCH, fitFindPayload, MAX_ASK_QUESTIONS } from "./jev-ask-tool.ts";
36
- import { JEV_ROUTING_EVENT, serializeJevRequest } from "./jev/decisions.ts";
35
+ import jevAskToolExtension, {
36
+ askPath,
37
+ childDirectory,
38
+ FIND_BATCH,
39
+ FIND_MAX_REQUESTS,
40
+ FIND_MAX_SOURCE_BYTES,
41
+ FindRequests,
42
+ fitJudgePayloads,
43
+ MAX_ASK_QUESTIONS,
44
+ renderSourceBlocks,
45
+ runJevFind,
46
+ unitView,
47
+ } from "./jev-ask-tool.ts";
48
+ import { chunkUnits, loadTypeScript, splitSourceUnits } from "./jev-find-source.ts";
49
+ import {
50
+ JEV_ROUTING_EVENT,
51
+ type JevAbstainReason,
52
+ type JevFailureDiagnostic,
53
+ serializeJevRequest,
54
+ } from "./jev/decisions.ts";
37
55
  import { jevResponse, providerTemplate } from "./jev/test-support.ts";
38
56
 
39
57
  const FILE_SENTINEL = "FILE_BODY_MUST_RETURN_TO_JE ONLY";
@@ -425,14 +443,346 @@ describe("jev_find", () => {
425
443
  return { text: result.content[0]?.text ?? "", details: result.details };
426
444
  };
427
445
 
446
+ type Stage = "directories" | "files" | "units";
447
+ /**
448
+ * A fake Jev for `runJevFind`: it reads each request the way Jev would (state keyed by stage,
449
+ * one noul per quoted key) and answers with `decide`, refusing an oversized request exactly as
450
+ * the real transport does. `fail` scripts whole-request failures by stage and attempt.
451
+ */
452
+ function fakeJev(
453
+ decide: (stage: Stage, key: string) => number | undefined,
454
+ options: {
455
+ maxBytes?: number;
456
+ fail?: (stage: Stage, attempt: number) => JevAbstainReason | undefined;
457
+ diagnostic?: JevFailureDiagnostic;
458
+ } = {},
459
+ ) {
460
+ const calls: Array<{ stage: Stage; keys: string[]; bytes: number; payload: Record<string, unknown> }> = [];
461
+ const ask = async (payload: Record<string, unknown>) => {
462
+ const state = payload.state as Record<string, unknown>;
463
+ const stage = (["directories", "files", "units"] as const).find((key) => key in state) as Stage;
464
+ const questions = payload.questions as Record<string, { instructions: { question: string } }>;
465
+ const keys = Object.values(questions).map((q) => /"([^"]+)"/.exec(q.instructions.question)?.[1] ?? "");
466
+ const bytes = Buffer.byteLength(JSON.stringify(payload), "utf-8");
467
+ calls.push({ stage, keys, bytes, payload });
468
+ if (serializeJevRequest(payload, options.maxBytes ?? 32 * 1024) === undefined) {
469
+ return { failure: "overflow" as const, elapsedMs: 0 };
470
+ }
471
+ const failure = options.fail?.(stage, calls.filter((call) => call.stage === stage).length);
472
+ if (failure) return { failure, ...(options.diagnostic ? { diagnostic: options.diagnostic } : {}), elapsedMs: 1 };
473
+ const answers: Record<string, unknown> = {};
474
+ Object.keys(questions).forEach((name, index) => {
475
+ const noul = decide(stage, keys[index]);
476
+ if (noul !== undefined) answers[name] = { noul };
477
+ });
478
+ return { answers, elapsedMs: 1 };
479
+ };
480
+ return { ask, calls, keys: (stage: Stage) => calls.filter((call) => call.stage === stage).flatMap((call) => call.keys) };
481
+ }
482
+
483
+ let tree: string;
484
+ const RETRY_SOURCE = [
485
+ 'import { x } from "./x";',
486
+ "",
487
+ "/** Doubles the delay on every retry. */",
488
+ "export function retryBackoff(attempt: number) {",
489
+ "\treturn 100 * 2 ** attempt;",
490
+ "}",
491
+ "",
492
+ "export const unrelated = x; // not the retry path",
493
+ "",
494
+ ].join("\n");
495
+
428
496
  beforeAll(() => {
429
497
  writeFileSync(join(repo, "src", "other.ts"), "// mentions round only in passing\nexport const x = 1;\n", "utf-8");
430
498
  writeFileSync(join(repo, ".env"), "round=SECRET_ENV_VALUE\n", "utf-8");
499
+ // 86 files: alpha/ (30), beta/core/ (30), beta/extra/ (25), and README.md at the root.
500
+ tree = join(root, "tree");
501
+ for (const [dir, count] of [["alpha", 30], ["beta/core", 30], ["beta/extra", 25]] as const) {
502
+ mkdirSync(join(tree, dir), { recursive: true });
503
+ for (let i = 0; i < count; i += 1) writeFileSync(join(tree, dir, `f${i}.ts`), `export const v${i} = ${i};\n`, "utf-8");
504
+ }
505
+ writeFileSync(join(tree, "beta/core/f3.ts"), RETRY_SOURCE, "utf-8");
506
+ writeFileSync(join(tree, "README.md"), "# Tree\n\nA fixture.\n", "utf-8");
507
+ });
508
+
509
+ const QUESTION = "how is the retry backoff computed";
510
+ const TREE_VERDICTS = (stage: Stage, key: string): number | undefined => {
511
+ if (stage === "directories") return ({ "alpha/": 0.1, "beta/": 0.9, "beta/core/": 0.8, "beta/extra/": 0.2 } as Record<string, number>)[key];
512
+ if (stage === "files") return key === "beta/core/f3.ts" ? 0.95 : 0.05;
513
+ return key.includes("retryBackoff") ? 0.9 : 0.1;
514
+ };
515
+
516
+ describe("directory descent", () => {
517
+ it("prunes a rejected directory and recurses into a kept one, judging root files alongside", async () => {
518
+ const jev = fakeJev(TREE_VERDICTS);
519
+ const result = await runJevFind({ rg: "rg", cwd: tree, root: ".", question: QUESTION, maxBytes: 32 * 1024, model: "jev-test", ask: jev.ask });
520
+
521
+ expect(jev.keys("directories")).toEqual(["beta/", "alpha/", "beta/core/", "beta/extra/"]);
522
+ // The kept directory is described by its most promising files and keyword hits.
523
+ const betaState = (jev.calls[0].payload.state as { directories: Record<string, string> }).directories["beta/"];
524
+ expect(betaState).toMatch(/^55 file\(s\): core\/f3\.ts/);
525
+ expect(betaState).toContain("Keyword hits:\ncore/f3.ts L3: /** Doubles the delay on every retry. */");
526
+ const judgedFiles = jev.keys("files");
527
+ expect(judgedFiles).toHaveLength(31);
528
+ expect(judgedFiles).toContain("README.md");
529
+ expect(judgedFiles.some((path) => path.startsWith("alpha/") || path.startsWith("beta/extra/"))).toBe(false);
530
+
531
+ expect(result.text).toMatch(/^jev_find: 1 relevant file\(s\) \(judged 31 of 86 candidates via jev-test in \d+ms\)$/m);
532
+ expect(result.text).toContain("Jev ruled out 2 directories holding 55 file(s).");
533
+ expect(result.text).toContain("- beta/core/f3.ts (0.95) — reading leads: retryBackoff lines 3-6");
534
+ expect(result.text.trimEnd().endsWith("End context.")).toBe(true);
535
+ expect(result.details).toMatchObject({ total: 86, judged: 31, relevant: 1 });
536
+ });
537
+
538
+ it("keeps directories whose question failed, as unknown rather than rejected", async () => {
539
+ const jev = fakeJev(TREE_VERDICTS, { fail: (stage) => (stage === "directories" ? "timeout" : undefined) });
540
+ const result = await runJevFind({ rg: "rg", cwd: tree, root: ".", question: QUESTION, maxBytes: 32 * 1024, model: "jev-test", ask: jev.ask });
541
+
542
+ const judgedFiles = jev.keys("files");
543
+ expect(judgedFiles).toHaveLength(86);
544
+ expect(judgedFiles).toContain("alpha/f0.ts");
545
+ expect(result.text).not.toContain("ruled out");
546
+ expect(result.text).toMatch(/directories were kept unjudged \(timeout\)/);
547
+ expect(result.text).toContain('Source block "beta/core/f3.ts" lines 3-6:');
548
+ });
549
+
550
+ it("stops free descent at the directory depth limit", async () => {
551
+ const deepTree = join(root, "deep-tree");
552
+ for (const branch of ["one", "two"]) {
553
+ const dir = join(deepTree, "a", "b", "c", "d", "e", branch);
554
+ mkdirSync(dir, { recursive: true });
555
+ for (let i = 0; i < 30; i += 1) writeFileSync(join(dir, `backoff${i}.ts`), `export const backoff${i} = ${i};\n`, "utf-8");
556
+ }
557
+ const jev = fakeJev(() => 0.9);
558
+ const result = await runJevFind({
559
+ rg: "rg",
560
+ cwd: deepTree,
561
+ root: ".",
562
+ question: "backoff",
563
+ maxBytes: 32 * 1024,
564
+ model: "jev-test",
565
+ ask: jev.ask,
566
+ });
567
+
568
+ expect(jev.keys("directories")).toEqual([]);
569
+ expect(jev.keys("files")).toHaveLength(60);
570
+ expect(result.details.requests).toBeLessThanOrEqual(FIND_MAX_REQUESTS);
571
+ });
572
+
573
+ it("names the next directory level below a root", () => {
574
+ expect(childDirectory("src/a/b.ts", ".")).toBe("src");
575
+ expect(childDirectory("src/a/b.ts", "src")).toBe("src/a");
576
+ expect(childDirectory("src/b.ts", "src")).toBeUndefined();
577
+ });
578
+ });
579
+
580
+ describe("source units", () => {
581
+ it("splits TypeScript into statements, merged imports, and class members that keep their header", async () => {
582
+ const ts = await loadTypeScript();
583
+ expect(ts).toBeDefined();
584
+ const filler = Array.from({ length: 40 }, (_, i) => `\t\tconst step${i} = ${i};`);
585
+ const text = [
586
+ 'import { a } from "a";', // 1
587
+ 'import { b } from "b";', // 2
588
+ "", // 3
589
+ "// ---- section banner ----", // 4
590
+ "", // 5
591
+ "/** Adds one. */", // 6
592
+ "export function add(x: number) {", // 7
593
+ "\treturn x + a + b;", // 8
594
+ "}", // 9
595
+ "", // 10
596
+ "export class Big {", // 11
597
+ "\t/** The first step. */", // 12
598
+ "\tfirst() {", // 13
599
+ ...filler, // 14-53
600
+ "\t}", // 54
601
+ "\tsecond() { return 2; } // trailing note", // 55
602
+ "}", // 56
603
+ "",
604
+ ].join("\n");
605
+ expect(splitSourceUnits("x.ts", text, ts)).toEqual([
606
+ { name: "imports", start: 1, end: 2 },
607
+ { name: "add", start: 6, end: 9 },
608
+ { name: "Big.first", start: 12, end: 54, header: 11 },
609
+ { name: "Big.second", start: 55, end: 55, header: 11 },
610
+ ]);
611
+ });
612
+
613
+ it("chunks other files, and scripts when TypeScript is missing, by blank lines into at most 40 lines", () => {
614
+ const long = Array.from({ length: 45 }, (_, i) => `line ${i}`);
615
+ const text = ["# Title", "intro", "", "## Setup", "run it", "", ...long, ""].join("\n");
616
+ expect(splitSourceUnits("README.md", text, undefined)).toEqual([
617
+ { name: "# Title", start: 1, end: 5 },
618
+ { name: "line 0", start: 7, end: 46 },
619
+ { name: "line 40", start: 47, end: 51 },
620
+ ]);
621
+ expect(splitSourceUnits("x.ts", RETRY_SOURCE, undefined)).toEqual(chunkUnits(RETRY_SOURCE));
622
+ });
623
+
624
+ it("returns accepted units verbatim with their own line numbers", async () => {
625
+ const jev = fakeJev(TREE_VERDICTS);
626
+ const result = await runJevFind({
627
+ rg: "rg",
628
+ cwd: tree,
629
+ root: "beta/core",
630
+ question: QUESTION,
631
+ maxBytes: 32 * 1024,
632
+ model: "jev-test",
633
+ ask: jev.ask,
634
+ });
635
+ const block = result.text.split('Source block "beta/core/f3.ts" lines 3-6:\n')[1]?.split("\n").slice(0, 4);
636
+ expect(block).toEqual(RETRY_SOURCE.split("\n").slice(2, 6).map((line, i) => `${i + 3}: ${line}`));
637
+ // Only the unit Jev accepted is returned; the rejected one is not.
638
+ expect(jev.keys("units")).toContain("beta/core/f3.ts lines 8-8 (unrelated)");
639
+ expect(result.text).not.toContain("8: export const unrelated");
640
+ });
641
+
642
+ it("falls back to the matching source unit when Jev cannot judge the units", async () => {
643
+ const jev = fakeJev(TREE_VERDICTS, { fail: (stage) => (stage === "units" ? "timeout" : undefined) });
644
+ const result = await runJevFind({ rg: "rg", cwd: tree, root: "beta/core", question: QUESTION, maxBytes: 32 * 1024, model: "jev-test", ask: jev.ask });
645
+
646
+ expect(result.text).toContain("- beta/core/f3.ts (0.95) — reading leads: retryBackoff lines 3-6");
647
+ expect(result.text).toContain("Jev did not judge the source units of 1 file(s) (timeout); matching source units or keyword windows are shown instead.");
648
+ expect(result.text).toContain('Source block "beta/core/f3.ts" lines 3-6:\n3: /** Doubles the delay on every retry. */');
649
+ expect(result.text).not.toContain("1: import { x }");
650
+ });
651
+
652
+ it("keeps the returned source within the output budget", async () => {
653
+ const big = join(root, "big");
654
+ mkdirSync(big, { recursive: true });
655
+ const fn = (n: number) => [
656
+ `export function retry${n}() {`,
657
+ ...Array.from({ length: 58 }, (_, i) => `\tconst backoff${i} = "${"x".repeat(60)}";`),
658
+ "}",
659
+ "",
660
+ ];
661
+ writeFileSync(join(big, "big.ts"), Array.from({ length: 8 }, (_, n) => fn(n)).flat().join("\n"), "utf-8");
662
+ const jev = fakeJev(() => 0.9);
663
+ const result = await runJevFind({ rg: "rg", cwd: big, root: ".", question: QUESTION, maxBytes: 32 * 1024, model: "jev-test", ask: jev.ask });
664
+
665
+ expect(result.details.sourceBytes).toBeLessThanOrEqual(FIND_MAX_SOURCE_BYTES);
666
+ expect(result.details.sourceBytes).toBeGreaterThan(FIND_MAX_SOURCE_BYTES / 2);
667
+ expect(result.text).toMatch(/source block\(s\) left out to stay within 16 KB/);
668
+ // Every numbered line is the file's own line under its own number.
669
+ const rows = readFileSync(join(big, "big.ts"), "utf-8").split("\n");
670
+ for (const match of result.text.matchAll(/^(\d+): (.*)$/gm)) expect(rows[Number(match[1]) - 1]).toBe(match[2]);
671
+ });
672
+
673
+ it("clips a block to the byte budget and leaves out one that cannot show three lines", () => {
674
+ const rows = Array.from({ length: 20 }, (_, i) => `row ${i + 1}`);
675
+ const unit = { name: "u", start: 1, end: 20 };
676
+ const rendered = renderSourceBlocks(
677
+ [
678
+ { path: "a.txt", unit, view: unitView(unit, undefined), rows },
679
+ { path: "b.txt", unit, view: unitView(unit, undefined), rows },
680
+ ],
681
+ 60,
682
+ );
683
+ expect(rendered.bytes).toBeLessThanOrEqual(60);
684
+ expect(rendered.lines).toEqual(['', 'Source block "a.txt" lines 1-20:', "1: row 1", "2: row 2", "3: row 3", "4: row 4", "5: row 5", "6: row 6", "…"]);
685
+ expect(rendered.omitted).toBe(1);
686
+ });
687
+
688
+ it("shows a long unit around its best keyword line", () => {
689
+ expect(unitView({ name: "f", start: 10, end: 200 }, 150, 20)).toEqual([10, 0, ...Array.from({ length: 19 }, (_, i) => 140 + i), 0]);
690
+ expect(unitView({ name: "m", start: 30, end: 31, header: 5 }, undefined)).toEqual([5, 0, 30, 31]);
691
+ });
431
692
  });
432
693
 
433
- it("ranks ripgrep's candidates by Jev relevance and returns pointers, never contents", async () => {
694
+ describe("reliability", () => {
695
+ it("retries a transport failure once and then judges", async () => {
696
+ const jev = fakeJev((stage, key) => (stage === "files" ? (key.endsWith("round.ts") ? 0.9 : 0.1) : 0.9), {
697
+ fail: (stage, attempt) => (stage === "files" && attempt === 1 ? "transport" : undefined),
698
+ });
699
+ const result = await runJevFind({ rg: "rg", cwd: repo, root: "src", question: "rounding", maxBytes: 32 * 1024, model: "jev-test", ask: jev.ask });
700
+
701
+ expect(jev.calls.filter((call) => call.stage === "files")).toHaveLength(2);
702
+ expect(result.details).toMatchObject({ judged: 2, retried: 1 });
703
+ expect(result.text).toContain("- src/round.ts (0.90)");
704
+ });
705
+
706
+ it("retries only once, then still returns ripgrep-ranked source", async () => {
707
+ const jev = fakeJev(() => 0.9, { fail: () => "transport" });
708
+ const result = await runJevFind({ rg: "rg", cwd: repo, root: "src", question: "rounding", maxBytes: 32 * 1024, model: "jev-test", ask: jev.ask });
709
+
710
+ expect(jev.calls).toHaveLength(2);
711
+ expect(result.text).toMatch(/^jev_find: Jev did not judge \(transport\); 2 candidate file\(s\), ranked by ripgrep:/);
712
+ expect(result.text).toContain('Source block "src/round.ts" lines 1-1:');
713
+ });
714
+
715
+ it("reports sanitized provider status and returns the matching source unit on transport failure", async () => {
716
+ const jev = fakeJev(TREE_VERDICTS, {
717
+ fail: () => "transport",
718
+ diagnostic: { kind: "provider-error", httpStatus: 503 },
719
+ });
720
+ const result = await runJevFind({
721
+ rg: "rg",
722
+ cwd: tree,
723
+ root: "beta/core",
724
+ question: QUESTION,
725
+ pattern: "retry",
726
+ maxBytes: 32 * 1024,
727
+ model: "jev-test",
728
+ ask: jev.ask,
729
+ });
730
+
731
+ expect(result.text).toContain("Diagnostic: Jev provider request failed (HTTP 503).");
732
+ expect(result.details).toMatchObject({ failure: "transport", diagnostic: { kind: "provider-error", httpStatus: 503 } });
733
+ expect(result.text).toContain("retryBackoff lines 3-6");
734
+ expect(result.text).toContain('Source block "beta/core/f3.ts" lines 3-6:\n3: /** Doubles the delay on every retry. */');
735
+ });
736
+
737
+ it("never sends more than the request cap", async () => {
738
+ const asked: number[] = [];
739
+ const requests = new FindRequests(async () => {
740
+ asked.push(1);
741
+ return { failure: "transport", elapsedMs: 0 };
742
+ }, 3);
743
+ const results = await requests.sendAll([{}, {}, {}, {}, {}], 5);
744
+ expect(asked).toHaveLength(3);
745
+ expect(results.slice(3)).toEqual([undefined, undefined]);
746
+ expect(requests.left).toBe(0);
747
+ });
748
+
749
+ it("fits every payload in every stage within maxBytes (the overflow regression)", async () => {
750
+ // The old fit gave up once snippets hit 64 bytes and sent the rest oversized: the questions, paths,
751
+ // and scaffolding of a full batch alone can outgrow a small budget.
752
+ const long = `src/${"deeply/nested/".repeat(12)}file.ts`;
753
+ const items = Array.from({ length: FIND_BATCH }, (_, i) => ({ key: `${long}${i}`, text: '"\n\t\\'.repeat(500), question: `Is "${long}${i}" relevant?` }));
754
+ const stage = { request: "q", stateKey: "files", criteria: { true: "yes", false: "no" } };
755
+ const fitted = fitJudgePayloads(stage, items, 4 * 1024);
756
+ expect(fitted.dropped).toEqual([]);
757
+ expect(fitted.payloads.flatMap((entry) => entry.items).sort((a, b) => a - b)).toEqual(items.map((_, i) => i));
758
+ for (const entry of fitted.payloads) expect(serializeJevRequest(entry.payload, 4 * 1024)).toBeDefined();
759
+ // An item that cannot fit alone is dropped, never sent oversized.
760
+ expect(fitJudgePayloads(stage, [{ key: "k", text: "", question: "x".repeat(5_000) }], 4 * 1024)).toEqual({ payloads: [], dropped: [0] });
761
+
762
+ const maxBytes = 6 * 1024;
763
+ const descent = fakeJev(TREE_VERDICTS, { maxBytes });
764
+ await runJevFind({ rg: "rg", cwd: tree, root: ".", question: `${QUESTION} ${"really ".repeat(2_000)}`, maxBytes, model: "jev-test", ask: descent.ask });
765
+ const units = fakeJev(TREE_VERDICTS, { maxBytes });
766
+ await runJevFind({ rg: "rg", cwd: tree, root: "beta/core", question: QUESTION, maxBytes, model: "jev-test", ask: units.ask });
767
+ const calls = [...descent.calls, ...units.calls];
768
+ expect(new Set(calls.map((call) => call.stage))).toEqual(new Set(["directories", "files", "units"]));
769
+ for (const call of calls) expect(call.bytes).toBeLessThanOrEqual(maxBytes);
770
+ expect(descent.calls.length).toBeLessThanOrEqual(FIND_MAX_REQUESTS);
771
+ });
772
+ });
773
+
774
+ it("registers jev_find as a finder that returns source, pointing subagents at it", () => {
775
+ const tool = harness().tools.jev_find;
776
+ expect(tool.description).toContain("verbatim");
777
+ expect(tool.description).not.toMatch(/never contents|pointers only/i);
778
+ expect(tool.description).toContain(".gitignore");
779
+ expect(tool.promptSnippet).toContain("source");
780
+ expect(tool.promptGuidelines?.join("\n")).toContain("subagent");
781
+ });
782
+
783
+ it("ranks files, returns the accepted source verbatim, and never sends secret files", async () => {
434
784
  const session = harness();
435
- // Jev: the file whose question names round.ts is relevant, the rest are not.
785
+ // Jev: anything naming round.ts is relevant, the rest are not.
436
786
  session.complete.mockImplementation(async (_model: unknown, request: { messages: Array<{ content: string }> }) => {
437
787
  const sent = JSON.parse(request.messages[0].content) as { questions: Record<string, { instructions: { question: string } }> };
438
788
  const answers = Object.fromEntries(
@@ -442,33 +792,29 @@ describe("jev_find", () => {
442
792
  });
443
793
  const result = await find(session, { question: "where is rounding implemented", pattern: "round", ignoreCase: true });
444
794
 
445
- expect(result.text).toMatch(/judged 2 of 2 candidate/);
446
- expect(result.text).toContain("- src/round.ts:1 (0.93)");
447
- expect(result.text).not.toContain("other.ts");
448
- expect(result.text).not.toContain(FILE_SENTINEL);
449
- const sent = session.sent() as { state: { files: Record<string, string> } };
450
- expect(Object.keys(sent.state.files).sort()).toEqual(["src/other.ts", "src/round.ts"]);
451
- expect(sent.state.files["src/round.ts"]).toContain(FILE_SENTINEL);
452
- expect(JSON.stringify(sent)).not.toContain("SECRET_ENV_VALUE");
795
+ expect(result.text).toMatch(/^jev_find: 1 relevant file\(s\) \(judged 2 of 2 candidates via jev-1\.13 in \d+ms\)/);
796
+ // The sentinel line is not valid TypeScript; the parser recovers and still names the declaration.
797
+ expect(result.text).toMatch(/^- src\/round\.ts \(0\.93\) — reading leads: round\b.* lines 1-1$/m);
798
+ expect(result.text).toContain(`Source block "src/round.ts" lines 1-1:\n1: export const round = () => ${FILE_SENTINEL};`);
799
+ expect(result.text).not.toContain("- src/other.ts");
800
+ const everything = JSON.stringify(session.complete.mock.calls);
801
+ expect(everything).toContain("src/other.ts");
802
+ expect(everything).not.toContain("SECRET_ENV_VALUE");
803
+ expect(session.telemetry.at(-1)).toMatchObject({ route: "find", outcome: "jev" });
453
804
  });
454
805
 
455
- it("lists a directory by glob and still returns candidates when Jev is unreachable", async () => {
806
+ it("still returns ripgrep-ranked files and their source when Jev is unreachable", async () => {
456
807
  const session = harness({ credential: false });
457
808
  const result = await find(session, { question: "rounding", path: "src", glob: "*.ts" });
458
809
 
459
810
  expect(session.complete).not.toHaveBeenCalled();
460
- expect(result.text).toMatch(/Jev did not judge \(no-credential\); 2 candidate/);
811
+ const lines = result.text.split("\n");
812
+ expect(lines[0]).toBe("jev_find: Jev did not judge (no-credential); 2 candidate file(s), ranked by ripgrep:");
461
813
  // The question word in the path orders the unjudged listing.
462
- expect(result.text.split("\n")[1]).toBe("- src/round.ts");
463
- });
464
-
465
- it("shrinks escape-heavy snippets until a full batch fits the request budget", () => {
466
- // Quotes and newlines double under JSON escaping, which is what overflowed live.
467
- const snippet = '"\n\t\\'.repeat(1_000);
468
- const batch = Array.from({ length: FIND_BATCH }, (_, i) => ({ path: `src/f${i}.ts`, lines: [], snippet }));
469
- const payload = fitFindPayload("q", batch, 32 * 1024);
470
- expect(serializeJevRequest(payload, 32 * 1024)).toBeDefined();
471
- expect(Object.keys(payload.questions as object)).toHaveLength(FIND_BATCH);
814
+ expect(lines[1]).toBe("- src/round.ts — file head lines 1-1");
815
+ expect(result.text).toContain(`Source block "src/round.ts" lines 1-1:\n1: export const round = () => ${FILE_SENTINEL};`);
816
+ expect(result.text.trimEnd().endsWith("End context.")).toBe(true);
817
+ expect(session.telemetry.at(-1)).toMatchObject({ route: "find", outcome: "fallback", reason: "no-credential" });
472
818
  });
473
819
 
474
820
  it("refuses a search root outside the working directory", async () => {