@selesai/code 0.13.23 → 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,17 @@
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
+
5
16
  ## [0.13.23] - 2026-09-13
6
17
 
7
18
  ### Added
@@ -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
  });
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
+ import { execCommand } from "../../core/exec.ts";
2
3
  import {
3
4
  applyTelemetryDefault,
4
5
  askReferences,
@@ -32,6 +33,19 @@ function execReturning(result: StubExecResult) {
32
33
  return { ...result };
33
34
  }
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
+
35
49
  describe("graftCommand", () => {
36
50
  it("invokes the executable directly off Windows", () => {
37
51
  const original = process.platform;
@@ -153,6 +167,15 @@ describe("probeGraft", () => {
153
167
  });
154
168
  });
155
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
+
156
179
  describe("graftArgs", () => {
157
180
  it("maps every read-only operation to its documented CLI invocation", () => {
158
181
  const cases: [GraftOp, string[]][] = [
@@ -11,8 +11,8 @@
11
11
  * paths, regexes, symbols, and question text cannot become shell injection.
12
12
  * - Graft's upstream agent-wiring command (`init`) is never invoked. A missing
13
13
  * compatible CLI is installed once through npm; incompatible CLIs are left alone.
14
- * - Semantic tools are read-only. Graph builds are command operations gated
15
- * behind explicit consent; CLI provisioning is automatic.
14
+ * - Semantic tools are read-only. Automatic builds use the active compatible
15
+ * Selesai model for deep enrichment, otherwise stay structural and local.
16
16
  * - Extension-spawned commands default to telemetry opt-out (`DO_NOT_TRACK=1`).
17
17
  *
18
18
  * Verified against @nanonets/graft 0.18.0 (see {@link MIN_GRAFT_VERSION}).
@@ -144,8 +144,8 @@ export async function probeGraft(exec: ExecLike, cwd: string, signal?: AbortSign
144
144
  // ---------------------------------------------------------------------------
145
145
 
146
146
  /**
147
- * The read-only operations this adapter exposes, plus the two consent-gated
148
- * build operations. Every other Graft subcommand (including `init`, `viz`, and
147
+ * The read-only operations this adapter exposes, plus structural and deep
148
+ * builds. Every other Graft subcommand (including `init`, `viz`, and
149
149
  * `uninstall`) is deliberately unreachable from tool calls.
150
150
  */
151
151
  export type GraftOp =
@@ -286,7 +286,7 @@ export async function runGraft(
286
286
  exec: ExecLike,
287
287
  repoRoot: string,
288
288
  op: GraftOp,
289
- options: { signal?: AbortSignal; timeout?: number } = {},
289
+ options: { signal?: AbortSignal; timeout?: number; env?: NodeJS.ProcessEnv } = {},
290
290
  ): Promise<GraftRun> {
291
291
  const args = graftArgs(op);
292
292
  const { command, args: argv } = graftCommand(args);
@@ -297,6 +297,7 @@ export async function runGraft(
297
297
  cwd: repoRoot,
298
298
  timeout: options.timeout ?? timeoutFor(op),
299
299
  signal: options.signal,
300
+ env: options.env,
300
301
  });
301
302
  } catch (error) {
302
303
  return {
@@ -139,14 +139,14 @@ describe("formatDoctor", () => {
139
139
  expect(text).toContain("fresh-structural");
140
140
  expect(text).toContain("nothing is injected");
141
141
  expect(text).toContain("DO_NOT_TRACK=1 set by this extension");
142
- expect(text).toContain("deep builds would need Graft's own provider configuration");
142
+ expect(text).toContain("automatic deep builds use Selesai's active compatible model");
143
143
  expect(text).toContain("graft deep adds provider-backed summaries");
144
144
  });
145
145
 
146
146
  it("shows only which provider variables are set, never their values", () => {
147
147
  const text = formatDoctor({ ...base, providerEnv: ["GRAFT_PROVIDER", "GRAFT_MODEL"] });
148
148
  expect(text).toContain("GRAFT_PROVIDER, GRAFT_MODEL set");
149
- expect(text).toContain("values never read or copied");
149
+ expect(text).toContain("values never shown");
150
150
  });
151
151
 
152
152
  it("reports whether this session actually applied the telemetry default", () => {
@@ -205,8 +205,8 @@ describe("buildEffects", () => {
205
205
 
206
206
  it("states the provider boundary and the cache for a deep build", () => {
207
207
  const text = buildEffects(true);
208
- expect(text).toContain("GRAFT_PROVIDER");
209
- expect(text).toContain("Graft's own environment");
208
+ expect(text).toContain("Selesai's active compatible model");
209
+ expect(text).toContain("Graft child process");
210
210
  expect(text).toContain("caches those summaries under graft/");
211
211
  });
212
212
  });
@@ -289,7 +289,7 @@ describe("mutation-capable commands", () => {
289
289
  }
290
290
  });
291
291
 
292
- it("installs a missing CLI, rechecks it, then asks only before building", async () => {
292
+ it("installs a missing CLI, rechecks it, then asks before the default deep build", async () => {
293
293
  const harness = makeRuntime({ state: { name: "unavailable", detail: "ENOENT" }, built: false });
294
294
  const ctx = makeCtx();
295
295
  await invoke(harness.runtime, "setup", ctx);
@@ -297,8 +297,8 @@ describe("mutation-capable commands", () => {
297
297
  expect(harness.installCli).toHaveBeenCalledWith(ctx);
298
298
  expect(harness.recheck).toHaveBeenCalledTimes(3);
299
299
  expect(ctx.ui.confirm).toHaveBeenCalledTimes(1);
300
- expect(ctx.ui.confirm.mock.calls[0]![0]).toBe("Set up Graft for this repository?");
301
- expect(harness.runBuild).toHaveBeenCalledWith(false, ctx);
300
+ expect(ctx.ui.confirm.mock.calls[0]![0]).toBe("Run the provider-backed deep build?");
301
+ expect(harness.runBuild).toHaveBeenCalledWith(true, ctx);
302
302
  });
303
303
 
304
304
  it("does not build when installing the missing CLI fails", async () => {
@@ -345,14 +345,16 @@ describe("mutation-capable commands", () => {
345
345
  expect(message).toContain(".gitignore");
346
346
  expect(message).toContain("init");
347
347
  expect(message).toContain("never run");
348
- expect(harness.runBuild).toHaveBeenCalledWith(false, ctx);
348
+ expect(harness.runBuild).toHaveBeenCalledWith(true, ctx);
349
349
  expect(notified(ctx)).toContain("Done.");
350
350
  });
351
351
 
352
- it("does not run a deep build when the structural build was confirmed", async () => {
352
+ it("defaults build to deep, with an explicit structural escape hatch", async () => {
353
353
  const harness = makeRuntime();
354
354
  await invoke(harness.runtime, "build", makeCtx());
355
- expect(harness.runBuild).toHaveBeenCalledWith(false, expect.anything());
355
+ expect(harness.runBuild).toHaveBeenCalledWith(true, expect.anything());
356
+ await invoke(harness.runtime, "build --structural", makeCtx());
357
+ expect(harness.runBuild).toHaveBeenLastCalledWith(false, expect.anything());
356
358
  });
357
359
 
358
360
  it("gives `build --deep` the same provider consent as `/graft deep`", async () => {
@@ -371,10 +373,10 @@ describe("mutation-capable commands", () => {
371
373
  expect(harness.runBuild).toHaveBeenNthCalledWith(2, true, viaDeep);
372
374
  });
373
375
 
374
- it("treats setup as the structural build", async () => {
376
+ it("treats setup as the default deep build", async () => {
375
377
  const harness = makeRuntime();
376
378
  await invoke(harness.runtime, "setup", makeCtx());
377
- expect(harness.runBuild).toHaveBeenCalledWith(false, expect.anything());
379
+ expect(harness.runBuild).toHaveBeenCalledWith(true, expect.anything());
378
380
  });
379
381
 
380
382
  it("shows the provider boundary before a deep build and only sends source off-machine after consent", async () => {
@@ -385,7 +387,7 @@ describe("mutation-capable commands", () => {
385
387
  const [title, message] = ctx.ui.confirm.mock.calls[0] as [string, string];
386
388
  expect(title).toContain("provider-backed");
387
389
  expect(message).toContain("leaves this machine");
388
- expect(message).toContain("does not read, copy, or forward");
390
+ expect(message).toContain("credential is passed only to this Graft child process");
389
391
  expect(harness.runBuild).toHaveBeenCalledWith(true, ctx);
390
392
  });
391
393
 
@@ -472,7 +474,7 @@ describe("runtime contract", () => {
472
474
  const ctx = makeCtx();
473
475
  await invoke(harness.runtime, "status", ctx);
474
476
  expect(PROVIDER_ENV_NAMES).toContain("GRAFT_API_KEY");
475
- expect(DEEP_PROVIDER_HINT).toContain("never forwards Selesai credentials");
477
+ expect(DEEP_PROVIDER_HINT).toContain("active Selesai model");
476
478
  expect(DOCTOR_USAGE).toContain("mode [pull|push|hybrid]");
477
479
  });
478
480
  });
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * The `/graft` command surface: setup, build, deep, refresh, status, doctor, mode.
3
3
  *
4
- * Command handlers own two things the tools never touch: consent, and honest
5
- * reporting. A command that can write to the repository says exactly which
6
- * files it may create or change and asks first; with no dialog UI it refuses
7
- * and prints the exact manual command instead.
4
+ * Command handlers own reporting and the explicit deep-build confirmation.
5
+ * They say exactly which repository files may change; with no dialog UI a
6
+ * manual deep build refuses and prints the exact command instead.
8
7
  *
9
8
  * Nothing here ever invokes Graft's upstream agent-wiring command. Selesai's
10
9
  * setup installs a missing CLI automatically, explains local effects, and builds
@@ -61,7 +60,7 @@ export interface GraftCommandRuntime {
61
60
  graphPresence(): GraphPresence;
62
61
  /** Install the compatible CLI when probing found none. */
63
62
  installCli(ctx: ExtensionContext): Promise<GraftInstallRun>;
64
- /** Structural or deep build. Call only after consent. */
63
+ /** Structural or deep build. Deep calls only after consent. */
65
64
  runBuild(deep: boolean, ctx: ExtensionContext): Promise<GraftRun>;
66
65
  /** Proactive refresh of an existing graph; undefined when there is nothing to refresh. */
67
66
  runRefresh(ctx: ExtensionContext): Promise<GraftRun | undefined>;
@@ -75,8 +74,8 @@ export const DOCTOR_USAGE = `/graft <command>
75
74
 
76
75
  status current availability, graph state, and retrieval mode
77
76
  doctor full health report with a recovery step for each problem
78
- setup install the CLI if needed, then explain build effects and ask for consent
79
- build build graft/ for this repository (structural; no model, no key)
77
+ setup install the CLI if needed, then build the deep graph with the active model
78
+ build [--structural] build graft/ (deep by default; structural stays local)
80
79
  deep provider-backed build: adds summaries and per-symbol crux
81
80
  refresh re-index an existing graph after edits
82
81
  mode [pull|push|hybrid] show or set the retrieval strategy
@@ -145,8 +144,8 @@ export function formatDoctor(input: DoctorInput): string {
145
144
  row(
146
145
  "provider env",
147
146
  input.providerEnv.length > 0
148
- ? `${input.providerEnv.join(", ")} set (values never read or copied) — deep builds use Graft's own provider configuration`
149
- : `none of ${PROVIDER_ENV_NAMES.join(", ")} set — deep builds would need Graft's own provider configuration`,
147
+ ? `${input.providerEnv.join(", ")} set (values never shown) — manual Graft CLI configuration is available`
148
+ : `none of ${PROVIDER_ENV_NAMES.join(", ")} set — automatic deep builds use Selesai's active compatible model`,
150
149
  );
151
150
 
152
151
  const recovery = recoveryFor(input.state);
@@ -185,7 +184,7 @@ export function buildEffects(deep: boolean): string {
185
184
  ];
186
185
  if (deep) {
187
186
  effects.push(
188
- " • sends source-derived file summaries and symbol cruxes through the LLM provider configured in Graft's own environment (GRAFT_PROVIDER / GRAFT_MODEL / GRAFT_BASE_URL / GRAFT_API_KEY)",
187
+ " • sends source-derived file summaries and symbol cruxes through Selesai's active compatible model; credentials are passed only to the Graft child process",
189
188
  " • caches those summaries under graft/ so later deep builds are incremental",
190
189
  );
191
190
  }
@@ -259,9 +258,10 @@ export function registerGraftCommands(pi: ExtensionAPI, runtime: GraftCommandRun
259
258
  const items: ArgumentCompletion[] = [
260
259
  { value: "status", label: "status", description: "availability, graph state, retrieval mode" },
261
260
  { value: "doctor", label: "doctor", description: "full health report and recovery steps" },
262
- { value: "setup", label: "setup", description: "install CLI if needed, then build graft/" },
263
- { value: "build", label: "build", description: "build graft/ (structural; no model, no key)" },
264
- { value: "deep", label: "deep", description: "provider-backed build (needs Graft provider config)" },
261
+ { value: "setup", label: "setup", description: "install CLI if needed, then build the deep graph" },
262
+ { value: "build", label: "build", description: "build graft/ with the active model (default deep)" },
263
+ { value: "build --structural", label: "build --structural", description: "build graft/ locally without model enrichment" },
264
+ { value: "deep", label: "deep", description: "provider-backed build with the active model" },
265
265
  { value: "refresh", label: "refresh", description: "re-index an existing graph after edits" },
266
266
  { value: "mode", label: "mode", description: "show or set pull | push | hybrid" },
267
267
  { value: "help", label: "help", description: "usage" },
@@ -317,7 +317,7 @@ export function registerGraftCommands(pi: ExtensionAPI, runtime: GraftCommandRun
317
317
  case "setup":
318
318
  case "build":
319
319
  case "deep": {
320
- const deep = subcommand === "deep" || rest.includes("--deep");
320
+ const deep = subcommand === "deep" || !rest.includes("--structural");
321
321
  await runtime.recheck(ctx);
322
322
  if (!(await ensureCli(ctx, runtime))) return;
323
323
  const repo = runtime.repoRoot() ?? "this repository";
@@ -330,7 +330,7 @@ export function registerGraftCommands(pi: ExtensionAPI, runtime: GraftCommandRun
330
330
  ? "Set up Graft for this repository?"
331
331
  : "Build the Graft graph?",
332
332
  deep
333
- ? `This runs \`graft build --deep\` in ${repo}.\n\nThe structural graph stays local and deterministic. The deep pass is different: Graft summarizes each changed file and extracts per-symbol cruxes using the LLM provider configured in Graft's own environment, so source-derived content leaves this machine.\n\nSelesai does not read, copy, or forward its own provider credentials to Graft.\n\nFiles it can write:\n${buildEffects(true)}`
333
+ ? `This runs \`graft build --deep\` in ${repo}.\n\nThe structural graph stays local and deterministic. The deep pass summarizes each changed file and extracts per-symbol cruxes using Selesai's active compatible model, so source-derived content leaves this machine. Its credential is passed only to this Graft child process.\n\nFiles it can write:\n${buildEffects(true)}`
334
334
  : `This runs \`graft build\` in ${repo} and can write:\n\n${buildEffects(false)}`,
335
335
  manualCommand,
336
336
  );
@@ -388,5 +388,5 @@ async function runAndReport(
388
388
 
389
389
  /** Where the deep-build guidance points a user whose provider is not configured. */
390
390
  export const DEEP_PROVIDER_HINT =
391
- `Set GRAFT_PROVIDER, GRAFT_MODEL, and GRAFT_API_KEY in your own environment (or a repo .env), then re-run /graft deep. ` +
392
- `${GRAFT_PACKAGE} reads them itself; this extension never forwards Selesai credentials.`;
391
+ `Select a compatible active Selesai model (TokenIn, LiteLLM, OpenAI, Anthropic, or OrcaRouter), then re-run /graft deep. ` +
392
+ `${GRAFT_PACKAGE} receives its credential only in that child process.`;
@@ -12,7 +12,7 @@
12
12
  * - Graft's upstream agent-wiring command (`init`) is never invoked.
13
13
  * - A missing compatible CLI is automatically installed once through npm;
14
14
  * incompatible user installations are never overwritten.
15
- * - Builds require explicit consent; deep (provider-backed) builds need their own.
15
+ * - The first build uses Selesai's active compatible model for deep enrichment; otherwise it stays local and structural.
16
16
  * - Every tool is read-only; the graph is a local cache, not a committed artifact.
17
17
  * - Graft processes default to telemetry opt-out without touching Graft's settings.
18
18
  *
@@ -175,11 +175,47 @@ export function isMutatingResult(event: { toolName: string; input: Record<string
175
175
  return false;
176
176
  }
177
177
 
178
+ const GRAFT_PROVIDER_FOR_SELESAI_PROVIDER: Record<string, "openai" | "anthropic" | "litellm" | "orcarouter"> = {
179
+ anthropic: "anthropic",
180
+ litellm: "litellm",
181
+ openai: "openai",
182
+ orcarouter: "orcarouter",
183
+ tokenin: "litellm",
184
+ };
185
+
186
+ /** Resolve the active Selesai model into the isolated environment Graft needs. */
187
+ async function activeModelGraftEnvironment(ctx: ExtensionContext): Promise<NodeJS.ProcessEnv | undefined> {
188
+ const model = ctx.model;
189
+ const provider = model ? GRAFT_PROVIDER_FOR_SELESAI_PROVIDER[model.provider] : undefined;
190
+ if (!provider) return undefined;
191
+
192
+ let auth: Awaited<ReturnType<typeof ctx.modelRegistry.getProviderAuth>>;
193
+ try {
194
+ auth = await ctx.modelRegistry.getProviderAuth(model.provider);
195
+ } catch {
196
+ return undefined;
197
+ }
198
+ const apiKey = auth?.auth.apiKey;
199
+ const baseUrl = auth?.auth.baseUrl;
200
+ if (!apiKey || (provider === "litellm" && !baseUrl)) return undefined;
201
+
202
+ const { GRAFT_PROVIDER: _provider, GRAFT_MODEL: _model, GRAFT_API_KEY: _apiKey, GRAFT_BASE_URL: _baseUrl, ...env } = process.env;
203
+ return {
204
+ ...env,
205
+ GRAFT_PROVIDER: provider,
206
+ GRAFT_MODEL: model.id,
207
+ GRAFT_API_KEY: apiKey,
208
+ ...(baseUrl ? { GRAFT_BASE_URL: baseUrl } : {}),
209
+ };
210
+ }
211
+
178
212
  export default function graftExtension(pi: ExtensionAPI): void {
179
213
  registerBuiltinAgentAugmentation(pi);
180
214
  const exec: ExecLike = (command, args, options) => pi.exec(command, args, options);
181
215
  let session = freshSession();
182
216
  let installPromise: Promise<GraftInstallRun> | undefined;
217
+ let structuralBuildPromise: Promise<GraftRun> | undefined;
218
+ let structuralBuildSession: GraftSession | undefined;
183
219
  const guard = createInjectionGuard();
184
220
 
185
221
  const applyEvent = (ctx: ExtensionContext | undefined, event: GraftEvent): void => {
@@ -241,6 +277,44 @@ export default function graftExtension(pi: ExtensionAPI): void {
241
277
  }
242
278
  };
243
279
 
280
+ /**
281
+ * Start the graph once. Compatible active Selesai models add the deep tier;
282
+ * otherwise the free local structural tier remains usable.
283
+ */
284
+ const autoBuild = (ctx: ExtensionContext, activeSession: GraftSession): Promise<GraftRun> | undefined => {
285
+ if (structuralBuildPromise && structuralBuildSession === activeSession) return structuralBuildPromise;
286
+ if (
287
+ !activeSession.repoRoot ||
288
+ activeSession.probe?.kind !== "ok" ||
289
+ activeSession.presence.built ||
290
+ activeSession.state.name === "failed"
291
+ ) return undefined;
292
+
293
+ const pending = activeModelGraftEnvironment(ctx).then(async (env) => {
294
+ const deep = env !== undefined;
295
+ if (session === activeSession && !activeSession.disposed) applyEvent(ctx, { type: "build-started", deep });
296
+ return runGraft(exec, activeSession.repoRoot!, { kind: "build", deep }, { env });
297
+ });
298
+ structuralBuildPromise = pending;
299
+ structuralBuildSession = activeSession;
300
+ void pending.then(
301
+ (run) => {
302
+ if (session === activeSession && !activeSession.disposed) recordBuild(ctx, run, run.op.kind === "build" && run.op.deep);
303
+ },
304
+ (error) => {
305
+ if (session === activeSession && !activeSession.disposed) {
306
+ applyEvent(ctx, { type: "build-failed", detail: error instanceof Error ? error.message : String(error) });
307
+ }
308
+ },
309
+ ).finally(() => {
310
+ if (structuralBuildPromise === pending && structuralBuildSession === activeSession) {
311
+ structuralBuildPromise = undefined;
312
+ structuralBuildSession = undefined;
313
+ }
314
+ });
315
+ return pending;
316
+ };
317
+
244
318
  /** Install a missing CLI without blocking session startup or racing another session. */
245
319
  const autoInstall = (ctx: ExtensionContext, activeSession: GraftSession): void => {
246
320
  if (ctx.hasUI) ctx.ui.notify("Installing Graft CLI…", "info");
@@ -258,6 +332,7 @@ export default function graftExtension(pi: ExtensionAPI): void {
258
332
  if (ctx.hasUI) ctx.ui.notify("Graft CLI installed, but is not yet usable. Restart Selesai and run /graft doctor.", "warning");
259
333
  return;
260
334
  }
335
+ autoBuild(ctx, activeSession);
261
336
  if (ctx.hasUI) ctx.ui.notify("Graft CLI installed.", "info");
262
337
  })();
263
338
  };
@@ -285,11 +360,14 @@ export default function graftExtension(pi: ExtensionAPI): void {
285
360
  throw new Error(`Graft is not usable here: ${detail} Run /graft doctor for a recovery step.`);
286
361
  }
287
362
  if (!session.presence.built && op.kind !== "check-freshness" && op.kind !== "build") {
288
- throw new Error(
289
- `There is no ${GRAPH_DIR_NAME}/ graph in ${session.repoRoot} yet. ${
290
- recoveryFor(session.state) ?? "Run /graft build to index this repository."
291
- }`,
292
- );
363
+ await autoBuild(ctx, session);
364
+ if (!session.presence.built) {
365
+ throw new Error(
366
+ `There is no ${GRAPH_DIR_NAME}/ graph in ${session.repoRoot} yet. ${
367
+ recoveryFor(session.state) ?? "Run /graft build to retry the structural index."
368
+ }`,
369
+ );
370
+ }
293
371
  }
294
372
  };
295
373
 
@@ -337,6 +415,7 @@ export default function graftExtension(pi: ExtensionAPI): void {
337
415
 
338
416
  await recheck(ctx);
339
417
  if (session.state.name === "unavailable") autoInstall(ctx, session);
418
+ else autoBuild(ctx, session);
340
419
  });
341
420
 
342
421
  pi.on("session_shutdown", (_event: SessionShutdownEvent, ctx: ExtensionContext) => {
@@ -450,9 +529,8 @@ export default function graftExtension(pi: ExtensionAPI): void {
450
529
  * Debounced, idle-aware proactive refresh.
451
530
  *
452
531
  * `agent_end` is the runtime's own idle boundary, so this needs no timer: a
453
- * burst of edits inside one turn costs at most one rebuild, and a structural
454
- * rebuild is free and offline. Refresh never *creates* a graph — an unbuilt
455
- * repository stays unbuilt until the user consents to a build.
532
+ * burst of edits inside one turn costs at most one rebuild. A compatible
533
+ * active Selesai model adds the deep tier; otherwise refresh stays local.
456
534
  */
457
535
  pi.on("agent_end", (_event, ctx) => {
458
536
  if (!session.enabled || session.disposed) return;
@@ -466,9 +544,11 @@ export default function graftExtension(pi: ExtensionAPI): void {
466
544
 
467
545
  void (async () => {
468
546
  try {
469
- const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep: false });
547
+ const env = await activeModelGraftEnvironment(ctx);
548
+ const deep = env !== undefined;
549
+ const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep }, { env });
470
550
  if (session.disposed) return;
471
- recordBuild(ctx, run, false);
551
+ recordBuild(ctx, run, deep);
472
552
  } catch (error) {
473
553
  if (session.disposed) return;
474
554
  applyEvent(ctx, {
@@ -501,8 +581,26 @@ export default function graftExtension(pi: ExtensionAPI): void {
501
581
  installCli,
502
582
  runBuild: async (deep, ctx) => {
503
583
  await ensureReady(ctx, { kind: "build", deep });
584
+ if (!deep) {
585
+ const pending = autoBuild(ctx, session);
586
+ if (pending) return pending;
587
+ }
588
+ const env = deep ? await activeModelGraftEnvironment(ctx) : undefined;
589
+ if (deep && !env) {
590
+ return {
591
+ op: { kind: "build", deep },
592
+ argv: ["graft", "build", "--deep"],
593
+ cwd: session.repoRoot!,
594
+ code: 1,
595
+ stdout: "",
596
+ stderr: "The active Selesai model cannot be used for Graft deep builds.",
597
+ killed: false,
598
+ cancelled: false,
599
+ timedOut: false,
600
+ };
601
+ }
504
602
  applyEvent(ctx, { type: "build-started", deep });
505
- const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep });
603
+ const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep }, { env });
506
604
  recordBuild(ctx, run, deep);
507
605
  return run;
508
606
  },
@@ -511,8 +609,10 @@ export default function graftExtension(pi: ExtensionAPI): void {
511
609
  await ensureReady(ctx, { kind: "build", deep: false });
512
610
  session.lastRefreshAt = Date.now();
513
611
  applyEvent(ctx, { type: "refresh-started" });
514
- const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep: false });
515
- recordBuild(ctx, run, false);
612
+ const env = await activeModelGraftEnvironment(ctx);
613
+ const deep = env !== undefined;
614
+ const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep }, { env });
615
+ recordBuild(ctx, run, deep);
516
616
  return run;
517
617
  },
518
618
  providerEnvNames: () => PROVIDER_ENV_NAMES.filter((name) => Boolean(process.env[name])),
@@ -263,7 +263,7 @@ describe("proactive refresh", () => {
263
263
  await session.reset();
264
264
  });
265
265
 
266
- it("never creates a graph that does not exist", async () => {
266
+ it("automatically creates the initial local structural graph", async () => {
267
267
  process.env.PI_CODING_AGENT_DIR = agentDir;
268
268
  const bare = join(root, "bare-repo");
269
269
  mkdirSync(join(bare, ".selesai"), { recursive: true });
@@ -271,6 +271,10 @@ describe("proactive refresh", () => {
271
271
  const harness = makePi(async (command: string, args: string[]) => {
272
272
  if (command === "git") return { ...OK, stdout: `${bare}\n` };
273
273
  if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
274
+ if (args.includes("build")) {
275
+ mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
276
+ writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
277
+ }
274
278
  return { ...OK, stdout: ASK_JSON };
275
279
  });
276
280
  graftExtension(harness.pi as never);
@@ -279,10 +283,41 @@ describe("proactive refresh", () => {
279
283
  { type: "session_start", reason: "startup" },
280
284
  ctx,
281
285
  );
282
- expect(ctx.statuses.get("graft")).toBe("graft: ○ unbuilt v0.18.0");
283
- const before = harness.exec.mock.calls.length;
284
- await (handlerFor(harness, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, ctx);
285
- expect(harness.exec.mock.calls.length).toBe(before);
286
+ await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0"));
287
+ expect(harness.commandsRun()).toContain("graft build");
288
+ });
289
+
290
+ it("uses the active TokenIn model for the default deep build", async () => {
291
+ process.env.PI_CODING_AGENT_DIR = agentDir;
292
+ const bare = join(root, "deep-repo");
293
+ mkdirSync(join(bare, ".selesai"), { recursive: true });
294
+ writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
295
+ const harness = makePi(async (command: string, args: string[]) => {
296
+ if (command === "git") return { ...OK, stdout: `${bare}\n` };
297
+ if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
298
+ if (args.includes("build")) {
299
+ mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
300
+ writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
301
+ writeFileSync(join(bare, "graft", "concept.md"), "# Concept", "utf-8");
302
+ }
303
+ return { ...OK, stdout: ASK_JSON };
304
+ });
305
+ graftExtension(harness.pi as never);
306
+ const ctx = makeCtx({ cwd: bare });
307
+ Object.assign(ctx, {
308
+ model: { provider: "tokenin", id: "deepseek-v4-flash" },
309
+ modelRegistry: {
310
+ getProviderAuth: vi.fn(async () => ({ auth: { apiKey: "test-token", baseUrl: "https://lite.andlet.me/v1" } })),
311
+ },
312
+ });
313
+ await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
314
+ { type: "session_start", reason: "startup" },
315
+ ctx,
316
+ );
317
+ await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.18.0"));
318
+ const build = harness.exec.mock.calls.find((call) => (call[1] as string[]).includes("build"))!;
319
+ expect(build[1]).toEqual(["build", "--deep"]);
320
+ expect(build[2]).toMatchObject({ env: { GRAFT_PROVIDER: "litellm", GRAFT_MODEL: "deepseek-v4-flash" } });
286
321
  });
287
322
 
288
323
  it("re-indexes after edits, shows the sync phase, and returns to fresh", async () => {
@@ -352,7 +387,7 @@ describe("retrieval strategy", () => {
352
387
  session.ctx,
353
388
  );
354
389
 
355
- it("injects a source-backed pack in push mode, which is the default", async () => {
390
+ it("injects a source-backed orientation pack in the default hybrid mode", async () => {
356
391
  const session = await bootSession();
357
392
  const result = await beforeAgentStart(session, "Fix the login crash");
358
393
  expect(result).toBeDefined();
@@ -450,7 +485,7 @@ describe("retrieval strategy", () => {
450
485
  expect(notified(ctx)).toContain("Graft context unavailable");
451
486
  });
452
487
 
453
- it("injects nothing when the graph has never been built, and says how to fix it", async () => {
488
+ it("waits for the initial structural build before injecting context", async () => {
454
489
  process.env.PI_CODING_AGENT_DIR = agentDir;
455
490
  const bare = join(root, "unbuilt-repo");
456
491
  mkdirSync(join(bare, ".selesai"), { recursive: true });
@@ -458,6 +493,10 @@ describe("retrieval strategy", () => {
458
493
  const harness = makePi(async (command: string, args: string[]) => {
459
494
  if (command === "git") return { ...OK, stdout: `${bare}\n` };
460
495
  if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
496
+ if (args.includes("build")) {
497
+ mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
498
+ writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
499
+ }
461
500
  return { ...OK, stdout: ASK_JSON };
462
501
  });
463
502
  graftExtension(harness.pi as never);
@@ -471,7 +510,7 @@ describe("retrieval strategy", () => {
471
510
  { type: "before_agent_start", prompt: "Fix the login crash" },
472
511
  ctx,
473
512
  ),
474
- ).toBeUndefined();
513
+ ).toMatchObject({ message: { customType: "graft-context" } });
475
514
  });
476
515
  });
477
516
 
@@ -523,7 +562,7 @@ describe("tool readiness", () => {
523
562
  ).rejects.toThrow(/Run \/graft doctor/);
524
563
  });
525
564
 
526
- it("tells the user to build before answering from a graph that is not there", async () => {
565
+ it("builds before answering from a graph that is not there", async () => {
527
566
  process.env.PI_CODING_AGENT_DIR = agentDir;
528
567
  const bare = join(root, "unbuilt-tools");
529
568
  mkdirSync(join(bare, ".selesai"), { recursive: true });
@@ -531,6 +570,10 @@ describe("tool readiness", () => {
531
570
  const harness = makePi(async (command: string, args: string[]) => {
532
571
  if (command === "git") return { ...OK, stdout: `${bare}\n` };
533
572
  if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
573
+ if (args.includes("build")) {
574
+ mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
575
+ writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
576
+ }
534
577
  return { ...OK, stdout: ASK_JSON };
535
578
  });
536
579
  graftExtension(harness.pi as never);
@@ -542,7 +585,8 @@ describe("tool readiness", () => {
542
585
  const tool = toolFor(harness, "graft_find_code");
543
586
  await expect(
544
587
  (tool.execute as (...args: unknown[]) => Promise<unknown>)("c", { question: "x" }, undefined, undefined, ctx),
545
- ).rejects.toThrow(/graft build/);
588
+ ).resolves.toBeDefined();
589
+ expect(harness.commandsRun()).toContain("graft build");
546
590
  });
547
591
 
548
592
  it("re-resolves lazily when an earlier attempt found no repository", async () => {
@@ -170,8 +170,8 @@ describe("retrieval mode", () => {
170
170
  expect(isRetrievalMode(undefined)).toBe(false);
171
171
  });
172
172
 
173
- it("defaults to push, the source-backed context mode", () => {
174
- expect(DEFAULT_RETRIEVAL_MODE).toBe("push");
173
+ it("defaults to hybrid, the relevance-gated context mode", () => {
174
+ expect(DEFAULT_RETRIEVAL_MODE).toBe("hybrid");
175
175
  });
176
176
 
177
177
  it("restores the newest mode chosen on this branch, ignoring other custom entries", () => {
@@ -30,7 +30,7 @@ import { INSTALL_COMMAND } from "./cli.ts";
30
30
  export type RetrievalMode = "pull" | "push" | "hybrid";
31
31
 
32
32
  export const RETRIEVAL_MODES: readonly RetrievalMode[] = ["pull", "push", "hybrid"];
33
- export const DEFAULT_RETRIEVAL_MODE: RetrievalMode = "push";
33
+ export const DEFAULT_RETRIEVAL_MODE: RetrievalMode = "hybrid";
34
34
 
35
35
  export function isRetrievalMode(value: unknown): value is RetrievalMode {
36
36
  return typeof value === "string" && (RETRIEVAL_MODES as readonly string[]).includes(value);
@@ -12,8 +12,9 @@
12
12
  * graft_check_freshness → graft check --json
13
13
  *
14
14
  * Every one of them is read-only under Selesai's capability model. Nothing here
15
- * can build, mutate, install, or configure anything — the build operations live
16
- * behind the consent-gated commands in `commands.ts`.
15
+ * can build, mutate, install, or configure anything — the extension builds the
16
+ * graph during session setup, using the active compatible Selesai model for the
17
+ * optional deep tier.
17
18
  */
18
19
 
19
20
  import { StringEnum } from "@earendil-works/pi-ai";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selesai/code",
3
- "version": "0.13.23",
3
+ "version": "0.13.24",
4
4
  "description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
5
5
  "type": "module",
6
6
  "repository": {