@selesai/code 0.13.22 → 0.13.24

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,26 @@
2
2
 
3
3
  All notable changes to `@selesai/code` will be documented in this file.
4
4
 
5
+ ## [0.13.24] - 2026-09-13
6
+
7
+ ### Added
8
+ - **Self-building Graft graph.** Graft now creates the initial graph itself instead of waiting for a manual build: the first session start, the first read-only tool call, and a completed CLI install each trigger one build for the repository. A compatible active Selesai model (TokenIn, LiteLLM, OpenAI, Anthropic, or OrcaRouter) adds the deep tier; otherwise the build stays local and structural.
9
+ - **Active-model deep builds.** Deep enrichment now runs on the active Selesai model instead of Graft's own provider configuration. The credential is resolved per build and passed only to the Graft child process, which never inherits it through the host environment.
10
+
11
+ ### Changed
12
+ - **Graft's default retrieval mode is `hybrid`.** Eligible coding turns start from a smaller orientation pack while precise Graft tools stay available; set `graft.mode: "push"` for the previous full-pack behavior.
13
+ - **Deep is the default build.** `/graft build` and `/graft setup` now run the provider-backed deep build, with the new `/graft build --structural` as the explicit local, offline escape hatch.
14
+ - **Graft doctor and recovery hints.** Provider reporting now describes active-model deep builds and the child-process credential boundary instead of asking for Graft's own provider environment variables.
15
+
16
+ ## [0.13.23] - 2026-09-13
17
+
18
+ ### Added
19
+ - **Bundled Graft code context.** Selesai now ships native integration for `@nanonets/graft`: it installs a compatible CLI when missing, builds a consent-gated local graph, injects bounded source-backed context for eligible coding turns, and exposes six read-only semantic retrieval tools plus `/graft` lifecycle commands. Structural builds stay local and offline; deep provider-backed builds require separate confirmation.
20
+ - **Graft-aware builtin subagents.** The `scout`, `reviewer`, `worker`, `delegate`, and `oracle` builtins automatically receive Graft's read-only code-context tools and child-only extension when Graft is loaded. Explicit user tool or extension overrides retain precedence.
21
+
22
+ ### Changed
23
+ - **Capability gateway Graft exemption.** Graft's code-context tools remain active rather than becoming dormant, preserving its pre-turn retrieval and precise follow-ups.
24
+
5
25
  ## [0.13.22] - 2026-09-12
6
26
 
7
27
  ### Fixed
@@ -11,6 +11,8 @@ export interface ExecOptions {
11
11
  timeout?: number;
12
12
  /** Working directory */
13
13
  cwd?: string;
14
+ /** Environment for this child process (defaults to the host environment). */
15
+ env?: NodeJS.ProcessEnv;
14
16
  }
15
17
  /**
16
18
  * Result of executing a shell command.
package/dist/core/exec.js CHANGED
@@ -11,6 +11,7 @@ export async function execCommand(command, args, cwd, options) {
11
11
  return new Promise((resolve) => {
12
12
  const proc = spawn(command, args, {
13
13
  cwd,
14
+ env: options?.env,
14
15
  shell: false,
15
16
  stdio: ["ignore", "pipe", "pipe"],
16
17
  });
@@ -6,9 +6,9 @@
6
6
  *
7
7
  * Behavior:
8
8
  * - On by default; disable via SELESAI_CAPABILITY_GATEWAY=0.
9
- * - At session start, extension tools (except the gateway's own tools) become
10
- * dormant: they stay registered but are removed from the active tool set.
11
- * Built-in tools are never touched.
9
+ * - At session start, extension tools become dormant except the gateway's own
10
+ * tools and Graft's code-context tools: they stay registered but are removed
11
+ * from the active tool set. Built-in tools are never touched.
12
12
  * - A compact catalog tool lists eligible tools/skills with one-line summaries.
13
13
  * - capability_discover validates one catalogued tool and activates its native
14
14
  * definition for the current agent run; capability_skill_show loads exactly
@@ -40,6 +40,17 @@ import {
40
40
  export const GATEWAY_ENV = "SELESAI_CAPABILITY_GATEWAY";
41
41
  export const GATEWAY_TOOLS = new Set(["capability_catalog", "capability_discover", "capability_skill_show"]);
42
42
 
43
+ // Graft supplies pre-turn hybrid context and must remain callable for precise
44
+ // follow-ups; making it dormant defeats both paths.
45
+ const ALWAYS_ACTIVE_EXTENSION_TOOLS = new Set([
46
+ "graft_check_freshness",
47
+ "graft_file_api",
48
+ "graft_find_all",
49
+ "graft_find_code",
50
+ "graft_repo_map",
51
+ "graft_trace_calls",
52
+ ]);
53
+
43
54
  export const CAPABILITY_INSTRUCTION = `Optional capabilities (extension tools and skills) are not listed here by default. To use one:
44
55
  - Search the compact catalog with capability_catalog (kind: "tool" or "skill", natural-language query) when no active tool fits or a specialized integration/workflow is requested.
45
56
  - Activate a catalogued tool with capability_discover, then call it normally on the next turn.
@@ -53,7 +64,7 @@ function isEnabled(): boolean {
53
64
  function eligibleTools(pi: ExtensionAPI): ToolInfo[] {
54
65
  return pi
55
66
  .getAllTools()
56
- .filter((tool) => !GATEWAY_TOOLS.has(tool.name) && !BUILTIN_TOOL_NAMES.has(tool.name));
67
+ .filter((tool) => !GATEWAY_TOOLS.has(tool.name) && !ALWAYS_ACTIVE_EXTENSION_TOOLS.has(tool.name) && !BUILTIN_TOOL_NAMES.has(tool.name));
57
68
  }
58
69
 
59
70
  function catalogEntries(pi: ExtensionAPI): CatalogEntry[] {
@@ -16,6 +16,7 @@ import {
16
16
  const EXTENSIONS_DIR = fileURLToPath(new URL("../../", import.meta.url));
17
17
  const GATEWAY_DIR = fileURLToPath(new URL(".", import.meta.url));
18
18
  const GREP_APP_DIR = fileURLToPath(new URL("../grep-app", import.meta.url));
19
+ const GRAFT_DIR = fileURLToPath(new URL("../pi-graft", import.meta.url));
19
20
  const INLINE_SKILLS_FILE = fileURLToPath(new URL("../inline-skills.ts", import.meta.url));
20
21
 
21
22
  interface Harness {
@@ -134,6 +135,20 @@ describe("capability gateway integration", () => {
134
135
  expect(active).toContain("capability_catalog");
135
136
  });
136
137
 
138
+ it("keeps all Graft tools active for hybrid context and precise follow-ups", async () => {
139
+ const h = await createGatewaySession({ enabled: true, extensions: [GATEWAY_DIR, GRAFT_DIR] });
140
+ harnesses.push(h);
141
+ const active = h.session.getActiveToolNames();
142
+ for (const name of [
143
+ "graft_check_freshness",
144
+ "graft_file_api",
145
+ "graft_find_all",
146
+ "graft_find_code",
147
+ "graft_repo_map",
148
+ "graft_trace_calls",
149
+ ]) expect(active).toContain(name);
150
+ });
151
+
137
152
  it("keeps all-visible behavior when disabled (compatibility mode)", async () => {
138
153
  const h = await createGatewaySession({ enabled: false });
139
154
  harnesses.push(h);
@@ -2,6 +2,7 @@
2
2
  "name": "@selesai/bundled-extensions",
3
3
  "version": "0.0.0",
4
4
  "private": true,
5
+ "type": "module",
5
6
  "description": "Built-in extensions shipped with @selesai/code, loaded via additionalExtensionPaths at boot.",
6
7
  "pi": {
7
8
  "extensions": [
@@ -19,6 +20,7 @@
19
20
  "./inline-skills.ts",
20
21
  "./rtk.ts",
21
22
  "./tokenin-onboarding.ts",
23
+ "./pi-graft",
22
24
  "./undo.ts",
23
25
  "./pi-subagents",
24
26
  "./pi-tool-display",
@@ -0,0 +1,431 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { execCommand } from "../../core/exec.ts";
3
+ import {
4
+ applyTelemetryDefault,
5
+ askReferences,
6
+ boundText,
7
+ compareVersions,
8
+ DEFAULT_FIND_LIMIT,
9
+ GRAFT_INSTALL_SPEC,
10
+ graftArgs,
11
+ graftCommand,
12
+ INSTALL_COMMAND,
13
+ INSTALL_TIMEOUT_MS,
14
+ installGraft,
15
+ graftFailureMessage,
16
+ isCompatibleVersion,
17
+ MAX_GRAFT_VERSION_EXCLUSIVE,
18
+ MIN_GRAFT_VERSION,
19
+ parseAskJson,
20
+ parsePointer,
21
+ npmCommand,
22
+ parseVersion,
23
+ probeGraft,
24
+ renderAsk,
25
+ runGraft,
26
+ TELEMETRY_ENV,
27
+ type GraftOp,
28
+ type GraftRun,
29
+ } from "./cli.ts";
30
+ import { FAILED, makePi, OK, type StubExecResult } from "./test-support.ts";
31
+
32
+ function execReturning(result: StubExecResult) {
33
+ return { ...result };
34
+ }
35
+
36
+ describe("execCommand environment", () => {
37
+ it("passes a supplied credential only to its child process", async () => {
38
+ const result = await execCommand(
39
+ process.execPath,
40
+ ["-e", "process.stdout.write(process.env.GRAFT_TEST_TOKEN ?? '')"],
41
+ process.cwd(),
42
+ { env: { ...process.env, GRAFT_TEST_TOKEN: "child-only" } },
43
+ );
44
+ expect(result).toMatchObject({ code: 0, stdout: "child-only" });
45
+ expect(process.env.GRAFT_TEST_TOKEN).toBeUndefined();
46
+ });
47
+ });
48
+
49
+ describe("graftCommand", () => {
50
+ it("invokes the executable directly off Windows", () => {
51
+ const original = process.platform;
52
+ if (original === "win32") return;
53
+ expect(graftCommand(["ask", "hi"])).toEqual({ command: "graft", args: ["ask", "hi"] });
54
+ });
55
+
56
+ it("routes through cmd.exe on win32 so the .cmd shim can be spawned without a shell", () => {
57
+ const original = process.platform;
58
+ Object.defineProperty(process, "platform", { value: "win32" });
59
+ try {
60
+ expect(graftCommand(["build"])).toEqual({ command: "cmd", args: ["/c", "graft", "build"] });
61
+ } finally {
62
+ Object.defineProperty(process, "platform", { value: original });
63
+ }
64
+ });
65
+ });
66
+
67
+ describe("installGraft", () => {
68
+ it("runs the compatible global npm install without a shell off Windows", async () => {
69
+ const { pi } = makePi(async () => execReturning(OK));
70
+ const run = await installGraft(pi.exec as never, "/repo");
71
+ expect(GRAFT_INSTALL_SPEC).toBe("@nanonets/graft@^0.18");
72
+ expect(INSTALL_COMMAND).toBe("npm install -g @nanonets/graft@^0.18");
73
+ const [command, args, options] = pi.exec.mock.calls[0] as [string, string[], { cwd: string; timeout: number }];
74
+ expect(command).toBe(process.platform === "win32" ? "cmd" : "npm");
75
+ expect(args).toContain("install");
76
+ expect(args).toContain(GRAFT_INSTALL_SPEC);
77
+ expect(options).toMatchObject({ cwd: "/repo", timeout: INSTALL_TIMEOUT_MS });
78
+ expect(run.code).toBe(0);
79
+ });
80
+
81
+ it("routes npm through cmd.exe on win32", () => {
82
+ const original = process.platform;
83
+ Object.defineProperty(process, "platform", { value: "win32" });
84
+ try {
85
+ expect(npmCommand(["install", "-g", GRAFT_INSTALL_SPEC])).toEqual({
86
+ command: "cmd",
87
+ args: ["/c", "npm", "install", "-g", GRAFT_INSTALL_SPEC],
88
+ });
89
+ } finally {
90
+ Object.defineProperty(process, "platform", { value: original });
91
+ }
92
+ });
93
+
94
+ it("normalizes install spawn failures", async () => {
95
+ const { pi } = makePi(async () => {
96
+ throw new Error("spawn npm ENOENT");
97
+ });
98
+ const run = await installGraft(pi.exec as never, "/repo");
99
+ expect(run.code).toBe(1);
100
+ expect(run.stderr).toContain("ENOENT");
101
+ });
102
+ });
103
+
104
+ describe("version handling", () => {
105
+ it("reads a semantic version out of arbitrary CLI output", () => {
106
+ expect(parseVersion("0.18.0")).toBe("0.18.0");
107
+ expect(parseVersion("graft v0.18.3\nlatest on npm: 0.19.0")).toBe("0.18.3");
108
+ expect(parseVersion("0.18.0-beta.2")).toBe("0.18.0-beta.2");
109
+ expect(parseVersion("no version here")).toBeUndefined();
110
+ });
111
+
112
+ it("compares numeric segments, ignoring suffixes", () => {
113
+ expect(compareVersions("0.18.0", "0.18.0")).toBe(0);
114
+ expect(compareVersions("0.17.9", "0.18.0")).toBeLessThan(0);
115
+ expect(compareVersions("0.18.1-beta.1", "0.18.0")).toBeGreaterThan(0);
116
+ expect(compareVersions("1.0.0", "0.99.99")).toBeGreaterThan(0);
117
+ });
118
+
119
+ it("accepts the verified range and rejects the boundaries", () => {
120
+ expect(isCompatibleVersion(MIN_GRAFT_VERSION)).toBe(true);
121
+ expect(isCompatibleVersion("0.18.4")).toBe(true);
122
+ expect(isCompatibleVersion("0.99.0")).toBe(true);
123
+ expect(isCompatibleVersion("0.17.9")).toBe(false);
124
+ expect(isCompatibleVersion(MAX_GRAFT_VERSION_EXCLUSIVE)).toBe(false);
125
+ expect(isCompatibleVersion("1.2.0")).toBe(false);
126
+ });
127
+ });
128
+
129
+ describe("probeGraft", () => {
130
+ it("accepts a supported version and reports it", async () => {
131
+ const { pi } = makePi(async () => execReturning({ ...OK, stdout: "0.18.0\n" }));
132
+ const probe = await probeGraft(pi.exec as never, "/repo");
133
+ expect(probe).toMatchObject({ kind: "ok", version: "0.18.0" });
134
+ if (probe.kind === "ok") expect(probe.argv.at(-1)).toBe("--version");
135
+ });
136
+
137
+ it("reports a missing executable when the spawn throws", async () => {
138
+ const { pi } = makePi(async () => {
139
+ throw new Error("spawn graft ENOENT");
140
+ });
141
+ const probe = await probeGraft(pi.exec as never, "/repo");
142
+ expect(probe.kind).toBe("missing");
143
+ if (probe.kind === "missing") expect(probe.detail).toContain("ENOENT");
144
+ });
145
+
146
+ it("reports a missing executable when the probe is killed", async () => {
147
+ const { pi } = makePi(async () => execReturning({ ...OK, killed: true }));
148
+ const probe = await probeGraft(pi.exec as never, "/repo");
149
+ expect(probe.kind).toBe("missing");
150
+ });
151
+
152
+ it("refuses to accept an out-of-range version", async () => {
153
+ const { pi } = makePi(async () => execReturning({ ...OK, stdout: "0.4.0\n" }));
154
+ const probe = await probeGraft(pi.exec as never, "/repo");
155
+ expect(probe.kind).toBe("incompatible");
156
+ if (probe.kind === "incompatible") {
157
+ expect(probe.version).toBe("0.4.0");
158
+ expect(probe.detail).toContain(MIN_GRAFT_VERSION);
159
+ }
160
+ });
161
+
162
+ it("treats an unreadable version as incompatible rather than assuming compatibility", async () => {
163
+ const { pi } = makePi(async () => execReturning({ ...OK, stdout: "graft (unknown build)\n" }));
164
+ const probe = await probeGraft(pi.exec as never, "/repo");
165
+ expect(probe.kind).toBe("incompatible");
166
+ if (probe.kind === "incompatible") expect(probe.version).toBeUndefined();
167
+ });
168
+ });
169
+
170
+ describe("runGraft", () => {
171
+ it("forwards a private provider environment only to the Graft child", async () => {
172
+ const { pi } = makePi(async () => execReturning(OK));
173
+ const env = { ...process.env, GRAFT_PROVIDER: "litellm", GRAFT_MODEL: "deepseek-v4-flash", GRAFT_API_KEY: "secret" };
174
+ await runGraft(pi.exec as never, "/repo", { kind: "build", deep: true }, { env });
175
+ expect(pi.exec.mock.calls[0]![2]).toMatchObject({ env });
176
+ });
177
+ });
178
+
179
+ describe("graftArgs", () => {
180
+ it("maps every read-only operation to its documented CLI invocation", () => {
181
+ const cases: [GraftOp, string[]][] = [
182
+ [{ kind: "find-code", question: "how does auth work" }, ["ask", "how does auth work", "--source", "--json"]],
183
+ [
184
+ { kind: "find-code", question: "auth", limit: 3, in: "packages/api" },
185
+ ["ask", "auth", "--source", "--json", "-n", "3", "--in", "packages/api"],
186
+ ],
187
+ [{ kind: "file-api", path: "src/app.ts" }, ["skeleton", "src/app.ts"]],
188
+ [{ kind: "trace-calls", symbol: "build", direction: "in" }, ["callers", "build"]],
189
+ [
190
+ { kind: "trace-calls", symbol: "build", direction: "out", depth: 3 },
191
+ ["callers", "build", "--direction", "out", "-d", "3"],
192
+ ],
193
+ [
194
+ { kind: "find-all", pattern: "TODO", ignoreCase: true, fixed: true, in: "src" },
195
+ ["grep", "TODO", "-i", "--fixed", "--in", "src"],
196
+ ],
197
+ [{ kind: "repo-map", maxDirs: 12 }, ["map", "--max-dirs", "12"]],
198
+ [{ kind: "check-freshness" }, ["check", "--json"]],
199
+ [{ kind: "build", deep: false }, ["build"]],
200
+ [{ kind: "build", deep: true }, ["build", "--deep"]],
201
+ ];
202
+ for (const [op, expected] of cases) expect(graftArgs(op)).toEqual(expected);
203
+ });
204
+
205
+ it("keeps the default depth implicit so the CLI's own default applies", () => {
206
+ expect(graftArgs({ kind: "trace-calls", symbol: "x", direction: "in", depth: 1 })).toEqual(["callers", "x"]);
207
+ });
208
+
209
+ it("passes hostile input as one verbatim argv entry, never as shell text", () => {
210
+ const question = 'rm -rf / && echo "pwned"; `whoami`';
211
+ // The whole string survives as exactly one argv element: there is no shell to
212
+ // reinterpret the metacharacters, and nothing splices it into another argument.
213
+ expect(graftArgs({ kind: "find-code", question })).toEqual(["ask", question, "--source", "--json"]);
214
+
215
+ const hostilePattern = '"; rm -rf $HOME; "';
216
+ expect(graftArgs({ kind: "find-all", pattern: hostilePattern })).toEqual(["grep", hostilePattern]);
217
+
218
+ expect(graftArgs({ kind: "file-api", path: "src/with space/and;rm.ts" })).toEqual([
219
+ "skeleton",
220
+ "src/with space/and;rm.ts",
221
+ ]);
222
+ });
223
+ });
224
+
225
+ describe("runGraft", () => {
226
+ it("runs in the resolved repository root and reports the full argv", async () => {
227
+ const { pi } = makePi(async () => execReturning({ ...OK, stdout: "ok" }));
228
+ const run = await runGraft(pi.exec as never, "/repo/root", { kind: "repo-map" });
229
+ expect(pi.exec).toHaveBeenCalledTimes(1);
230
+ const [command, args, options] = pi.exec.mock.calls[0] as [string, string[], { cwd: string; timeout: number }];
231
+ expect(command).toBe(process.platform === "win32" ? "cmd" : "graft");
232
+ expect(args).toContain("map");
233
+ expect(options.cwd).toBe("/repo/root");
234
+ expect(options.timeout).toBe(60_000);
235
+ expect(run.cwd).toBe("/repo/root");
236
+ expect(run.argv.at(-1)).toBe("map");
237
+ });
238
+
239
+ it("uses the long build timeout for builds", async () => {
240
+ const { pi } = makePi(async () => execReturning(OK));
241
+ await runGraft(pi.exec as never, "/repo", { kind: "build", deep: true });
242
+ const [, , options] = pi.exec.mock.calls[0] as [string, string[], { timeout: number }];
243
+ expect(options.timeout).toBe(1_800_000);
244
+ });
245
+
246
+ it("distinguishes a caller abort from a timeout", async () => {
247
+ const { pi } = makePi(async () => execReturning({ ...OK, killed: true }));
248
+ const controller = new AbortController();
249
+ controller.abort();
250
+ const cancelled = await runGraft(pi.exec as never, "/repo", { kind: "repo-map" }, { signal: controller.signal });
251
+ expect(cancelled.cancelled).toBe(true);
252
+ expect(cancelled.timedOut).toBe(false);
253
+
254
+ const noSignal = await runGraft(pi.exec as never, "/repo", { kind: "repo-map" });
255
+ expect(noSignal.timedOut).toBe(true);
256
+ expect(noSignal.cancelled).toBe(false);
257
+ });
258
+
259
+ it("normalizes a spawn failure into a failed run instead of throwing", async () => {
260
+ const { pi } = makePi(async () => {
261
+ throw new Error("spawn graft ENOENT");
262
+ });
263
+ const run = await runGraft(pi.exec as never, "/repo", { kind: "repo-map" });
264
+ expect(run.code).toBe(1);
265
+ expect(run.stderr).toContain("ENOENT");
266
+ });
267
+ });
268
+
269
+ describe("graftFailureMessage", () => {
270
+ const base: GraftRun = {
271
+ op: { kind: "repo-map" },
272
+ argv: ["graft", "map"],
273
+ cwd: "/repo",
274
+ code: 1,
275
+ stdout: "",
276
+ stderr: "",
277
+ killed: false,
278
+ cancelled: false,
279
+ timedOut: false,
280
+ };
281
+
282
+ it("names the command and its exit status, taking the first stderr line", () => {
283
+ expect(graftFailureMessage({ ...base, stderr: "boom\ndetails" })).toBe("graft map failed (exit 1): boom");
284
+ });
285
+
286
+ it("falls back to stdout when stderr is empty", () => {
287
+ expect(graftFailureMessage({ ...base, stdout: "✗ no graft/ here" })).toContain("no graft/ here");
288
+ });
289
+
290
+ it("reports cancellation and timeouts distinctly", () => {
291
+ expect(graftFailureMessage({ ...base, cancelled: true })).toContain("cancelled");
292
+ expect(graftFailureMessage({ ...base, timedOut: true })).toContain("timed out");
293
+ });
294
+ });
295
+
296
+ describe("boundText", () => {
297
+ it("passes short output through untouched", () => {
298
+ const bounded = boundText("one\ntwo");
299
+ expect(bounded).toEqual({ text: "one\ntwo", truncated: false, truncatedBy: null, totalBytes: 7 });
300
+ });
301
+
302
+ it("truncates by lines and says how to see the rest", () => {
303
+ const bounded = boundText(Array.from({ length: 50 }, (_, i) => `line ${i}`).join("\n"), { maxLines: 10 });
304
+ expect(bounded.truncated).toBe(true);
305
+ expect(bounded.truncatedBy).toBe("lines");
306
+ expect(bounded.text).toContain("line 0");
307
+ expect(bounded.text).not.toContain("line 20");
308
+ expect(bounded.text).toContain("[graft] output truncated (10 of 50 lines)");
309
+ });
310
+
311
+ it("truncates by bytes and reports the byte ceiling", () => {
312
+ const bounded = boundText("x".repeat(500), { maxBytes: 100, maxLines: 100 });
313
+ expect(bounded.truncated).toBe(true);
314
+ expect(bounded.truncatedBy).toBe("bytes");
315
+ expect(bounded.text).toContain("of 500 bytes");
316
+ });
317
+ });
318
+
319
+ describe("parseAskJson", () => {
320
+ const valid = JSON.stringify({
321
+ query: "auth",
322
+ mode: "lexical",
323
+ hits: [
324
+ { kind: "symbol", title: "verify · function", pointer: "src/auth.ts:L10-L20", snippet: "checks", score: 3, code: "x" },
325
+ { kind: "concept", title: "Auth", pointer: "src/auth.ts", related: ["a"], score: 1 },
326
+ ],
327
+ coverage: 0.6,
328
+ coverageStrong: 0.5,
329
+ saved: { files: 2, baselineChars: 4000 },
330
+ rules: [{ text: "always verify" }],
331
+ });
332
+
333
+ it("reads the documented hit shape", () => {
334
+ const payload = parseAskJson(valid);
335
+ expect(payload?.mode).toBe("lexical");
336
+ expect(payload?.coverage).toBe(0.6);
337
+ expect(payload?.saved).toEqual({ files: 2, baselineChars: 4000 });
338
+ expect(payload?.hits).toHaveLength(2);
339
+ expect(payload?.hits[0]).toMatchObject({ pointer: "src/auth.ts:L10-L20", code: "x" });
340
+ expect(payload?.rules).toEqual([{ id: undefined, text: "always verify" }]);
341
+ });
342
+
343
+ it("returns undefined for anything it cannot trust", () => {
344
+ expect(parseAskJson("not json")).toBeUndefined();
345
+ expect(parseAskJson("[]")).toBeUndefined();
346
+ expect(parseAskJson(JSON.stringify({ query: "x" }))).toBeUndefined();
347
+ });
348
+
349
+ it("skips malformed hits instead of failing the whole parse", () => {
350
+ const payload = parseAskJson(JSON.stringify({ hits: [{ title: "no pointer" }, { pointer: "src/a.ts" }, 7] }));
351
+ expect(payload?.hits).toHaveLength(1);
352
+ expect(payload?.hits[0]?.title).toBe("src/a.ts");
353
+ });
354
+
355
+ it("drops a malformed saved block rather than surfacing wrong numbers", () => {
356
+ expect(parseAskJson(JSON.stringify({ hits: [], saved: { files: "two" } }))?.saved).toBeUndefined();
357
+ });
358
+ });
359
+
360
+ describe("parsePointer", () => {
361
+ it("splits a file:span pointer", () => {
362
+ expect(parsePointer("src/a.ts:L1-L9")).toEqual({ path: "src/a.ts", span: "L1-L9" });
363
+ expect(parsePointer("src/a.ts:12")).toEqual({ path: "src/a.ts:12" });
364
+ expect(parsePointer("src/a.ts")).toEqual({ path: "src/a.ts" });
365
+ });
366
+ });
367
+
368
+ describe("renderAsk", () => {
369
+ it("renders a compact ranked pack with pointers, snippets, source and receipts", () => {
370
+ const payload = parseAskJson(
371
+ JSON.stringify({
372
+ mode: "lexical",
373
+ note: "ranked by relevance",
374
+ hits: [
375
+ { kind: "symbol", title: "verify · function", pointer: "src/auth.ts:L10-L20", snippet: "checks the token", code: "return true;" },
376
+ { kind: "caller", title: "login", pointer: "src/login.ts:L1-L4", snippet: "", relation: "calls" },
377
+ ],
378
+ saved: { files: 2, baselineChars: 4000 },
379
+ rules: [{ text: "always verify" }],
380
+ }),
381
+ )!;
382
+ const text = renderAsk(payload);
383
+ expect(text).toContain("graft ask (lexical) — 2 hit(s)");
384
+ expect(text).toContain("1. verify · function — src/auth.ts:L10-L20");
385
+ expect(text).toContain("return true;");
386
+ expect(text).toContain("2. login (calls) — src/login.ts:L1-L4");
387
+ expect(text).toContain("governing rules:");
388
+ expect(text).toContain("reading them instead would cost ~1000 tokens");
389
+ expect(askReferences(payload)).toEqual([
390
+ { path: "src/auth.ts", span: "L10-L20" },
391
+ { path: "src/login.ts", span: "L1-L4" },
392
+ ]);
393
+ });
394
+
395
+ it("labels a scoped hit with its sub-project", () => {
396
+ const payload = parseAskJson(JSON.stringify({ hits: [{ pointer: "src/a.ts", scope: "packages/api" }] }))!;
397
+ expect(renderAsk(payload)).toContain("packages/api/src/a.ts");
398
+ });
399
+ });
400
+
401
+ describe("applyTelemetryDefault", () => {
402
+ it("sets DO_NOT_TRACK and restores the prior absence on session shutdown", () => {
403
+ const env: NodeJS.ProcessEnv = {};
404
+ const handle = applyTelemetryDefault(env);
405
+ expect(env[TELEMETRY_ENV]).toBe("1");
406
+ handle.restore();
407
+ expect(env[TELEMETRY_ENV]).toBeUndefined();
408
+ });
409
+
410
+ it("leaves an explicit user preference alone", () => {
411
+ const env: NodeJS.ProcessEnv = { [TELEMETRY_ENV]: "0" };
412
+ const handle = applyTelemetryDefault(env);
413
+ expect(env[TELEMETRY_ENV]).toBe("0");
414
+ handle.restore();
415
+ expect(env[TELEMETRY_ENV]).toBe("0");
416
+ });
417
+ });
418
+
419
+ describe("modest defaults", () => {
420
+ it("keeps the ask limit aligned with the CLI default", () => {
421
+ expect(DEFAULT_FIND_LIMIT).toBe(8);
422
+ });
423
+
424
+ it("never calls the executable through a shell", async () => {
425
+ const { pi } = makePi(async () => execReturning(FAILED));
426
+ await runGraft(pi.exec as never, "/repo", { kind: "find-all", pattern: "a|b" });
427
+ const [command] = pi.exec.mock.calls[0] as [string];
428
+ expect(["graft", "cmd"]).toContain(command);
429
+ expect(vi.isMockFunction(pi.exec)).toBe(true);
430
+ });
431
+ });