@flyingrobots/graft 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/ARCHITECTURE.md +386 -0
  2. package/CHANGELOG.md +47 -0
  3. package/CODE_OF_CONDUCT.md +65 -0
  4. package/README.md +153 -17
  5. package/bin/graft.js +4 -14
  6. package/docs/ADVANCED_GUIDE.md +49 -0
  7. package/docs/CLI.md +43 -0
  8. package/docs/GUIDE.md +321 -32
  9. package/docs/MCP.md +44 -0
  10. package/package.json +15 -4
  11. package/src/adapters/node-fs.ts +4 -0
  12. package/src/adapters/node-git.ts +47 -0
  13. package/src/adapters/node-process-runner.ts +27 -0
  14. package/src/cli/index-cmd.ts +75 -11
  15. package/src/cli/init.ts +808 -57
  16. package/src/cli/main.ts +437 -0
  17. package/src/contracts/capabilities.ts +341 -0
  18. package/src/contracts/causal-ontology.ts +622 -0
  19. package/src/contracts/causal-surface-next-action.ts +18 -0
  20. package/src/contracts/output-schemas.ts +1169 -0
  21. package/src/git/diff.ts +25 -21
  22. package/src/git/target-git-hook-bootstrap.ts +56 -0
  23. package/src/hooks/posttooluse-read.ts +21 -74
  24. package/src/hooks/pretooluse-read.ts +20 -56
  25. package/src/hooks/read-governor.ts +95 -0
  26. package/src/hooks/read-messages.ts +53 -0
  27. package/src/mcp/burden.ts +123 -0
  28. package/src/mcp/cache.ts +51 -0
  29. package/src/mcp/cached-file.ts +10 -8
  30. package/src/mcp/context.ts +65 -2
  31. package/src/mcp/daemon-control-plane.ts +554 -0
  32. package/src/mcp/daemon-job-scheduler.ts +279 -0
  33. package/src/mcp/daemon-repos.ts +216 -0
  34. package/src/mcp/daemon-server.ts +396 -0
  35. package/src/mcp/daemon-worker-pool.ts +310 -0
  36. package/src/mcp/daemon-worker-process.ts +52 -0
  37. package/src/mcp/metrics.ts +108 -1
  38. package/src/mcp/monitor-tick-job.ts +99 -0
  39. package/src/mcp/persisted-local-history.ts +1246 -0
  40. package/src/mcp/persistent-monitor-runtime.ts +549 -0
  41. package/src/mcp/policy.ts +84 -0
  42. package/src/mcp/receipt.ts +82 -12
  43. package/src/mcp/repo-concurrency.ts +318 -0
  44. package/src/mcp/repo-state.ts +777 -0
  45. package/src/mcp/repo-tool-job.ts +302 -0
  46. package/src/mcp/run-capture-config.ts +33 -0
  47. package/src/mcp/runtime-causal-context.ts +72 -0
  48. package/src/mcp/runtime-observability.ts +219 -0
  49. package/src/mcp/runtime-staged-target.ts +161 -0
  50. package/src/mcp/runtime-workspace-overlay.ts +255 -0
  51. package/src/mcp/semantic-transition-guidance.ts +60 -0
  52. package/src/mcp/semantic-transition-summary.ts +130 -0
  53. package/src/mcp/server.ts +696 -55
  54. package/src/mcp/stdio-server.ts +12 -0
  55. package/src/mcp/stdio.ts +2 -5
  56. package/src/mcp/tools/activity-view.ts +325 -0
  57. package/src/mcp/tools/causal-attach.ts +67 -0
  58. package/src/mcp/tools/causal-status.ts +58 -0
  59. package/src/mcp/tools/changed-since.ts +13 -11
  60. package/src/mcp/tools/code-find.ts +164 -0
  61. package/src/mcp/tools/code-refs.ts +466 -0
  62. package/src/mcp/tools/code-show.ts +252 -0
  63. package/src/mcp/tools/daemon-monitors.ts +14 -0
  64. package/src/mcp/tools/daemon-repos.ts +22 -0
  65. package/src/mcp/tools/daemon-sessions.ts +14 -0
  66. package/src/mcp/tools/daemon-status.ts +12 -0
  67. package/src/mcp/tools/doctor.ts +45 -2
  68. package/src/mcp/tools/explain.ts +4 -0
  69. package/src/mcp/tools/file-outline.ts +7 -3
  70. package/src/mcp/tools/git-files.ts +73 -0
  71. package/src/mcp/tools/graft-diff.ts +12 -4
  72. package/src/mcp/tools/map.ts +92 -38
  73. package/src/mcp/tools/monitor-pause.ts +18 -0
  74. package/src/mcp/tools/monitor-resume.ts +18 -0
  75. package/src/mcp/tools/monitor-start.ts +20 -0
  76. package/src/mcp/tools/monitor-stop.ts +18 -0
  77. package/src/mcp/tools/precision-match.ts +51 -0
  78. package/src/mcp/tools/precision-query.ts +127 -0
  79. package/src/mcp/tools/precision.ts +312 -0
  80. package/src/mcp/tools/run-capture.ts +126 -44
  81. package/src/mcp/tools/safe-read.ts +14 -12
  82. package/src/mcp/tools/since.ts +7 -2
  83. package/src/mcp/tools/state.ts +11 -3
  84. package/src/mcp/tools/stats.ts +5 -1
  85. package/src/mcp/tools/workspace-authorizations.ts +14 -0
  86. package/src/mcp/tools/workspace-authorize.ts +20 -0
  87. package/src/mcp/tools/workspace-bind.ts +25 -0
  88. package/src/mcp/tools/workspace-rebind.ts +25 -0
  89. package/src/mcp/tools/workspace-revoke.ts +18 -0
  90. package/src/mcp/tools/workspace-status.ts +12 -0
  91. package/src/mcp/warp-pool.ts +36 -0
  92. package/src/mcp/workspace-router.ts +984 -0
  93. package/src/operations/file-outline.ts +12 -2
  94. package/src/operations/graft-diff.ts +56 -10
  95. package/src/operations/safe-read.ts +27 -4
  96. package/src/operations/state.ts +6 -9
  97. package/src/parser/lang.ts +19 -3
  98. package/src/parser/outline.ts +191 -2
  99. package/src/parser/types.ts +9 -1
  100. package/src/policy/types.ts +4 -3
  101. package/src/ports/filesystem.ts +1 -0
  102. package/src/ports/git.ts +16 -0
  103. package/src/ports/process-runner.ts +22 -0
  104. package/src/release/security-gate.ts +102 -0
  105. package/src/session/tracker.ts +31 -0
  106. package/src/version.ts +3 -0
  107. package/src/warp/indexer.ts +171 -56
  108. package/src/warp/observers.ts +1 -1
  109. package/src/warp/open.ts +4 -3
  110. package/src/warp/plumbing.d.ts +5 -1
  111. package/src/warp/writer-id.ts +30 -0
package/src/mcp/server.ts CHANGED
@@ -1,20 +1,48 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import * as crypto from "node:crypto";
2
3
  import * as path from "node:path";
3
4
  import { z } from "zod";
4
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
- import { SessionTracker } from "../session/tracker.js";
6
- import { Metrics } from "./metrics.js";
7
- import { ObservationCache } from "./cache.js";
8
6
  import { buildReceiptResult } from "./receipt.js";
9
7
  import type { ToolHandler, ToolContext, ToolDefinition } from "./context.js";
10
- import { createPathResolver } from "./context.js";
11
8
  import type { McpToolResult } from "./receipt.js";
9
+ import { evaluateMcpPolicy } from "./policy.js";
12
10
  import { nodeFs } from "../adapters/node-fs.js";
13
11
  import { CanonicalJsonCodec } from "../adapters/canonical-json.js";
14
- import { evaluatePolicy } from "../policy/evaluate.js";
12
+ import { nodeProcessRunner } from "../adapters/node-process-runner.js";
13
+ import type { ProcessRunner } from "../ports/process-runner.js";
14
+ import { nodeGit } from "../adapters/node-git.js";
15
15
  import { RefusedResult } from "../policy/types.js";
16
16
  import type WarpApp from "@git-stunts/git-warp";
17
17
  import { openWarp } from "../warp/open.js";
18
+ import type { RunCaptureConfig } from "./run-capture-config.js";
19
+ import { resolveRunCaptureConfig } from "./run-capture-config.js";
20
+ import {
21
+ WorkspaceBindingRequiredError,
22
+ WorkspaceCapabilityDeniedError,
23
+ WorkspaceRouter,
24
+ type WorkspaceExecutionContext,
25
+ type WorkspaceSessionMode,
26
+ } from "./workspace-router.js";
27
+ import { DaemonControlPlane, type DaemonRuntimeDescriptor } from "./daemon-control-plane.js";
28
+ import { DaemonRepoOverview } from "./daemon-repos.js";
29
+ import { DaemonJobScheduler } from "./daemon-job-scheduler.js";
30
+ import { InlineDaemonWorkerPool, type DaemonWorkerPool } from "./daemon-worker-pool.js";
31
+ import { PersistentMonitorRuntime } from "./persistent-monitor-runtime.js";
32
+ import { mergeRepoConcurrencySummaryWithLiveSessions } from "./repo-concurrency.js";
33
+ import { OFFLOADED_DAEMON_REPO_TOOL_NAMES, type OffloadedRepoToolName } from "./repo-tool-job.js";
34
+ import {
35
+ RuntimeEventLogger,
36
+ classifyRuntimeFailure,
37
+ ensureGraftDirExcluded,
38
+ resolveRuntimeObservabilityState,
39
+ sanitizeArgKeys,
40
+ type RuntimeObservabilityState,
41
+ } from "./runtime-observability.js";
42
+ import { InMemoryWarpPool, type WarpPool } from "./warp-pool.js";
43
+ import { buildSessionWarpWriterId } from "../warp/writer-id.js";
44
+ import { PersistedLocalHistoryStore } from "./persisted-local-history.js";
45
+ import { GRAFT_VERSION } from "../version.js";
18
46
 
19
47
  // Tool definitions — each file exports a ToolDefinition object
20
48
  import { safeReadTool } from "./tools/safe-read.js";
@@ -30,11 +58,31 @@ import { explainTool } from "./tools/explain.js";
30
58
  import { setBudgetTool } from "./tools/budget.js";
31
59
  import { sinceTool } from "./tools/since.js";
32
60
  import { mapTool } from "./tools/map.js";
61
+ import { codeShowTool } from "./tools/code-show.js";
62
+ import { codeFindTool } from "./tools/code-find.js";
63
+ import { codeRefsTool } from "./tools/code-refs.js";
64
+ import { daemonMonitorsTool } from "./tools/daemon-monitors.js";
65
+ import { daemonReposTool } from "./tools/daemon-repos.js";
66
+ import { daemonSessionsTool } from "./tools/daemon-sessions.js";
67
+ import { daemonStatusTool } from "./tools/daemon-status.js";
68
+ import { monitorPauseTool } from "./tools/monitor-pause.js";
69
+ import { monitorResumeTool } from "./tools/monitor-resume.js";
70
+ import { monitorStartTool } from "./tools/monitor-start.js";
71
+ import { monitorStopTool } from "./tools/monitor-stop.js";
72
+ import { activityViewTool } from "./tools/activity-view.js";
73
+ import { causalStatusTool } from "./tools/causal-status.js";
74
+ import { causalAttachTool } from "./tools/causal-attach.js";
75
+ import { workspaceAuthorizeTool } from "./tools/workspace-authorize.js";
76
+ import { workspaceAuthorizationsTool } from "./tools/workspace-authorizations.js";
77
+ import { workspaceBindTool } from "./tools/workspace-bind.js";
78
+ import { workspaceRevokeTool } from "./tools/workspace-revoke.js";
79
+ import { workspaceStatusTool } from "./tools/workspace-status.js";
80
+ import { workspaceRebindTool } from "./tools/workspace-rebind.js";
33
81
 
34
82
  export type { McpToolResult, ToolHandler, ToolContext };
35
83
 
36
84
  /** All registered tool definitions. Add new tools here. */
37
- const TOOL_REGISTRY: readonly ToolDefinition[] = [
85
+ export const TOOL_REGISTRY: readonly ToolDefinition[] = [
38
86
  safeReadTool,
39
87
  fileOutlineTool,
40
88
  readRangeTool,
@@ -44,83 +92,664 @@ const TOOL_REGISTRY: readonly ToolDefinition[] = [
44
92
  stateSaveTool,
45
93
  stateLoadTool,
46
94
  doctorTool,
95
+ activityViewTool,
96
+ causalStatusTool,
97
+ causalAttachTool,
47
98
  statsTool,
48
99
  explainTool,
49
100
  setBudgetTool,
50
101
  sinceTool,
51
102
  mapTool,
103
+ codeShowTool,
104
+ codeFindTool,
105
+ codeRefsTool,
106
+ ];
107
+
108
+ export const DAEMON_TOOL_REGISTRY: readonly ToolDefinition[] = [
109
+ daemonReposTool,
110
+ daemonStatusTool,
111
+ daemonSessionsTool,
112
+ daemonMonitorsTool,
113
+ monitorStartTool,
114
+ monitorPauseTool,
115
+ monitorResumeTool,
116
+ monitorStopTool,
117
+ workspaceAuthorizeTool,
118
+ workspaceAuthorizationsTool,
119
+ workspaceRevokeTool,
120
+ workspaceBindTool,
121
+ workspaceStatusTool,
122
+ workspaceRebindTool,
123
+ ];
124
+
125
+ export const ALL_TOOL_REGISTRY: readonly ToolDefinition[] = [
126
+ ...TOOL_REGISTRY,
127
+ ...DAEMON_TOOL_REGISTRY,
52
128
  ];
53
129
 
54
130
  export interface GraftServer {
55
131
  getRegisteredTools(): string[];
56
132
  callTool(name: string, args: Record<string, unknown>): Promise<McpToolResult>;
57
133
  injectSessionMessages(count: number): void;
134
+ getWorkspaceStatus(): import("./workspace-router.js").WorkspaceStatus;
135
+ getRuntimeCausalContext(): import("./runtime-causal-context.js").RuntimeCausalContext | null;
58
136
  getMcpServer(): McpServer;
59
137
  }
60
138
 
61
- export function createGraftServer(): GraftServer {
62
- const mcpServer = new McpServer({ name: "graft", version: "0.0.0" });
63
- const session = new SessionTracker();
64
- const sessionId = crypto.randomUUID();
65
- const projectRoot = process.cwd();
66
- const graftDir = path.join(projectRoot, ".graft");
67
- const metrics = new Metrics();
68
- const cache = new ObservationCache();
139
+ export interface CreateGraftServerOptions {
140
+ mode?: WorkspaceSessionMode;
141
+ sessionId?: string;
142
+ projectRoot?: string;
143
+ graftDir?: string;
144
+ env?: Readonly<Record<string, string | undefined>>;
145
+ runCapture?: Partial<RunCaptureConfig>;
146
+ runtimeObservability?: Partial<RuntimeObservabilityState>;
147
+ warpPool?: WarpPool;
148
+ daemonControlPlane?: DaemonControlPlane;
149
+ monitorRuntime?: PersistentMonitorRuntime;
150
+ daemonScheduler?: DaemonJobScheduler;
151
+ daemonWorkerPool?: DaemonWorkerPool;
152
+ daemonRuntime?: (() => DaemonRuntimeDescriptor) | undefined;
153
+ processRunner?: ProcessRunner;
154
+ }
155
+
156
+ export function createGraftServer(options: CreateGraftServerOptions = {}): GraftServer {
157
+ const mcpServer = new McpServer({ name: "graft", version: GRAFT_VERSION });
158
+ const sessionId = options.sessionId ?? crypto.randomUUID();
159
+ const mode = options.mode ?? "repo_local";
160
+ const sessionWarpWriterId = mode === "daemon" ? buildSessionWarpWriterId(sessionId) : undefined;
161
+ const projectRoot = mode === "repo_local" ? (options.projectRoot ?? process.cwd()) : options.projectRoot;
162
+ const graftDir = options.graftDir
163
+ ?? (mode === "repo_local" && projectRoot !== undefined ? path.join(projectRoot, ".graft") : undefined);
164
+ if (graftDir === undefined) {
165
+ throw new Error("daemon mode requires an explicit graftDir");
166
+ }
69
167
  const codec = new CanonicalJsonCodec();
168
+ const runCapture = resolveRunCaptureConfig({
169
+ ...(options.env !== undefined ? { env: options.env } : {}),
170
+ ...(options.runCapture !== undefined ? { overrides: options.runCapture } : {}),
171
+ });
172
+ const observability = resolveRuntimeObservabilityState({
173
+ graftDir,
174
+ ...(options.env !== undefined ? { env: options.env } : {}),
175
+ ...(options.runtimeObservability !== undefined ? { overrides: options.runtimeObservability } : {}),
176
+ });
177
+ const runtimeLogger = new RuntimeEventLogger({
178
+ fs: nodeFs,
179
+ codec,
180
+ logPath: observability.logPath,
181
+ maxBytes: observability.maxBytes,
182
+ });
183
+ const warpPool = options.warpPool ?? new InMemoryWarpPool((cwd, writerId) => openWarp({ cwd, writerId }));
184
+ const processRunner = options.processRunner ?? nodeProcessRunner;
185
+ const persistedLocalHistory = new PersistedLocalHistoryStore({
186
+ fs: nodeFs,
187
+ codec,
188
+ graftDir,
189
+ });
190
+ const daemonScheduler = mode === "daemon" ? (options.daemonScheduler ?? new DaemonJobScheduler()) : null;
191
+ const daemonWorkerPool = mode === "daemon" ? (options.daemonWorkerPool ?? new InlineDaemonWorkerPool()) : null;
192
+ const daemonControlPlane = mode === "daemon"
193
+ ? (options.daemonControlPlane ?? new DaemonControlPlane({
194
+ fs: nodeFs,
195
+ codec,
196
+ git: nodeGit,
197
+ graftDir,
198
+ }))
199
+ : null;
200
+ const monitorRuntime = mode !== "daemon" || daemonControlPlane === null || daemonScheduler === null || daemonWorkerPool === null
201
+ ? null
202
+ : (options.monitorRuntime ?? new PersistentMonitorRuntime({
203
+ fs: nodeFs,
204
+ codec,
205
+ git: nodeGit,
206
+ graftDir,
207
+ controlPlane: daemonControlPlane,
208
+ scheduler: daemonScheduler,
209
+ workerPool: daemonWorkerPool,
210
+ }));
211
+ const daemonStartedAt = new Date().toISOString();
212
+ const daemonRuntime = options.daemonRuntime ?? (() => {
213
+ return {
214
+ transport: "unix_socket",
215
+ sameUserOnly: true,
216
+ socketPath: graftDir,
217
+ mcpPath: "/mcp",
218
+ healthPath: "/healthz",
219
+ activeWarpRepos: warpPool.size(),
220
+ startedAt: daemonStartedAt,
221
+ };
222
+ });
70
223
  const handlers = new Map<string, ToolHandler>();
71
224
  const schemas = new Map<string, z.ZodObject>();
225
+ const executionContextStorage = new AsyncLocalStorage<WorkspaceExecutionContext>();
226
+ const invocationStorage = new AsyncLocalStorage<{
227
+ readonly traceId: string;
228
+ readonly startedAtMs: number;
229
+ response?: { readonly receipt: import("./receipt.js").McpToolReceipt; readonly tripwireSignals: readonly string[] };
230
+ }>();
72
231
  let seq = 0;
232
+ const workspaceRouter = new WorkspaceRouter({
233
+ mode,
234
+ fs: nodeFs,
235
+ git: nodeGit,
236
+ graftDir,
237
+ ...(projectRoot !== undefined ? { projectRoot } : {}),
238
+ warpPool,
239
+ transportSessionId: sessionId,
240
+ ...(sessionWarpWriterId !== undefined ? { warpWriterId: sessionWarpWriterId } : {}),
241
+ ...(daemonControlPlane !== null ? { authorizationPolicy: daemonControlPlane } : {}),
242
+ ...(daemonControlPlane !== null ? { sharedAttachPolicy: daemonControlPlane } : {}),
243
+ persistedLocalHistory,
244
+ });
245
+ const daemonRepoOverview = daemonControlPlane !== null && monitorRuntime !== null
246
+ ? new DaemonRepoOverview({
247
+ controlPlane: daemonControlPlane,
248
+ monitorRuntime,
249
+ })
250
+ : null;
251
+ const runtimeReady = (async () => {
252
+ if (mode === "repo_local" && projectRoot !== undefined) {
253
+ await ensureGraftDirExcluded(projectRoot, graftDir, nodeFs);
254
+ }
255
+ await workspaceRouter.initialize();
256
+ await Promise.all([
257
+ daemonControlPlane?.initialize() ?? Promise.resolve(),
258
+ monitorRuntime?.initialize() ?? Promise.resolve(),
259
+ ]);
260
+ })().then(() => undefined);
261
+
262
+ async function emitRuntimeEvent(event: Parameters<RuntimeEventLogger["log"]>[0]): Promise<void> {
263
+ try {
264
+ await runtimeReady;
265
+ await runtimeLogger.log(event);
266
+ } catch {
267
+ // Observability must never become the reason a tool call fails.
268
+ }
269
+ }
270
+
271
+ function getActiveExecutionContext(): WorkspaceExecutionContext | null {
272
+ return executionContextStorage.getStore() ?? null;
273
+ }
73
274
 
74
- function respond(tool: string, data: Record<string, unknown>): McpToolResult {
275
+ function respond(tool: ToolDefinition["name"], data: Record<string, unknown>): McpToolResult {
276
+ const invocation = invocationStorage.getStore();
277
+ if (invocation === undefined) {
278
+ throw new Error("MCP respond() called outside an active invocation");
279
+ }
75
280
  seq++;
76
- const { result, textBytes } = buildReceiptResult(tool, data, {
77
- sessionId, seq, codec,
281
+ const execution = getActiveExecutionContext();
282
+ const session = execution?.session ?? workspaceRouter.session;
283
+ const metrics = execution?.metrics ?? workspaceRouter.metrics;
284
+ const tripwires = session.checkTripwires();
285
+ const { result, textBytes, receipt } = buildReceiptResult(tool, data, {
286
+ sessionId,
287
+ traceId: invocation.traceId,
288
+ seq,
289
+ latencyMs: Date.now() - invocation.startedAtMs,
290
+ codec,
78
291
  metrics: metrics.snapshot(),
79
- tripwires: session.checkTripwires(),
292
+ tripwires,
80
293
  budget: session.getBudget(),
81
294
  });
82
- metrics.addBytesReturned(textBytes);
295
+ invocation.response = {
296
+ receipt,
297
+ tripwireSignals: tripwires.map((wire) => wire.signal),
298
+ };
299
+ metrics.recordToolResult(tool, textBytes);
83
300
  session.recordBytesConsumed(textBytes);
84
301
  return result;
85
302
  }
86
303
 
87
- // Lazy WARP initialization — only loaded when a WARP-backed tool needs it.
88
- // Single pending promise prevents duplicate instances from concurrent calls.
89
- // On rejection, clear cache so subsequent calls can retry.
90
- let warpPromise: Promise<WarpApp> | null = null;
91
- function getWarp(): Promise<WarpApp> {
92
- warpPromise ??= openWarp({ cwd: projectRoot }).catch((err: unknown) => {
93
- warpPromise = null;
94
- throw err;
95
- });
96
- return warpPromise;
304
+ const ctx: ToolContext = {
305
+ get projectRoot(): string {
306
+ return getActiveExecutionContext()?.projectRoot ?? workspaceRouter.getProjectRoot();
307
+ },
308
+ get graftDir(): string {
309
+ return getActiveExecutionContext()?.graftDir ?? workspaceRouter.graftDir;
310
+ },
311
+ get graftignorePatterns(): readonly string[] {
312
+ return getActiveExecutionContext()?.graftignorePatterns ?? workspaceRouter.getGraftignorePatterns();
313
+ },
314
+ get session() {
315
+ return getActiveExecutionContext()?.session ?? workspaceRouter.session;
316
+ },
317
+ get cache() {
318
+ return getActiveExecutionContext()?.cache ?? workspaceRouter.cache;
319
+ },
320
+ get metrics() {
321
+ return getActiveExecutionContext()?.metrics ?? workspaceRouter.metrics;
322
+ },
323
+ respond,
324
+ resolvePath(relative: string): string {
325
+ return getActiveExecutionContext()?.resolvePath(relative) ?? workspaceRouter.getPathResolver()(relative);
326
+ },
327
+ fs: nodeFs,
328
+ codec,
329
+ process: processRunner,
330
+ git: nodeGit,
331
+ runCapture,
332
+ observability,
333
+ getWarp(): Promise<WarpApp> {
334
+ return getActiveExecutionContext()?.getWarp() ?? workspaceRouter.getWarp();
335
+ },
336
+ getRepoState() {
337
+ return getActiveExecutionContext()?.repoState.getState() ?? workspaceRouter.getRepoState();
338
+ },
339
+ getCausalContext() {
340
+ return getActiveExecutionContext()?.getCausalContext() ?? workspaceRouter.captureExecutionContext().getCausalContext();
341
+ },
342
+ getWorkspaceOverlayFooting() {
343
+ return workspaceRouter.getWorkspaceOverlayFooting();
344
+ },
345
+ getPersistedLocalHistorySummary() {
346
+ return workspaceRouter.getPersistedLocalHistorySummary();
347
+ },
348
+ getPersistedLocalActivityWindow(limit) {
349
+ return workspaceRouter.getPersistedLocalActivityWindow(limit);
350
+ },
351
+ getRepoConcurrencySummary() {
352
+ return (async () => {
353
+ const summary = await workspaceRouter.getRepoConcurrencySummary();
354
+ if (summary === null || daemonControlPlane === null) {
355
+ return summary;
356
+ }
357
+ const status = workspaceRouter.getStatus();
358
+ if (status.bindState !== "bound" || status.repoId === null || status.worktreeId === null) {
359
+ return summary;
360
+ }
361
+ const causalContext = workspaceRouter.captureExecutionContext().getCausalContext();
362
+ return mergeRepoConcurrencySummaryWithLiveSessions({
363
+ currentSummary: summary,
364
+ currentRepoId: status.repoId,
365
+ currentWorktreeId: status.worktreeId,
366
+ currentCheckoutEpochId: causalContext.checkoutEpochId,
367
+ sessions: daemonControlPlane.listSessions(),
368
+ });
369
+ })();
370
+ },
371
+ declareCausalAttach(request) {
372
+ return workspaceRouter.declareAttach(request);
373
+ },
374
+ getWorkspaceStatus() {
375
+ return getActiveExecutionContext()?.status ?? workspaceRouter.getStatus();
376
+ },
377
+ bindWorkspace(request, actionName) {
378
+ return workspaceRouter.bind(request, actionName);
379
+ },
380
+ rebindWorkspace(request, actionName) {
381
+ return workspaceRouter.rebind(request, actionName);
382
+ },
383
+ getDaemonStatus() {
384
+ if (daemonControlPlane === null) {
385
+ throw new Error("daemon_status is only available in daemon mode");
386
+ }
387
+ if (daemonScheduler === null) {
388
+ throw new Error("daemon_status is only available in daemon mode");
389
+ }
390
+ if (daemonWorkerPool === null) {
391
+ throw new Error("daemon_status is only available in daemon mode");
392
+ }
393
+ return Promise.resolve(daemonControlPlane.getStatus(
394
+ daemonRuntime(),
395
+ monitorRuntime?.getCounts(),
396
+ daemonScheduler.getCounts(),
397
+ daemonWorkerPool.getCounts(),
398
+ ));
399
+ },
400
+ listDaemonRepos(filter) {
401
+ if (daemonRepoOverview === null) {
402
+ throw new Error("daemon_repos is only available in daemon mode");
403
+ }
404
+ return daemonRepoOverview.list(filter);
405
+ },
406
+ listDaemonSessions() {
407
+ if (daemonControlPlane === null) {
408
+ throw new Error("daemon_sessions is only available in daemon mode");
409
+ }
410
+ return Promise.resolve(daemonControlPlane.listSessions());
411
+ },
412
+ listDaemonMonitors() {
413
+ if (monitorRuntime === null) {
414
+ throw new Error("daemon_monitors is only available in daemon mode");
415
+ }
416
+ return monitorRuntime.listStatuses();
417
+ },
418
+ startMonitor(request) {
419
+ if (monitorRuntime === null) {
420
+ throw new Error("monitor_start is only available in daemon mode");
421
+ }
422
+ return monitorRuntime.startMonitor(request);
423
+ },
424
+ pauseMonitor(request) {
425
+ if (monitorRuntime === null) {
426
+ throw new Error("monitor_pause is only available in daemon mode");
427
+ }
428
+ return monitorRuntime.pauseMonitor(request);
429
+ },
430
+ resumeMonitor(request) {
431
+ if (monitorRuntime === null) {
432
+ throw new Error("monitor_resume is only available in daemon mode");
433
+ }
434
+ return monitorRuntime.resumeMonitor(request);
435
+ },
436
+ stopMonitor(request) {
437
+ if (monitorRuntime === null) {
438
+ throw new Error("monitor_stop is only available in daemon mode");
439
+ }
440
+ return monitorRuntime.stopMonitor(request);
441
+ },
442
+ listWorkspaceAuthorizations() {
443
+ if (daemonControlPlane === null) {
444
+ throw new Error("workspace_authorizations is only available in daemon mode");
445
+ }
446
+ return daemonControlPlane.listAuthorizedWorkspaces();
447
+ },
448
+ authorizeWorkspace(request) {
449
+ if (daemonControlPlane === null) {
450
+ throw new Error("workspace_authorize is only available in daemon mode");
451
+ }
452
+ return daemonControlPlane.authorizeWorkspace(request);
453
+ },
454
+ revokeWorkspace(request) {
455
+ if (daemonControlPlane === null) {
456
+ throw new Error("workspace_revoke is only available in daemon mode");
457
+ }
458
+ return daemonControlPlane.revokeWorkspace(request);
459
+ },
460
+ };
461
+
462
+ const daemonAlwaysAvailableTools = new Set<string>([
463
+ "daemon_repos",
464
+ "daemon_status",
465
+ "daemon_sessions",
466
+ "daemon_monitors",
467
+ "monitor_start",
468
+ "monitor_pause",
469
+ "monitor_resume",
470
+ "monitor_stop",
471
+ "workspace_authorize",
472
+ "workspace_authorizations",
473
+ "workspace_revoke",
474
+ "workspace_bind",
475
+ "workspace_status",
476
+ "causal_status",
477
+ "workspace_rebind",
478
+ "explain",
479
+ ]);
480
+
481
+ const repoStateOptionalTools = new Set<string>([
482
+ "daemon_repos",
483
+ "daemon_status",
484
+ "daemon_sessions",
485
+ "daemon_monitors",
486
+ "monitor_start",
487
+ "monitor_pause",
488
+ "monitor_resume",
489
+ "monitor_stop",
490
+ "workspace_authorize",
491
+ "workspace_authorizations",
492
+ "workspace_revoke",
493
+ "workspace_bind",
494
+ "workspace_status",
495
+ "workspace_rebind",
496
+ "explain",
497
+ ]);
498
+
499
+ function enforceDaemonToolAccess(name: string): void {
500
+ if (mode !== "daemon") return;
501
+ if (daemonAlwaysAvailableTools.has(name)) {
502
+ return;
503
+ }
504
+ if (!workspaceRouter.isBound()) {
505
+ throw new WorkspaceBindingRequiredError(name);
506
+ }
507
+ if (name === "run_capture" && workspaceRouter.getStatus().capabilityProfile?.runCapture !== true) {
508
+ throw new WorkspaceCapabilityDeniedError(name);
509
+ }
97
510
  }
98
511
 
99
- const ctx: ToolContext = { projectRoot, graftDir, session, cache, metrics, respond, resolvePath: createPathResolver(projectRoot), fs: nodeFs, codec, getWarp };
512
+ const daemonScheduledRepoTools = new Set<string>([
513
+ "safe_read",
514
+ "file_outline",
515
+ "changed_since",
516
+ "graft_diff",
517
+ "graft_since",
518
+ "graft_map",
519
+ "code_show",
520
+ "code_find",
521
+ "code_refs",
522
+ ]);
523
+ const daemonOffloadedRepoTools = new Set<string>(OFFLOADED_DAEMON_REPO_TOOL_NAMES);
524
+ const attributedReadTools = new Set<string>([
525
+ "safe_read",
526
+ "file_outline",
527
+ "read_range",
528
+ ]);
529
+
530
+ function isOffloadedRepoTool(name: string): name is OffloadedRepoToolName {
531
+ return daemonOffloadedRepoTools.has(name);
532
+ }
533
+
534
+ function resolveDaemonOffloadedRepoTool(
535
+ name: string,
536
+ parsed: Record<string, unknown>,
537
+ dirty: boolean,
538
+ ): OffloadedRepoToolName | null {
539
+ if (name === "code_find") {
540
+ return dirty ? "code_find_live" : null;
541
+ }
542
+ if (name === "code_show") {
543
+ return parsed["ref"] === undefined ? "code_show_live" : null;
544
+ }
545
+ return isOffloadedRepoTool(name) ? name : null;
546
+ }
547
+
548
+ function parseToolPayload(result: McpToolResult): Record<string, unknown> | null {
549
+ const textBlock = result.content.find((entry) => entry.type === "text");
550
+ if (textBlock === undefined) {
551
+ return null;
552
+ }
553
+ try {
554
+ return JSON.parse(textBlock.text) as Record<string, unknown>;
555
+ } catch {
556
+ return null;
557
+ }
558
+ }
559
+
560
+ async function invokeTool(
561
+ name: string,
562
+ handler: ToolHandler,
563
+ args: Record<string, unknown>,
564
+ schema?: z.ZodObject,
565
+ ): Promise<McpToolResult> {
566
+ await runtimeReady;
567
+ workspaceRouter.session.recordMessage();
568
+ workspaceRouter.session.recordToolCall(name);
569
+ const traceId = crypto.randomUUID();
570
+ const startedAtMs = Date.now();
571
+ const argKeys = sanitizeArgKeys(args);
572
+
573
+ if (observability.enabled) {
574
+ await emitRuntimeEvent({
575
+ event: "tool_call_started",
576
+ sessionId,
577
+ traceId,
578
+ tool: name,
579
+ argKeys,
580
+ sessionDepth: workspaceRouter.session.getSessionDepth(),
581
+ });
582
+ }
100
583
 
101
- function wrapWithPolicyCheck(toolName: string, inner: ToolHandler): ToolHandler {
102
- return (args: Record<string, unknown>) => {
584
+ const invocation = { traceId, startedAtMs } as {
585
+ readonly traceId: string;
586
+ readonly startedAtMs: number;
587
+ response?: { readonly receipt: import("./receipt.js").McpToolReceipt; readonly tripwireSignals: readonly string[] };
588
+ };
589
+ let execution: WorkspaceExecutionContext | null = null;
590
+
591
+ try {
592
+ const parsed: Record<string, unknown> = schema !== undefined ? schema.parse(args) : args;
593
+ enforceDaemonToolAccess(name);
594
+
595
+ execution = daemonScheduler !== null
596
+ && workspaceRouter.isBound()
597
+ && daemonScheduledRepoTools.has(name)
598
+ ? workspaceRouter.captureExecutionContext()
599
+ : null;
600
+
601
+ const executeHandler = async (): Promise<McpToolResult> => {
602
+ return invocationStorage.run(invocation, async () => {
603
+ if (execution !== null) {
604
+ const activeExecution = execution;
605
+ return executionContextStorage.run(activeExecution, async () => {
606
+ if (!repoStateOptionalTools.has(name)) {
607
+ await activeExecution.repoState.observe();
608
+ }
609
+ return handler(parsed);
610
+ });
611
+ }
612
+ if (workspaceRouter.isBound() && !repoStateOptionalTools.has(name)) {
613
+ await workspaceRouter.observeRepoState();
614
+ }
615
+ return handler(parsed);
616
+ });
617
+ };
618
+
619
+ const result = execution !== null && daemonScheduler !== null
620
+ ? await daemonScheduler.enqueue({
621
+ sessionId,
622
+ sliceId: execution.sliceId,
623
+ repoId: execution.repoId,
624
+ worktreeId: execution.worktreeId,
625
+ tool: name,
626
+ kind: "repo_tool",
627
+ priority: "interactive",
628
+ writerId: execution.warpWriterId,
629
+ }, async () => {
630
+ const activeExecution = execution;
631
+ if (activeExecution !== null && daemonWorkerPool !== null) {
632
+ await activeExecution.repoState.observe();
633
+ const offloadedTool = resolveDaemonOffloadedRepoTool(
634
+ name,
635
+ parsed,
636
+ activeExecution.repoState.getState().dirty,
637
+ );
638
+ if (offloadedTool === null) {
639
+ return executeHandler();
640
+ }
641
+ const cacheSnapshots = typeof parsed["path"] === "string"
642
+ ? (() => {
643
+ const filePath = activeExecution.resolvePath(parsed["path"]);
644
+ const snapshot = activeExecution.cache.snapshotEntry(filePath);
645
+ return snapshot === null ? {} : { [filePath]: snapshot };
646
+ })()
647
+ : undefined;
648
+ const workerResult = await daemonWorkerPool.runRepoTool({
649
+ sessionId,
650
+ workspaceSliceId: activeExecution.sliceId,
651
+ traceId,
652
+ seq: ++seq,
653
+ startedAtMs,
654
+ tool: offloadedTool,
655
+ args: parsed,
656
+ projectRoot: activeExecution.projectRoot,
657
+ graftDir: activeExecution.graftDir,
658
+ graftignorePatterns: activeExecution.graftignorePatterns,
659
+ repoId: activeExecution.repoId,
660
+ worktreeId: activeExecution.worktreeId,
661
+ gitCommonDir: activeExecution.gitCommonDir,
662
+ writerId: activeExecution.warpWriterId,
663
+ capabilityProfile: activeExecution.capabilityProfile,
664
+ repoState: activeExecution.repoState.getState(),
665
+ sessionSnapshot: activeExecution.session.snapshot(),
666
+ metricsSnapshot: activeExecution.metrics.snapshot(),
667
+ ...(cacheSnapshots !== undefined ? { cacheSnapshots } : {}),
668
+ });
669
+ for (const update of workerResult.cacheUpdates) {
670
+ activeExecution.cache.applySnapshot(update.path, update.observation);
671
+ }
672
+ activeExecution.metrics.applyDelta(workerResult.metricsDelta);
673
+ activeExecution.metrics.recordToolResult(name as ToolDefinition["name"], workerResult.textBytes);
674
+ activeExecution.session.recordBytesConsumed(workerResult.textBytes);
675
+ invocation.response = {
676
+ receipt: workerResult.receipt,
677
+ tripwireSignals: workerResult.tripwireSignals,
678
+ };
679
+ return workerResult.result;
680
+ }
681
+ return executeHandler();
682
+ })
683
+ : await executeHandler();
684
+
685
+ if (observability.enabled && invocation.response !== undefined) {
686
+ await emitRuntimeEvent({
687
+ event: "tool_call_completed",
688
+ sessionId,
689
+ traceId,
690
+ seq: invocation.response.receipt.seq,
691
+ tool: name,
692
+ latencyMs: invocation.response.receipt.latencyMs,
693
+ projection: invocation.response.receipt.projection,
694
+ reason: invocation.response.receipt.reason,
695
+ burdenKind: invocation.response.receipt.burden.kind,
696
+ nonReadBurden: invocation.response.receipt.burden.nonRead,
697
+ returnedBytes: invocation.response.receipt.returnedBytes,
698
+ fileBytes: invocation.response.receipt.fileBytes,
699
+ sessionDepth: execution?.session.getSessionDepth() ?? workspaceRouter.session.getSessionDepth(),
700
+ tripwireSignals: invocation.response.tripwireSignals,
701
+ metrics: invocation.response.receipt.cumulative,
702
+ });
703
+ }
704
+
705
+ if (workspaceRouter.isBound() && attributedReadTools.has(name)) {
706
+ const payload = parseToolPayload(result);
707
+ if (payload !== null) {
708
+ await workspaceRouter.noteReadObservation(
709
+ name as "safe_read" | "file_outline" | "read_range",
710
+ parsed,
711
+ payload,
712
+ execution,
713
+ );
714
+ }
715
+ }
716
+
717
+ return result;
718
+ } catch (error) {
719
+ if (observability.enabled) {
720
+ const failure = classifyRuntimeFailure(error);
721
+ await emitRuntimeEvent({
722
+ event: "tool_call_failed",
723
+ sessionId,
724
+ traceId,
725
+ tool: name,
726
+ latencyMs: Date.now() - startedAtMs,
727
+ sessionDepth: execution?.session.getSessionDepth() ?? workspaceRouter.session.getSessionDepth(),
728
+ argKeys,
729
+ errorKind: failure.kind,
730
+ errorName: failure.name,
731
+ });
732
+ }
733
+ throw error;
734
+ }
735
+ }
736
+
737
+ function wrapWithPolicyCheck(toolName: ToolDefinition["name"], inner: ToolHandler): ToolHandler {
738
+ return async (args: Record<string, unknown>) => {
103
739
  const rawPath = args["path"] as string | undefined;
104
740
  if (rawPath === undefined) return inner(args);
105
741
  const filePath = ctx.resolvePath(rawPath);
106
742
  let content: string;
107
743
  try {
108
- content = ctx.fs.readFileSync(filePath, "utf-8");
744
+ content = await ctx.fs.readFile(filePath, "utf-8");
109
745
  } catch {
110
746
  // File unreadable — let the inner handler deal with the error
111
747
  return inner(args);
112
748
  }
113
749
  const actual = { lines: content.split("\n").length, bytes: Buffer.byteLength(content) };
114
- const budget = session.getBudget();
115
- const policy = evaluatePolicy(
116
- { path: filePath, lines: actual.lines, bytes: actual.bytes },
117
- {
118
- sessionDepth: session.getSessionDepth(),
119
- budgetRemaining: budget?.remaining,
120
- },
121
- );
750
+ const policy = evaluateMcpPolicy(ctx, filePath, actual);
122
751
  if (policy instanceof RefusedResult) {
123
- metrics.recordRefusal();
752
+ ctx.metrics.recordRefusal();
124
753
  return respond(toolName, {
125
754
  path: filePath,
126
755
  projection: "refused",
@@ -134,7 +763,9 @@ export function createGraftServer(): GraftServer {
134
763
  };
135
764
  }
136
765
 
137
- for (const def of TOOL_REGISTRY) {
766
+ const activeToolRegistry = mode === "daemon" ? ALL_TOOL_REGISTRY : TOOL_REGISTRY;
767
+
768
+ for (const def of activeToolRegistry) {
138
769
  const rawHandler = def.createHandler(ctx);
139
770
  const handler = def.policyCheck === true ? wrapWithPolicyCheck(def.name, rawHandler) : rawHandler;
140
771
  handlers.set(def.name, handler);
@@ -143,20 +774,24 @@ export function createGraftServer(): GraftServer {
143
774
  const zodSchema = z.object(def.schema).strict();
144
775
  schemas.set(def.name, zodSchema);
145
776
  mcpServer.registerTool(def.name, { description: def.description, inputSchema: def.schema }, async (args) => {
146
- session.recordMessage();
147
- session.recordToolCall(def.name);
148
- const parsed: Record<string, unknown> = zodSchema.parse(args);
149
- return handler(parsed);
777
+ return invokeTool(def.name, handler, args, zodSchema);
150
778
  });
151
779
  } else {
152
780
  mcpServer.registerTool(def.name, { description: def.description }, async () => {
153
- session.recordMessage();
154
- session.recordToolCall(def.name);
155
- return handler({});
781
+ return invokeTool(def.name, handler, {});
156
782
  });
157
783
  }
158
784
  }
159
785
 
786
+ if (observability.enabled) {
787
+ void emitRuntimeEvent({
788
+ event: "session_started",
789
+ sessionId,
790
+ logPath: observability.logPath,
791
+ logPolicy: observability.logPolicy,
792
+ });
793
+ }
794
+
160
795
  return {
161
796
  getRegisteredTools(): string[] {
162
797
  return [...handlers.keys()];
@@ -166,16 +801,22 @@ export function createGraftServer(): GraftServer {
166
801
  if (handler === undefined) {
167
802
  throw new Error(`Unknown tool: ${name}`);
168
803
  }
169
- session.recordMessage();
170
- session.recordToolCall(name);
171
804
  const schema = schemas.get(name);
172
- const parsed: Record<string, unknown> = schema !== undefined ? schema.parse(args) : args;
173
- return handler(parsed);
805
+ return invokeTool(name, handler, args, schema);
174
806
  },
175
807
  injectSessionMessages(count: number): void {
176
808
  for (let i = 0; i < count; i++) {
177
- session.recordMessage();
809
+ workspaceRouter.session.recordMessage();
810
+ }
811
+ },
812
+ getWorkspaceStatus() {
813
+ return workspaceRouter.getStatus();
814
+ },
815
+ getRuntimeCausalContext() {
816
+ if (workspaceRouter.getStatus().bindState !== "bound") {
817
+ return null;
178
818
  }
819
+ return workspaceRouter.captureExecutionContext().getCausalContext();
179
820
  },
180
821
  getMcpServer(): McpServer {
181
822
  return mcpServer;