@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.
@@ -0,0 +1,621 @@
1
+ /**
2
+ * pi-graft — native Selesai integration for the Graft codebase-context CLI.
3
+ *
4
+ * Graft builds a local, regenerable graph of a repository (deterministic
5
+ * tree-sitter structure, plus optional provider-backed summaries) and answers
6
+ * semantic questions against it. This extension owns that integration the way a
7
+ * Selesai user expects: executable discovery, graph lifecycle, status, consent,
8
+ * semantic tools, retrieval strategy, and mutation-aware freshness — all native,
9
+ * none of it MCP, and none of it touching another agent's configuration.
10
+ *
11
+ * Scope discipline:
12
+ * - Graft's upstream agent-wiring command (`init`) is never invoked.
13
+ * - A missing compatible CLI is automatically installed once through npm;
14
+ * incompatible user installations are never overwritten.
15
+ * - The first build uses Selesai's active compatible model for deep enrichment; otherwise it stays local and structural.
16
+ * - Every tool is read-only; the graph is a local cache, not a committed artifact.
17
+ * - Graft processes default to telemetry opt-out without touching Graft's settings.
18
+ *
19
+ * Lifecycle: the factory only registers. Repository resolution, probing, status,
20
+ * and the telemetry default all begin in `session_start` and are torn down in
21
+ * `session_shutdown`, so no invocation that skips a session starts any work.
22
+ */
23
+
24
+ import { existsSync, readdirSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+ import type { ExtensionAPI, ExtensionContext, SessionStartEvent, SessionShutdownEvent } from "@selesai/code";
28
+ import {
29
+ applyTelemetryDefault,
30
+ GRAPH_DIR_NAME,
31
+ graftFailureMessage,
32
+ installGraft,
33
+ parseAskJson,
34
+ probeGraft,
35
+ runGraft,
36
+ type ExecLike,
37
+ type GraftInstallRun,
38
+ type GraftOp,
39
+ type GraftProbe,
40
+ type GraftRun,
41
+ type TelemetryDefaultHandle,
42
+ } from "./cli.ts";
43
+ import { PROVIDER_ENV_NAMES, registerGraftCommands } from "./commands.ts";
44
+ import {
45
+ buildInjectionPack,
46
+ createInjectionGuard,
47
+ injectionBudget,
48
+ injectionMessage,
49
+ planInjection,
50
+ } from "./prompt.ts";
51
+ import {
52
+ ABSENT_GRAPH,
53
+ DEFAULT_MAX_INJECTION_BYTES,
54
+ DEFAULT_RETRIEVAL_MODE,
55
+ DEFAULT_REFRESH_DEBOUNCE_SECONDS,
56
+ type GraftEvent,
57
+ type GraftSettings,
58
+ type GraftState,
59
+ type GraphPresence,
60
+ INITIAL_GRAFT_STATE,
61
+ INJECTION_ENTRY_TYPE,
62
+ readBranchMode,
63
+ readGraftSettings,
64
+ recoveryFor,
65
+ reduceGraftState,
66
+ type RetrievalMode,
67
+ STATUS_KEY,
68
+ statusText,
69
+ } from "./state.ts";
70
+ import { GRAFT_TOOL_NAMES, registerGraftTools } from "./tools.ts";
71
+
72
+ export type { GraftProbe, GraftRun } from "./cli.ts";
73
+ export type { GraftSettings, GraftStateName, RetrievalMode } from "./state.ts";
74
+
75
+ /** The loaded module is the provider foreground children must load. */
76
+ export const GRAFT_EXTENSION_PATH = fileURLToPath(import.meta.url);
77
+ const BUILTIN_AGENT_AUGMENTATION_REQUEST_EVENT = "pi-subagents:request-builtin-agent-augmentations";
78
+ const GRAFT_AWARE_AGENT_NAMES = ["scout", "reviewer", "worker", "delegate", "oracle"];
79
+
80
+ type BuiltinAgentAugmentationRequest = {
81
+ register(augmentation: {
82
+ id: string;
83
+ agentNames: readonly string[];
84
+ addTools: readonly string[];
85
+ subagentOnlyExtensions: readonly string[];
86
+ }): void;
87
+ };
88
+
89
+ function registerBuiltinAgentAugmentation(pi: ExtensionAPI): void {
90
+ pi.events.on(BUILTIN_AGENT_AUGMENTATION_REQUEST_EVENT, (request: unknown) => {
91
+ if (!request || typeof request !== "object" || typeof (request as BuiltinAgentAugmentationRequest).register !== "function") return;
92
+ (request as BuiltinAgentAugmentationRequest).register({
93
+ id: "pi-graft",
94
+ agentNames: GRAFT_AWARE_AGENT_NAMES,
95
+ addTools: GRAFT_TOOL_NAMES,
96
+ subagentOnlyExtensions: [GRAFT_EXTENSION_PATH],
97
+ });
98
+ });
99
+ }
100
+
101
+ /** Session-scoped state. Created in `session_start`, cleared in `session_shutdown`. */
102
+ interface GraftSession {
103
+ settings: GraftSettings;
104
+ enabled: boolean;
105
+ repoRoot: string | undefined;
106
+ presence: GraphPresence;
107
+ state: GraftState;
108
+ probe: GraftProbe | undefined;
109
+ mode: RetrievalMode;
110
+ telemetry: TelemetryDefaultHandle | undefined;
111
+ disposed: boolean;
112
+ lastRefreshAt: number;
113
+ }
114
+
115
+ function freshSession(): GraftSession {
116
+ return {
117
+ settings: {},
118
+ enabled: false,
119
+ repoRoot: undefined,
120
+ presence: ABSENT_GRAPH,
121
+ state: INITIAL_GRAFT_STATE,
122
+ probe: undefined,
123
+ mode: DEFAULT_RETRIEVAL_MODE,
124
+ telemetry: undefined,
125
+ disposed: true,
126
+ lastRefreshAt: 0,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Best-effort graph detection on disk.
132
+ *
133
+ * `ponytail:` a filesystem check, not `graft check`. It answers "is there a
134
+ * graph here" cheaply, which is what a session-start status line needs; it does
135
+ * not pretend to answer "is it current" — that is `graft_check_freshness`'s job,
136
+ * for which Graft itself is authoritative.
137
+ */
138
+ export function detectGraphPresence(repoRoot: string): GraphPresence {
139
+ if (!repoRoot) return ABSENT_GRAPH;
140
+ const graphDir = join(repoRoot, GRAPH_DIR_NAME);
141
+ const built = existsSync(join(graphDir, ".graph", "wiring.json")) || existsSync(join(graphDir, "INDEX.md"));
142
+ if (!built) return ABSENT_GRAPH;
143
+ let deep = false;
144
+ try {
145
+ deep = readdirSync(graphDir).some((entry) => entry.endsWith(".md") && entry !== "INDEX.md");
146
+ } catch {
147
+ deep = false;
148
+ }
149
+ return { built: true, deep };
150
+ }
151
+
152
+ /**
153
+ * Bash commands that surely change the working tree.
154
+ *
155
+ * `ponytail:` a lexical list, not a shell parser, and deliberately biased toward
156
+ * false negatives. A missed mutation costs only a stale status label: Graft
157
+ * refreshes its graph against the working tree before every query, so retrieval
158
+ * correctness never depends on this. Upgrade path is a real parser if the label
159
+ * ever becomes load-bearing.
160
+ */
161
+ const MUTATING_BASH =
162
+ /(^|[;&|]\s*)(rm|rmdir|mv|cp|mkdir|touch|chmod|chown|ln|tee|truncate|patch|dd)\b|>>?\s*[^&|\s]|sed\s+-i|perl\s+-i|\bgit\s+(add|rm|mv|checkout|switch|restore|apply|commit|merge|rebase|cherry-pick|reset|stash|clean|revert)\b|\b(npm|pnpm|yarn|bun)\s+(install|i|add|remove|uninstall|update|upgrade)\b|\b(pip|poetry|uv|cargo|go|gem|composer)\s+(install|add|get|remove|update)\b/;
163
+
164
+ export function bashLooksMutating(command: string): boolean {
165
+ return MUTATING_BASH.test(command);
166
+ }
167
+
168
+ /** True when a successful tool result changed the working tree. */
169
+ export function isMutatingResult(event: { toolName: string; input: Record<string, unknown> }): boolean {
170
+ if (event.toolName === "edit" || event.toolName === "write") return true;
171
+ if (event.toolName === "bash") {
172
+ const command = event.input.command;
173
+ return typeof command === "string" && bashLooksMutating(command);
174
+ }
175
+ return false;
176
+ }
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
+
212
+ export default function graftExtension(pi: ExtensionAPI): void {
213
+ registerBuiltinAgentAugmentation(pi);
214
+ const exec: ExecLike = (command, args, options) => pi.exec(command, args, options);
215
+ let session = freshSession();
216
+ let installPromise: Promise<GraftInstallRun> | undefined;
217
+ let structuralBuildPromise: Promise<GraftRun> | undefined;
218
+ let structuralBuildSession: GraftSession | undefined;
219
+ const guard = createInjectionGuard();
220
+
221
+ const applyEvent = (ctx: ExtensionContext | undefined, event: GraftEvent): void => {
222
+ session.state = reduceGraftState(session.state, event);
223
+ if (ctx?.hasUI) ctx.ui.setStatus(STATUS_KEY, statusText(session.state));
224
+ };
225
+
226
+ const setMode = (mode: RetrievalMode, ctx?: ExtensionContext): void => {
227
+ session.mode = mode;
228
+ if (ctx?.hasUI) ctx.ui.setStatus(`${STATUS_KEY}-mode`, `graft mode: ${mode}`);
229
+ };
230
+
231
+ /** Share one global npm install between startup and a concurrent `/graft` command. */
232
+ const installCli = (ctx: ExtensionContext): Promise<GraftInstallRun> => {
233
+ if (installPromise) return installPromise;
234
+ const pending = installGraft(exec, ctx.cwd);
235
+ installPromise = pending;
236
+ void pending.finally(() => {
237
+ if (installPromise === pending) installPromise = undefined;
238
+ });
239
+ return pending;
240
+ };
241
+
242
+ /** Resolve the repository root the way Graft does: the Git working tree. */
243
+ const resolveRepoRoot = async (ctx: ExtensionContext): Promise<string | undefined> => {
244
+ try {
245
+ const result = await exec("git", ["rev-parse", "--show-toplevel"], { cwd: ctx.cwd, timeout: 5_000 });
246
+ if (result.killed || result.code !== 0) return undefined;
247
+ return result.stdout.trim() || undefined;
248
+ } catch {
249
+ return undefined;
250
+ }
251
+ };
252
+
253
+ /**
254
+ * Re-resolve repository, executable, and graph presence.
255
+ *
256
+ * Called from `session_start`, from the `/graft` commands, and lazily before
257
+ * the first tool call. Never from the factory, never on a timer.
258
+ */
259
+ const recheck = async (ctx: ExtensionContext): Promise<void> => {
260
+ if (!session.enabled || session.disposed) return;
261
+ const repoRoot = await resolveRepoRoot(ctx);
262
+ session.repoRoot = repoRoot;
263
+ applyEvent(ctx, { type: "resolved", gitRepo: repoRoot !== undefined });
264
+ if (!repoRoot) {
265
+ session.probe = undefined;
266
+ return;
267
+ }
268
+ session.presence = detectGraphPresence(repoRoot);
269
+ const probe = await probeGraft(exec, repoRoot, ctx.signal);
270
+ session.probe = probe;
271
+ if (probe.kind === "ok") {
272
+ applyEvent(ctx, { type: "probe-ok", version: probe.version, graph: session.presence });
273
+ } else if (probe.kind === "missing") {
274
+ applyEvent(ctx, { type: "probe-missing", detail: probe.detail });
275
+ } else {
276
+ applyEvent(ctx, { type: "probe-incompatible", version: probe.version, detail: probe.detail });
277
+ }
278
+ };
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
+
318
+ /** Install a missing CLI without blocking session startup or racing another session. */
319
+ const autoInstall = (ctx: ExtensionContext, activeSession: GraftSession): void => {
320
+ if (ctx.hasUI) ctx.ui.notify("Installing Graft CLI…", "info");
321
+ void (async () => {
322
+ const install = await installCli(ctx);
323
+ if (session !== activeSession || activeSession.disposed) return;
324
+ if (install.code !== 0 || install.killed) {
325
+ const detail = (install.stderr.trim() || install.stdout.trim() || `exit status ${install.code}`).split("\n")[0];
326
+ if (ctx.hasUI) ctx.ui.notify(`Graft CLI install failed: ${detail}`, "warning");
327
+ return;
328
+ }
329
+ await recheck(ctx);
330
+ if (session !== activeSession || activeSession.disposed) return;
331
+ if (session.state.name === "unavailable" || session.state.name === "incompatible") {
332
+ if (ctx.hasUI) ctx.ui.notify("Graft CLI installed, but is not yet usable. Restart Selesai and run /graft doctor.", "warning");
333
+ return;
334
+ }
335
+ autoBuild(ctx, activeSession);
336
+ if (ctx.hasUI) ctx.ui.notify("Graft CLI installed.", "info");
337
+ })();
338
+ };
339
+
340
+ /**
341
+ * Refuse a call that cannot produce an honest answer, with the recovery step
342
+ * for the current state. `graft check` is the exception among reads: it is the
343
+ * drift report, so it is allowed to answer for a graph that is not there.
344
+ */
345
+ const ensureReady = async (ctx: ExtensionContext, op: GraftOp): Promise<void> => {
346
+ if (session.disposed) throw new Error("Graft is not ready: no active Selesai session.");
347
+ if (!session.enabled) {
348
+ throw new Error(
349
+ "The Graft extension is disabled by settings. Set `graft.enabled: true` in Selesai settings to use its tools.",
350
+ );
351
+ }
352
+ // Re-resolve when an earlier attempt could not resolve anything: the user
353
+ // may have installed the CLI or opened the repository since.
354
+ if (!session.probe || session.repoRoot === undefined) await recheck(ctx);
355
+ if (!session.repoRoot) {
356
+ throw new Error("Graft needs a Git repository, and this working directory is not inside one. Run /graft doctor.");
357
+ }
358
+ if (session.probe?.kind !== "ok") {
359
+ const detail = session.probe?.detail ?? "the graft executable is unavailable";
360
+ throw new Error(`Graft is not usable here: ${detail} Run /graft doctor for a recovery step.`);
361
+ }
362
+ if (!session.presence.built && op.kind !== "check-freshness" && op.kind !== "build") {
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
+ }
371
+ }
372
+ };
373
+
374
+ const runOp = async (ctx: ExtensionContext, op: GraftOp, signal?: AbortSignal): Promise<GraftRun> => {
375
+ await ensureReady(ctx, op);
376
+ return runGraft(exec, session.repoRoot!, op, { signal });
377
+ };
378
+
379
+ /** A read-only query answered, and Graft refreshed the graph before answering. */
380
+ const noteQuery = (ctx: ExtensionContext, run: GraftRun): void => {
381
+ if (run.code !== 0) return;
382
+ if (run.op.kind === "check-freshness" || run.op.kind === "build") return;
383
+ session.presence = detectGraphPresence(session.repoRoot ?? ctx.cwd);
384
+ applyEvent(ctx, { type: "query-succeeded", presence: session.presence });
385
+ };
386
+
387
+ const recordBuild = (ctx: ExtensionContext, run: GraftRun, deep: boolean): void => {
388
+ session.presence = detectGraphPresence(session.repoRoot ?? ctx.cwd);
389
+ applyEvent(
390
+ ctx,
391
+ run.code === 0
392
+ ? { type: "build-succeeded", deep, presence: session.presence }
393
+ : { type: "build-failed", detail: graftFailureMessage(run) },
394
+ );
395
+ };
396
+
397
+ // -------------------------------------------------------------------------
398
+ // Lifecycle
399
+ // -------------------------------------------------------------------------
400
+
401
+ pi.on("session_start", async (event: SessionStartEvent, ctx: ExtensionContext) => {
402
+ // A start without a preceding shutdown (a reload that raced one, a second
403
+ // start in the same process) must not leak the previous defaults.
404
+ session.telemetry?.restore();
405
+ session = freshSession();
406
+ session.disposed = false;
407
+ session.settings = readGraftSettings({ cwd: ctx.cwd, trusted: ctx.isProjectTrusted() });
408
+ session.enabled = session.settings.enabled !== false;
409
+ if (!session.enabled) {
410
+ if (ctx.hasUI) ctx.ui.setStatus(STATUS_KEY, undefined);
411
+ return;
412
+ }
413
+ if (session.settings.telemetry !== "inherit") session.telemetry = applyTelemetryDefault();
414
+ setMode(readBranchMode(ctx.sessionManager.getBranch()) ?? session.settings.mode ?? DEFAULT_RETRIEVAL_MODE, ctx);
415
+
416
+ await recheck(ctx);
417
+ if (session.state.name === "unavailable") autoInstall(ctx, session);
418
+ else autoBuild(ctx, session);
419
+ });
420
+
421
+ pi.on("session_shutdown", (_event: SessionShutdownEvent, ctx: ExtensionContext) => {
422
+ session.disposed = true;
423
+ session.telemetry?.restore();
424
+ if (ctx.hasUI) {
425
+ ctx.ui.setStatus(STATUS_KEY, undefined);
426
+ ctx.ui.setStatus(`${STATUS_KEY}-mode`, undefined);
427
+ }
428
+ guard.beginTurn();
429
+ session = freshSession();
430
+ });
431
+
432
+ pi.on("turn_start", () => {
433
+ guard.beginTurn();
434
+ });
435
+
436
+ // -------------------------------------------------------------------------
437
+ // Retrieval strategy
438
+ // -------------------------------------------------------------------------
439
+
440
+ pi.on("before_agent_start", async (event, ctx) => {
441
+ if (!session.enabled || session.disposed) return;
442
+ const plan = planInjection({
443
+ mode: session.mode,
444
+ prompt: event.prompt,
445
+ state: session.state,
446
+ enabled: session.enabled,
447
+ });
448
+ if (!plan.inject || !guard.shouldInject()) return;
449
+
450
+ try {
451
+ const budget = injectionBudget(
452
+ plan.mode,
453
+ session.settings.maxInjectionBytes ?? DEFAULT_MAX_INJECTION_BYTES,
454
+ );
455
+ const run = await runOp(
456
+ ctx,
457
+ { kind: "find-code", question: plan.query, limit: budget.limit, in: session.settings.in },
458
+ ctx.signal,
459
+ );
460
+ if (run.code !== 0) {
461
+ // The turn proceeds without context, but not silently: a failed
462
+ // lookup the user cannot see is a context gap they cannot explain.
463
+ if (ctx.hasUI) ctx.ui.notify(`Graft context unavailable: ${graftFailureMessage(run)}`, "warning");
464
+ return;
465
+ }
466
+ if (session.disposed) return;
467
+ noteQuery(ctx, run);
468
+
469
+ const payload = parseAskJson(run.stdout);
470
+ if (!payload) return;
471
+ const pack = buildInjectionPack(payload, {
472
+ maxBytes: budget.maxBytes,
473
+ minCoverage: session.settings.minCoverage,
474
+ });
475
+ // A low-coverage pack is noise the model would have to reason around, so
476
+ // it is dropped rather than injected.
477
+ if (!pack) return;
478
+
479
+ guard.markInjected();
480
+ const references = pack.references.map((r) => (r.span ? `${r.path}:${r.span}` : r.path));
481
+ pi.appendEntry(INJECTION_ENTRY_TYPE, {
482
+ mode: plan.mode,
483
+ query: plan.query,
484
+ state: session.state.name,
485
+ references,
486
+ bytes: pack.bytes,
487
+ truncated: pack.truncated,
488
+ coverage: pack.coverage,
489
+ });
490
+
491
+ return {
492
+ message: {
493
+ customType: "graft-context",
494
+ content: injectionMessage({ pack, mode: plan.mode, state: session.state.name }),
495
+ display: true,
496
+ details: {
497
+ mode: plan.mode,
498
+ references: pack.references,
499
+ bytes: pack.bytes,
500
+ truncated: pack.truncated,
501
+ coverage: pack.coverage,
502
+ },
503
+ },
504
+ };
505
+ } catch (error) {
506
+ // Injection must never break a turn: a failed Graft query just means the
507
+ // model starts without a pack, exactly as in pull mode.
508
+ if (ctx.hasUI) {
509
+ ctx.ui.notify(
510
+ `Graft context unavailable: ${error instanceof Error ? error.message : String(error)}`,
511
+ "warning",
512
+ );
513
+ }
514
+ return;
515
+ }
516
+ });
517
+
518
+ // -------------------------------------------------------------------------
519
+ // Mutation awareness
520
+ // -------------------------------------------------------------------------
521
+
522
+ pi.on("tool_result", async (event, ctx) => {
523
+ if (!session.enabled || session.disposed || event.isError) return;
524
+ if (!isMutatingResult({ toolName: event.toolName, input: event.input })) return;
525
+ applyEvent(ctx, { type: "mutation-observed" });
526
+ });
527
+
528
+ /**
529
+ * Debounced, idle-aware proactive refresh.
530
+ *
531
+ * `agent_end` is the runtime's own idle boundary, so this needs no timer: a
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.
534
+ */
535
+ pi.on("agent_end", (_event, ctx) => {
536
+ if (!session.enabled || session.disposed) return;
537
+ if (session.state.name !== "changed" || !session.presence.built) return;
538
+ const debounceMs = (session.settings.refreshDebounceSeconds ?? DEFAULT_REFRESH_DEBOUNCE_SECONDS) * 1000;
539
+ if (Date.now() - session.lastRefreshAt < debounceMs) return;
540
+ session.lastRefreshAt = Date.now();
541
+ if (ctx.hasUI) {
542
+ ctx.ui.setStatus(STATUS_KEY, statusText(reduceGraftState(session.state, { type: "refresh-started" })));
543
+ }
544
+
545
+ void (async () => {
546
+ try {
547
+ const env = await activeModelGraftEnvironment(ctx);
548
+ const deep = env !== undefined;
549
+ const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep }, { env });
550
+ if (session.disposed) return;
551
+ recordBuild(ctx, run, deep);
552
+ } catch (error) {
553
+ if (session.disposed) return;
554
+ applyEvent(ctx, {
555
+ type: "build-failed",
556
+ detail: error instanceof Error ? error.message : String(error),
557
+ });
558
+ }
559
+ })();
560
+ });
561
+
562
+ // -------------------------------------------------------------------------
563
+ // Registration
564
+ // -------------------------------------------------------------------------
565
+
566
+ registerGraftTools(pi, {
567
+ run: runOp,
568
+ settings: () => session.settings,
569
+ state: () => session.state,
570
+ noteQuery,
571
+ });
572
+
573
+ registerGraftCommands(pi, {
574
+ recheck,
575
+ state: () => session.state,
576
+ settings: () => session.settings,
577
+ mode: () => session.mode,
578
+ setMode,
579
+ repoRoot: () => session.repoRoot,
580
+ graphPresence: () => session.presence,
581
+ installCli,
582
+ runBuild: async (deep, ctx) => {
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
+ }
602
+ applyEvent(ctx, { type: "build-started", deep });
603
+ const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep }, { env });
604
+ recordBuild(ctx, run, deep);
605
+ return run;
606
+ },
607
+ runRefresh: async (ctx) => {
608
+ if (!session.presence.built) return undefined;
609
+ await ensureReady(ctx, { kind: "build", deep: false });
610
+ session.lastRefreshAt = Date.now();
611
+ applyEvent(ctx, { type: "refresh-started" });
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);
616
+ return run;
617
+ },
618
+ providerEnvNames: () => PROVIDER_ENV_NAMES.filter((name) => Boolean(process.env[name])),
619
+ telemetryDefaultApplied: () => session.telemetry !== undefined,
620
+ });
621
+ }