@flyingrobots/graft 0.3.5 → 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 +69 -0
  3. package/CODE_OF_CONDUCT.md +65 -0
  4. package/README.md +153 -17
  5. package/bin/graft.js +4 -11
  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 +17 -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 +86 -0
  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 +67 -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 +704 -45
  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 +136 -0
  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 +49 -0
  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 +513 -0
  108. package/src/warp/observers.ts +105 -0
  109. package/src/warp/open.ts +31 -0
  110. package/src/warp/plumbing.d.ts +15 -0
  111. package/src/warp/writer-id.ts +30 -0
package/src/mcp/server.ts CHANGED
@@ -1,18 +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
+ import type WarpApp from "@git-stunts/git-warp";
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";
16
46
 
17
47
  // Tool definitions — each file exports a ToolDefinition object
18
48
  import { safeReadTool } from "./tools/safe-read.js";
@@ -26,11 +56,33 @@ import { doctorTool } from "./tools/doctor.js";
26
56
  import { statsTool } from "./tools/stats.js";
27
57
  import { explainTool } from "./tools/explain.js";
28
58
  import { setBudgetTool } from "./tools/budget.js";
59
+ import { sinceTool } from "./tools/since.js";
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";
29
81
 
30
82
  export type { McpToolResult, ToolHandler, ToolContext };
31
83
 
32
84
  /** All registered tool definitions. Add new tools here. */
33
- const TOOL_REGISTRY: readonly ToolDefinition[] = [
85
+ export const TOOL_REGISTRY: readonly ToolDefinition[] = [
34
86
  safeReadTool,
35
87
  fileOutlineTool,
36
88
  readRangeTool,
@@ -40,69 +92,664 @@ const TOOL_REGISTRY: readonly ToolDefinition[] = [
40
92
  stateSaveTool,
41
93
  stateLoadTool,
42
94
  doctorTool,
95
+ activityViewTool,
96
+ causalStatusTool,
97
+ causalAttachTool,
43
98
  statsTool,
44
99
  explainTool,
45
100
  setBudgetTool,
101
+ sinceTool,
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,
46
128
  ];
47
129
 
48
130
  export interface GraftServer {
49
131
  getRegisteredTools(): string[];
50
132
  callTool(name: string, args: Record<string, unknown>): Promise<McpToolResult>;
51
133
  injectSessionMessages(count: number): void;
134
+ getWorkspaceStatus(): import("./workspace-router.js").WorkspaceStatus;
135
+ getRuntimeCausalContext(): import("./runtime-causal-context.js").RuntimeCausalContext | null;
52
136
  getMcpServer(): McpServer;
53
137
  }
54
138
 
55
- export function createGraftServer(): GraftServer {
56
- const mcpServer = new McpServer({ name: "graft", version: "0.0.0" });
57
- const session = new SessionTracker();
58
- const sessionId = crypto.randomUUID();
59
- const projectRoot = process.cwd();
60
- const graftDir = path.join(projectRoot, ".graft");
61
- const metrics = new Metrics();
62
- 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
+ }
63
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
+ });
64
223
  const handlers = new Map<string, ToolHandler>();
65
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
+ }>();
66
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
+ }
67
274
 
68
- 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
+ }
69
280
  seq++;
70
- const { result, textBytes } = buildReceiptResult(tool, data, {
71
- 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,
72
291
  metrics: metrics.snapshot(),
73
- tripwires: session.checkTripwires(),
292
+ tripwires,
74
293
  budget: session.getBudget(),
75
294
  });
76
- metrics.addBytesReturned(textBytes);
295
+ invocation.response = {
296
+ receipt,
297
+ tripwireSignals: tripwires.map((wire) => wire.signal),
298
+ };
299
+ metrics.recordToolResult(tool, textBytes);
77
300
  session.recordBytesConsumed(textBytes);
78
301
  return result;
79
302
  }
80
303
 
81
- const ctx: ToolContext = { projectRoot, graftDir, session, cache, metrics, respond, resolvePath: createPathResolver(projectRoot), fs: nodeFs, codec };
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
+ }
510
+ }
511
+
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
+ }
583
+
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
+ }
82
716
 
83
- function wrapWithPolicyCheck(toolName: string, inner: ToolHandler): ToolHandler {
84
- return (args: Record<string, unknown>) => {
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>) => {
85
739
  const rawPath = args["path"] as string | undefined;
86
740
  if (rawPath === undefined) return inner(args);
87
741
  const filePath = ctx.resolvePath(rawPath);
88
742
  let content: string;
89
743
  try {
90
- content = ctx.fs.readFileSync(filePath, "utf-8");
744
+ content = await ctx.fs.readFile(filePath, "utf-8");
91
745
  } catch {
92
746
  // File unreadable — let the inner handler deal with the error
93
747
  return inner(args);
94
748
  }
95
749
  const actual = { lines: content.split("\n").length, bytes: Buffer.byteLength(content) };
96
- const budget = session.getBudget();
97
- const policy = evaluatePolicy(
98
- { path: filePath, lines: actual.lines, bytes: actual.bytes },
99
- {
100
- sessionDepth: session.getSessionDepth(),
101
- budgetRemaining: budget?.remaining,
102
- },
103
- );
750
+ const policy = evaluateMcpPolicy(ctx, filePath, actual);
104
751
  if (policy instanceof RefusedResult) {
105
- metrics.recordRefusal();
752
+ ctx.metrics.recordRefusal();
106
753
  return respond(toolName, {
107
754
  path: filePath,
108
755
  projection: "refused",
@@ -116,7 +763,9 @@ export function createGraftServer(): GraftServer {
116
763
  };
117
764
  }
118
765
 
119
- for (const def of TOOL_REGISTRY) {
766
+ const activeToolRegistry = mode === "daemon" ? ALL_TOOL_REGISTRY : TOOL_REGISTRY;
767
+
768
+ for (const def of activeToolRegistry) {
120
769
  const rawHandler = def.createHandler(ctx);
121
770
  const handler = def.policyCheck === true ? wrapWithPolicyCheck(def.name, rawHandler) : rawHandler;
122
771
  handlers.set(def.name, handler);
@@ -125,20 +774,24 @@ export function createGraftServer(): GraftServer {
125
774
  const zodSchema = z.object(def.schema).strict();
126
775
  schemas.set(def.name, zodSchema);
127
776
  mcpServer.registerTool(def.name, { description: def.description, inputSchema: def.schema }, async (args) => {
128
- session.recordMessage();
129
- session.recordToolCall(def.name);
130
- const parsed: Record<string, unknown> = zodSchema.parse(args);
131
- return handler(parsed);
777
+ return invokeTool(def.name, handler, args, zodSchema);
132
778
  });
133
779
  } else {
134
780
  mcpServer.registerTool(def.name, { description: def.description }, async () => {
135
- session.recordMessage();
136
- session.recordToolCall(def.name);
137
- return handler({});
781
+ return invokeTool(def.name, handler, {});
138
782
  });
139
783
  }
140
784
  }
141
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
+
142
795
  return {
143
796
  getRegisteredTools(): string[] {
144
797
  return [...handlers.keys()];
@@ -148,16 +801,22 @@ export function createGraftServer(): GraftServer {
148
801
  if (handler === undefined) {
149
802
  throw new Error(`Unknown tool: ${name}`);
150
803
  }
151
- session.recordMessage();
152
- session.recordToolCall(name);
153
804
  const schema = schemas.get(name);
154
- const parsed: Record<string, unknown> = schema !== undefined ? schema.parse(args) : args;
155
- return handler(parsed);
805
+ return invokeTool(name, handler, args, schema);
156
806
  },
157
807
  injectSessionMessages(count: number): void {
158
808
  for (let i = 0; i < count; i++) {
159
- 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;
160
818
  }
819
+ return workspaceRouter.captureExecutionContext().getCausalContext();
161
820
  },
162
821
  getMcpServer(): McpServer {
163
822
  return mcpServer;