@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
@@ -0,0 +1,984 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as path from "node:path";
3
+ import type WarpApp from "@git-stunts/git-warp";
4
+ import { ObservationCache } from "./cache.js";
5
+ import { createPathResolver } from "./context.js";
6
+ import { Metrics } from "./metrics.js";
7
+ import { loadProjectGraftignore } from "./policy.js";
8
+ import {
9
+ type PersistedLocalActivityWindow,
10
+ PersistedLocalHistoryAttachUnavailableError,
11
+ PersistedLocalHistoryStore,
12
+ type PersistedLocalHistoryAttachDeclaration,
13
+ type PersistedLocalHistoryContext,
14
+ type PersistedLocalHistorySharedAttachSource,
15
+ type RepoConcurrencySummary,
16
+ type PersistedLocalHistorySummary,
17
+ } from "./persisted-local-history.js";
18
+ import { RepoStateTracker } from "./repo-state.js";
19
+ import { buildRuntimeCausalContext, type RuntimeCausalContext } from "./runtime-causal-context.js";
20
+ import { buildRuntimeStagedTarget } from "./runtime-staged-target.js";
21
+ import {
22
+ buildRuntimeWorkspaceOverlayFooting,
23
+ type GitTransitionHookEvent,
24
+ type RuntimeWorkspaceOverlayFooting,
25
+ } from "./runtime-workspace-overlay.js";
26
+ import { SessionTracker } from "../session/tracker.js";
27
+ import type { FileSystem } from "../ports/filesystem.js";
28
+ import type { GitClient } from "../ports/git.js";
29
+ import type { WarpPool } from "./warp-pool.js";
30
+ import { DEFAULT_WARP_WRITER_ID } from "../warp/writer-id.js";
31
+
32
+ export type WorkspaceSessionMode = "repo_local" | "daemon";
33
+ export type WorkspaceBindState = "bound" | "unbound";
34
+ export type WorkspaceBindAction = "bind" | "rebind";
35
+
36
+ export interface WorkspaceCapabilityProfile {
37
+ readonly boundedReads: boolean;
38
+ readonly structuralTools: boolean;
39
+ readonly precisionTools: boolean;
40
+ readonly stateBookmarks: boolean;
41
+ readonly runtimeLogs: "session_local_only";
42
+ readonly runCapture: boolean;
43
+ }
44
+
45
+ export interface WorkspaceStatus {
46
+ readonly sessionMode: WorkspaceSessionMode;
47
+ readonly bindState: WorkspaceBindState;
48
+ readonly repoId: string | null;
49
+ readonly worktreeId: string | null;
50
+ readonly worktreeRoot: string | null;
51
+ readonly gitCommonDir: string | null;
52
+ readonly graftDir: string | null;
53
+ readonly capabilityProfile: WorkspaceCapabilityProfile | null;
54
+ }
55
+
56
+ export interface WorkspaceActionResult extends WorkspaceStatus {
57
+ readonly ok: boolean;
58
+ readonly action: WorkspaceBindAction;
59
+ readonly freshSessionSlice: boolean;
60
+ readonly errorCode?: string;
61
+ readonly error?: string;
62
+ }
63
+
64
+ export interface CausalAttachResult extends WorkspaceStatus {
65
+ readonly ok: boolean;
66
+ readonly action: "attach";
67
+ readonly persistedLocalHistory: PersistedLocalHistorySummary;
68
+ readonly errorCode?: string;
69
+ readonly error?: string;
70
+ }
71
+
72
+ export interface WorkspaceBindRequest {
73
+ readonly cwd: string;
74
+ readonly worktreeRoot?: string | undefined;
75
+ readonly gitCommonDir?: string | undefined;
76
+ readonly repoId?: string | undefined;
77
+ }
78
+
79
+ export class WorkspaceBindingRequiredError extends Error {
80
+ readonly code = "UNBOUND_SESSION";
81
+
82
+ constructor(toolName: string) {
83
+ super(`Tool ${toolName} requires an active workspace binding. Call workspace_bind first.`);
84
+ this.name = "WorkspaceBindingRequiredError";
85
+ }
86
+ }
87
+
88
+ export class WorkspaceCapabilityDeniedError extends Error {
89
+ readonly code = "CAPABILITY_DENIED";
90
+
91
+ constructor(toolName: string) {
92
+ super(`Tool ${toolName} is not enabled in the daemon default capability profile.`);
93
+ this.name = "WorkspaceCapabilityDeniedError";
94
+ }
95
+ }
96
+
97
+ interface WorkspaceBindError {
98
+ readonly code: string;
99
+ readonly message: string;
100
+ }
101
+
102
+ interface WorkspaceSlice {
103
+ readonly sliceId: string;
104
+ readonly session: SessionTracker;
105
+ readonly cache: ObservationCache;
106
+ readonly metrics: Metrics;
107
+ readonly graftDir: string;
108
+ readonly repoState: RepoStateTracker | null;
109
+ }
110
+
111
+ interface BoundWorkspace {
112
+ readonly repoId: string;
113
+ readonly worktreeId: string;
114
+ readonly worktreeRoot: string;
115
+ readonly gitCommonDir: string;
116
+ readonly graftignorePatterns: readonly string[];
117
+ readonly resolvePath: (input: string) => string;
118
+ readonly capabilityProfile: WorkspaceCapabilityProfile;
119
+ readonly warpWriterId: string;
120
+ readonly transportSessionId: string;
121
+ readonly slice: WorkspaceSlice;
122
+ readonly getWarp: () => Promise<WarpApp>;
123
+ }
124
+
125
+ export interface WorkspaceExecutionContext {
126
+ readonly sliceId: string;
127
+ readonly repoId: string;
128
+ readonly worktreeId: string;
129
+ readonly projectRoot: string;
130
+ readonly worktreeRoot: string;
131
+ readonly gitCommonDir: string;
132
+ readonly graftignorePatterns: readonly string[];
133
+ readonly resolvePath: (input: string) => string;
134
+ readonly capabilityProfile: WorkspaceCapabilityProfile;
135
+ readonly warpWriterId: string;
136
+ getCausalContext(): RuntimeCausalContext;
137
+ readonly status: WorkspaceStatus;
138
+ readonly session: SessionTracker;
139
+ readonly cache: ObservationCache;
140
+ readonly metrics: Metrics;
141
+ readonly graftDir: string;
142
+ readonly repoState: RepoStateTracker;
143
+ readonly getWarp: () => Promise<WarpApp>;
144
+ }
145
+
146
+ type AttributedReadToolName = "safe_read" | "file_outline" | "read_range";
147
+
148
+ export interface ResolvedWorkspace {
149
+ readonly repoId: string;
150
+ readonly worktreeId: string;
151
+ readonly worktreeRoot: string;
152
+ readonly gitCommonDir: string;
153
+ }
154
+
155
+ export interface WorkspaceAuthorizationPolicy {
156
+ getCapabilityProfile(resolved: ResolvedWorkspace): Promise<WorkspaceCapabilityProfile | null>;
157
+ noteBound(resolved: ResolvedWorkspace): Promise<void>;
158
+ }
159
+
160
+ export interface WorkspaceSharedAttachPolicy {
161
+ resolveSharedAttachSource(input: {
162
+ readonly sessionId: string;
163
+ readonly repoId: string;
164
+ readonly worktreeId: string;
165
+ }): PersistedLocalHistorySharedAttachSource | null;
166
+ }
167
+
168
+ interface WorkspaceRouterOptions {
169
+ readonly mode: WorkspaceSessionMode;
170
+ readonly fs: FileSystem;
171
+ readonly git: GitClient;
172
+ readonly graftDir: string;
173
+ readonly projectRoot?: string | undefined;
174
+ readonly warpPool: WarpPool;
175
+ readonly transportSessionId: string;
176
+ readonly warpWriterId?: string | undefined;
177
+ readonly authorizationPolicy?: WorkspaceAuthorizationPolicy | undefined;
178
+ readonly sharedAttachPolicy?: WorkspaceSharedAttachPolicy | undefined;
179
+ readonly persistedLocalHistory: PersistedLocalHistoryStore;
180
+ }
181
+
182
+ export const DEFAULT_DAEMON_CAPABILITY_PROFILE: WorkspaceCapabilityProfile = Object.freeze({
183
+ boundedReads: true,
184
+ structuralTools: true,
185
+ precisionTools: true,
186
+ stateBookmarks: true,
187
+ runtimeLogs: "session_local_only",
188
+ runCapture: false,
189
+ });
190
+
191
+ export const DEFAULT_REPO_LOCAL_CAPABILITY_PROFILE: WorkspaceCapabilityProfile = Object.freeze({
192
+ boundedReads: true,
193
+ structuralTools: true,
194
+ precisionTools: true,
195
+ stateBookmarks: true,
196
+ runtimeLogs: "session_local_only",
197
+ runCapture: true,
198
+ });
199
+
200
+ function stableId(prefix: string, input: string): string {
201
+ return `${prefix}:${crypto.createHash("sha256").update(input).digest("hex").slice(0, 16)}`;
202
+ }
203
+
204
+ async function readGitValue(git: GitClient, cwd: string, args: readonly string[]): Promise<string | null> {
205
+ const result = await git.run({ args, cwd });
206
+ if (result.error !== undefined || result.status !== 0) {
207
+ return null;
208
+ }
209
+ const trimmed = result.stdout.trim();
210
+ return trimmed.length > 0 ? trimmed : null;
211
+ }
212
+
213
+ function toAbsolutePath(base: string, value: string): string {
214
+ return path.isAbsolute(value) ? value : path.resolve(base, value);
215
+ }
216
+
217
+ export async function resolveWorkspaceRequest(
218
+ git: GitClient,
219
+ request: WorkspaceBindRequest,
220
+ ): Promise<ResolvedWorkspace | WorkspaceBindError> {
221
+ const cwd = path.resolve(request.cwd);
222
+ const worktreeRoot = await readGitValue(git, cwd, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
223
+ if (worktreeRoot === null) {
224
+ return {
225
+ code: "NOT_A_GIT_REPO",
226
+ message: `cwd is not inside a git worktree: ${cwd}`,
227
+ };
228
+ }
229
+
230
+ const rawGitCommonDir = await readGitValue(git, cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
231
+ if (rawGitCommonDir === null) {
232
+ return {
233
+ code: "WORKSPACE_RESOLUTION_FAILED",
234
+ message: `Unable to resolve git common dir from ${cwd}`,
235
+ };
236
+ }
237
+
238
+ const gitCommonDir = toAbsolutePath(worktreeRoot, rawGitCommonDir);
239
+ return {
240
+ repoId: stableId("repo", gitCommonDir),
241
+ worktreeId: stableId("worktree", worktreeRoot),
242
+ worktreeRoot,
243
+ gitCommonDir,
244
+ };
245
+ }
246
+
247
+ export class WorkspaceRouter {
248
+ private bindingCounter = 0;
249
+ private sliceIdCounter = 0;
250
+ private currentSlice: WorkspaceSlice;
251
+ private currentBinding: BoundWorkspace | null = null;
252
+ private initialization: Promise<void> | null = null;
253
+
254
+ constructor(private readonly options: WorkspaceRouterOptions) {
255
+ const initialProjectRoot = options.mode === "repo_local" ? options.projectRoot : undefined;
256
+ this.currentSlice = this.createSlice(
257
+ options.mode === "repo_local" ? options.graftDir : path.join(options.graftDir, "unbound"),
258
+ initialProjectRoot,
259
+ );
260
+ }
261
+
262
+ async initialize(): Promise<void> {
263
+ if (this.options.mode !== "repo_local") {
264
+ return;
265
+ }
266
+ if (this.currentBinding !== null) {
267
+ return;
268
+ }
269
+ if (this.initialization !== null) {
270
+ await this.initialization;
271
+ return;
272
+ }
273
+
274
+ const projectRoot = this.options.projectRoot;
275
+ if (projectRoot === undefined) {
276
+ throw new Error("repo_local workspace router requires projectRoot");
277
+ }
278
+
279
+ this.initialization = (async () => {
280
+ const resolved = await resolveWorkspaceRequest(this.options.git, { cwd: projectRoot });
281
+ const initialWorkspace = "code" in resolved
282
+ ? {
283
+ repoId: stableId("repo", projectRoot),
284
+ worktreeId: stableId("worktree", projectRoot),
285
+ worktreeRoot: projectRoot,
286
+ gitCommonDir: projectRoot,
287
+ }
288
+ : {
289
+ repoId: resolved.repoId,
290
+ worktreeId: stableId("worktree", projectRoot),
291
+ worktreeRoot: projectRoot,
292
+ gitCommonDir: resolved.gitCommonDir,
293
+ };
294
+ const currentBinding = this.createBoundWorkspace(
295
+ initialWorkspace,
296
+ this.options.graftDir,
297
+ DEFAULT_REPO_LOCAL_CAPABILITY_PROFILE,
298
+ undefined,
299
+ this.currentSlice,
300
+ );
301
+ const currentRepoState = currentBinding.slice.repoState;
302
+ if (currentRepoState === null) {
303
+ throw new WorkspaceBindingRequiredError("workspace");
304
+ }
305
+ await currentRepoState.initialize();
306
+ await this.options.persistedLocalHistory.noteBinding({
307
+ current: this.buildPersistedLocalHistoryContext(currentBinding, currentRepoState.getState()),
308
+ });
309
+ this.currentBinding = currentBinding;
310
+ })();
311
+
312
+ await this.initialization;
313
+ }
314
+
315
+ get mode(): WorkspaceSessionMode {
316
+ return this.options.mode;
317
+ }
318
+
319
+ get session(): SessionTracker {
320
+ return this.currentSlice.session;
321
+ }
322
+
323
+ get cache(): ObservationCache {
324
+ return this.currentSlice.cache;
325
+ }
326
+
327
+ get metrics(): Metrics {
328
+ return this.currentSlice.metrics;
329
+ }
330
+
331
+ get graftDir(): string {
332
+ return this.currentSlice.graftDir;
333
+ }
334
+
335
+ isBound(): boolean {
336
+ return this.currentBinding !== null;
337
+ }
338
+
339
+ getProjectRoot(): string {
340
+ return this.requireBinding().worktreeRoot;
341
+ }
342
+
343
+ getGraftignorePatterns(): readonly string[] {
344
+ return this.requireBinding().graftignorePatterns;
345
+ }
346
+
347
+ getPathResolver(): (input: string) => string {
348
+ return this.requireBinding().resolvePath;
349
+ }
350
+
351
+ getWarp(): Promise<WarpApp> {
352
+ return this.requireBinding().getWarp();
353
+ }
354
+
355
+ async observeRepoState(): Promise<void> {
356
+ const binding = this.requireBinding();
357
+ const repoState = this.requireRepoState();
358
+ const previousObservation = repoState.getState();
359
+ const nextObservation = await repoState.observe();
360
+ const checkoutBoundaryHookEvent = previousObservation.checkoutEpoch !== nextObservation.checkoutEpoch
361
+ ? await this.resolveCheckoutBoundaryHookEvent(binding, previousObservation.observedAt, nextObservation)
362
+ : null;
363
+ const previousContext = this.buildPersistedLocalHistoryContext(binding, previousObservation);
364
+ const nextContext = this.buildPersistedLocalHistoryContext(
365
+ binding,
366
+ nextObservation,
367
+ checkoutBoundaryHookEvent,
368
+ );
369
+ if (previousContext.checkoutEpochId !== nextContext.checkoutEpochId) {
370
+ await this.options.persistedLocalHistory.noteCheckoutBoundary({
371
+ previous: previousContext,
372
+ current: nextContext,
373
+ });
374
+ }
375
+ }
376
+
377
+ getRepoState() {
378
+ return this.requireRepoState().getState();
379
+ }
380
+
381
+ getStatus(): WorkspaceStatus {
382
+ if (this.currentBinding === null) {
383
+ return {
384
+ sessionMode: this.options.mode,
385
+ bindState: "unbound",
386
+ repoId: null,
387
+ worktreeId: null,
388
+ worktreeRoot: null,
389
+ gitCommonDir: null,
390
+ graftDir: null,
391
+ capabilityProfile: null,
392
+ };
393
+ }
394
+
395
+ return {
396
+ sessionMode: this.options.mode,
397
+ bindState: "bound",
398
+ repoId: this.currentBinding.repoId,
399
+ worktreeId: this.currentBinding.worktreeId,
400
+ worktreeRoot: this.currentBinding.worktreeRoot,
401
+ gitCommonDir: this.currentBinding.gitCommonDir,
402
+ graftDir: this.currentBinding.slice.graftDir,
403
+ capabilityProfile: this.currentBinding.capabilityProfile,
404
+ };
405
+ }
406
+
407
+ async getPersistedLocalHistorySummary(): Promise<PersistedLocalHistorySummary> {
408
+ const binding = this.currentBinding;
409
+ if (binding?.slice.repoState === null || binding === null) {
410
+ return {
411
+ availability: "none",
412
+ persistence: "persisted_local_history",
413
+ historyPath: null,
414
+ totalContinuityRecords: 0,
415
+ active: false,
416
+ lastOperation: null,
417
+ lastObservedAt: null,
418
+ continuityKey: null,
419
+ causalSessionId: null,
420
+ strandId: null,
421
+ checkoutEpochId: null,
422
+ continuedFromCausalSessionId: null,
423
+ continuityConfidence: "unknown",
424
+ continuityEvidence: [],
425
+ attribution: {
426
+ actor: {
427
+ actorId: "unknown",
428
+ actorKind: "unknown",
429
+ displayName: "Unknown",
430
+ source: "persisted_local_history.fallback",
431
+ authorityScope: "inferred",
432
+ },
433
+ confidence: "unknown",
434
+ basis: "unknown_fallback",
435
+ evidence: [],
436
+ },
437
+ latestReadEvent: null,
438
+ latestStageEvent: null,
439
+ latestTransitionEvent: null,
440
+ preserves: [
441
+ "continuity_operations",
442
+ "read_events",
443
+ "stage_events",
444
+ "transition_events",
445
+ "runtime_context_ids",
446
+ "workspace_overlay_snapshots",
447
+ ],
448
+ excludes: [
449
+ "raw_chat_transcripts",
450
+ "queue_bookkeeping",
451
+ "canonical_provenance",
452
+ "canonical_structural_truth",
453
+ ],
454
+ nextAction: "bind_workspace_to_begin_local_history",
455
+ };
456
+ }
457
+ const status = this.getStatus();
458
+ const repoState = binding.slice.repoState.getState();
459
+ const causalContext = this.buildCausalContext(binding, repoState);
460
+ let summary = await this.options.persistedLocalHistory.summarize(status, causalContext);
461
+
462
+ if (repoState.semanticTransition !== null) {
463
+ await this.options.persistedLocalHistory.noteSemanticTransitionObservation({
464
+ current: this.buildPersistedLocalHistoryContext(binding, repoState),
465
+ semanticTransition: repoState.semanticTransition,
466
+ transition: repoState.lastTransition,
467
+ attribution: summary.attribution,
468
+ });
469
+ summary = await this.options.persistedLocalHistory.summarize(status, causalContext);
470
+ }
471
+
472
+ const stagedTarget = buildRuntimeStagedTarget(status, causalContext, repoState, summary.attribution);
473
+
474
+ if (stagedTarget.availability === "full_file") {
475
+ await this.options.persistedLocalHistory.noteStageObservation({
476
+ current: this.buildPersistedLocalHistoryContext(binding, repoState),
477
+ stagedTarget,
478
+ attribution: summary.attribution,
479
+ });
480
+ return this.options.persistedLocalHistory.summarize(status, causalContext);
481
+ }
482
+
483
+ return summary;
484
+ }
485
+
486
+ async getRepoConcurrencySummary(): Promise<RepoConcurrencySummary | null> {
487
+ const binding = this.currentBinding;
488
+ if (binding?.slice.repoState === null || binding === null) {
489
+ return null;
490
+ }
491
+ return this.options.persistedLocalHistory.summarizeRepoConcurrency(this.getStatus());
492
+ }
493
+
494
+ async getPersistedLocalActivityWindow(limit: number): Promise<PersistedLocalActivityWindow> {
495
+ const binding = this.currentBinding;
496
+ if (binding?.slice.repoState === null || binding === null) {
497
+ return {
498
+ historyPath: null,
499
+ limit,
500
+ totalMatchingItems: 0,
501
+ truncated: false,
502
+ items: [],
503
+ };
504
+ }
505
+
506
+ await this.getPersistedLocalHistorySummary();
507
+
508
+ const status = this.getStatus();
509
+ const repoState = binding.slice.repoState.getState();
510
+ const causalContext = this.buildCausalContext(binding, repoState);
511
+ return this.options.persistedLocalHistory.listRecentActivity(
512
+ status,
513
+ causalContext,
514
+ limit,
515
+ );
516
+ }
517
+
518
+ async getWorkspaceOverlayFooting(): Promise<RuntimeWorkspaceOverlayFooting | null> {
519
+ const binding = this.currentBinding;
520
+ if (binding?.slice.repoState === null || binding === null) {
521
+ return null;
522
+ }
523
+ return buildRuntimeWorkspaceOverlayFooting(
524
+ this.options.fs,
525
+ this.options.git,
526
+ binding.worktreeRoot,
527
+ binding.gitCommonDir,
528
+ binding.slice.repoState.getState(),
529
+ );
530
+ }
531
+
532
+ async noteReadObservation(
533
+ toolName: AttributedReadToolName,
534
+ args: Record<string, unknown>,
535
+ result: Record<string, unknown>,
536
+ execution?: WorkspaceExecutionContext | null,
537
+ ): Promise<void> {
538
+ const active = execution ?? this.captureCurrentExecutionContext();
539
+ if (active === null) {
540
+ return;
541
+ }
542
+
543
+ const readObservation = this.buildReadObservation(active, toolName, args, result);
544
+ if (readObservation === null) {
545
+ return;
546
+ }
547
+
548
+ const summary = await this.options.persistedLocalHistory.summarize(
549
+ active.status,
550
+ active.getCausalContext(),
551
+ );
552
+
553
+ await this.options.persistedLocalHistory.noteReadObservation({
554
+ current: this.buildPersistedLocalHistoryContextFromExecution(active, active.repoState.getState()),
555
+ attribution: summary.attribution,
556
+ ...readObservation,
557
+ });
558
+ }
559
+
560
+ captureExecutionContext(): WorkspaceExecutionContext {
561
+ const binding = this.requireBinding();
562
+ const repoState = binding.slice.repoState;
563
+ if (repoState === null) {
564
+ throw new WorkspaceBindingRequiredError("workspace");
565
+ }
566
+ return {
567
+ sliceId: binding.slice.sliceId,
568
+ repoId: binding.repoId,
569
+ worktreeId: binding.worktreeId,
570
+ projectRoot: binding.worktreeRoot,
571
+ worktreeRoot: binding.worktreeRoot,
572
+ gitCommonDir: binding.gitCommonDir,
573
+ graftignorePatterns: binding.graftignorePatterns,
574
+ resolvePath: binding.resolvePath,
575
+ capabilityProfile: binding.capabilityProfile,
576
+ warpWriterId: binding.warpWriterId,
577
+ getCausalContext: () => this.buildCausalContext(binding, repoState.getState()),
578
+ status: {
579
+ sessionMode: this.options.mode,
580
+ bindState: "bound",
581
+ repoId: binding.repoId,
582
+ worktreeId: binding.worktreeId,
583
+ worktreeRoot: binding.worktreeRoot,
584
+ gitCommonDir: binding.gitCommonDir,
585
+ graftDir: binding.slice.graftDir,
586
+ capabilityProfile: binding.capabilityProfile,
587
+ },
588
+ session: binding.slice.session,
589
+ cache: binding.slice.cache,
590
+ metrics: binding.slice.metrics,
591
+ graftDir: binding.slice.graftDir,
592
+ repoState,
593
+ getWarp: binding.getWarp,
594
+ };
595
+ }
596
+
597
+ async bind(request: WorkspaceBindRequest, actionName: string): Promise<WorkspaceActionResult> {
598
+ return this.bindInternal("bind", request, actionName);
599
+ }
600
+
601
+ async rebind(request: WorkspaceBindRequest, actionName: string): Promise<WorkspaceActionResult> {
602
+ if (this.currentBinding === null) {
603
+ return {
604
+ ok: false,
605
+ action: "rebind",
606
+ freshSessionSlice: false,
607
+ ...this.getStatus(),
608
+ errorCode: "UNBOUND_SESSION",
609
+ error: "workspace_rebind requires an active workspace binding.",
610
+ };
611
+ }
612
+ return this.bindInternal("rebind", request, actionName);
613
+ }
614
+
615
+ async declareAttach(
616
+ declaration: PersistedLocalHistoryAttachDeclaration,
617
+ ): Promise<CausalAttachResult> {
618
+ const binding = this.currentBinding;
619
+ if (binding?.slice.repoState === null || binding === null) {
620
+ return {
621
+ ok: false,
622
+ action: "attach",
623
+ ...this.getStatus(),
624
+ persistedLocalHistory: await this.getPersistedLocalHistorySummary(),
625
+ errorCode: "UNBOUND_SESSION",
626
+ error: "causal_attach requires an active workspace binding.",
627
+ };
628
+ }
629
+
630
+ try {
631
+ await this.options.persistedLocalHistory.declareAttach({
632
+ current: this.buildPersistedLocalHistoryContext(binding, binding.slice.repoState.getState()),
633
+ declaration,
634
+ });
635
+ } catch (error) {
636
+ if (error instanceof PersistedLocalHistoryAttachUnavailableError) {
637
+ const sharedAttachSource = this.options.sharedAttachPolicy?.resolveSharedAttachSource({
638
+ sessionId: this.options.transportSessionId,
639
+ repoId: binding.repoId,
640
+ worktreeId: binding.worktreeId,
641
+ }) ?? null;
642
+ if (sharedAttachSource !== null) {
643
+ await this.options.persistedLocalHistory.declareSharedAttach({
644
+ current: this.buildPersistedLocalHistoryContext(binding, binding.slice.repoState.getState()),
645
+ declaration,
646
+ source: sharedAttachSource,
647
+ });
648
+ return {
649
+ ok: true,
650
+ action: "attach",
651
+ ...this.getStatus(),
652
+ persistedLocalHistory: await this.getPersistedLocalHistorySummary(),
653
+ };
654
+ }
655
+ return {
656
+ ok: false,
657
+ action: "attach",
658
+ ...this.getStatus(),
659
+ persistedLocalHistory: await this.getPersistedLocalHistorySummary(),
660
+ errorCode: error.code,
661
+ error: error.message,
662
+ };
663
+ }
664
+ throw error;
665
+ }
666
+
667
+ return {
668
+ ok: true,
669
+ action: "attach",
670
+ ...this.getStatus(),
671
+ persistedLocalHistory: await this.getPersistedLocalHistorySummary(),
672
+ };
673
+ }
674
+
675
+ private async bindInternal(
676
+ action: WorkspaceBindAction,
677
+ request: WorkspaceBindRequest,
678
+ actionName: string,
679
+ ): Promise<WorkspaceActionResult> {
680
+ const resolved = await resolveWorkspaceRequest(this.options.git, request);
681
+ if ("code" in resolved) {
682
+ return {
683
+ ok: false,
684
+ action,
685
+ freshSessionSlice: false,
686
+ ...this.getStatus(),
687
+ errorCode: resolved.code,
688
+ error: resolved.message,
689
+ };
690
+ }
691
+
692
+ const sliceDir = path.join(
693
+ this.options.graftDir,
694
+ "bindings",
695
+ `slice-${String(++this.bindingCounter).padStart(4, "0")}`,
696
+ );
697
+ await this.options.fs.mkdir(sliceDir, { recursive: true });
698
+
699
+ const capabilityProfile = this.options.mode === "repo_local"
700
+ ? DEFAULT_REPO_LOCAL_CAPABILITY_PROFILE
701
+ : (await this.options.authorizationPolicy?.getCapabilityProfile(resolved)) ?? null;
702
+ if (capabilityProfile === null) {
703
+ return {
704
+ ok: false,
705
+ action,
706
+ freshSessionSlice: false,
707
+ ...this.getStatus(),
708
+ errorCode: "WORKSPACE_NOT_AUTHORIZED",
709
+ error: `Workspace ${resolved.worktreeRoot} is not authorized for daemon binding. Call workspace_authorize first.`,
710
+ };
711
+ }
712
+
713
+ const nextBinding = this.createBoundWorkspace(resolved, sliceDir, capabilityProfile, actionName);
714
+ const nextRepoState = nextBinding.slice.repoState;
715
+ if (nextRepoState === null) {
716
+ throw new WorkspaceBindingRequiredError("workspace");
717
+ }
718
+ await nextRepoState.initialize();
719
+ const previousBinding = this.currentBinding;
720
+ const previousRepoState = previousBinding?.slice.repoState;
721
+ await this.options.persistedLocalHistory.noteBinding({
722
+ current: this.buildPersistedLocalHistoryContext(nextBinding, nextRepoState.getState()),
723
+ previous: previousBinding === null || previousRepoState == null
724
+ ? null
725
+ : this.buildPersistedLocalHistoryContext(previousBinding, previousRepoState.getState()),
726
+ });
727
+ if (this.options.mode === "daemon") {
728
+ await this.options.authorizationPolicy?.noteBound(resolved);
729
+ }
730
+ this.currentBinding = nextBinding;
731
+ this.currentSlice = nextBinding.slice;
732
+
733
+ return {
734
+ ok: true,
735
+ action,
736
+ freshSessionSlice: true,
737
+ ...this.getStatus(),
738
+ };
739
+ }
740
+
741
+ private createBoundWorkspace(
742
+ resolved: ResolvedWorkspace,
743
+ graftDir: string,
744
+ capabilityProfile: WorkspaceCapabilityProfile,
745
+ actionName: string | undefined,
746
+ sliceOverride?: WorkspaceSlice,
747
+ ): BoundWorkspace {
748
+ const slice = sliceOverride ?? this.createSlice(graftDir, resolved.worktreeRoot);
749
+ if (actionName !== undefined) {
750
+ slice.session.recordMessage();
751
+ slice.session.recordToolCall(actionName);
752
+ }
753
+
754
+ return {
755
+ ...resolved,
756
+ graftignorePatterns: loadProjectGraftignore(this.options.fs, resolved.worktreeRoot),
757
+ resolvePath: createPathResolver(resolved.worktreeRoot),
758
+ capabilityProfile,
759
+ transportSessionId: this.options.transportSessionId,
760
+ warpWriterId: this.options.warpWriterId ?? DEFAULT_WARP_WRITER_ID,
761
+ slice,
762
+ getWarp: () => this.options.warpPool.getOrOpen(
763
+ resolved.repoId,
764
+ resolved.worktreeRoot,
765
+ this.options.warpWriterId ?? DEFAULT_WARP_WRITER_ID,
766
+ ),
767
+ };
768
+ }
769
+
770
+ private createSlice(graftDir: string, projectRoot?: string): WorkspaceSlice {
771
+ return {
772
+ sliceId: `slice-${String(++this.sliceIdCounter).padStart(4, "0")}`,
773
+ session: new SessionTracker(),
774
+ cache: new ObservationCache(),
775
+ metrics: new Metrics(),
776
+ graftDir,
777
+ repoState: projectRoot !== undefined ? new RepoStateTracker(projectRoot, this.options.fs, this.options.git) : null,
778
+ };
779
+ }
780
+
781
+ private requireBinding(): BoundWorkspace {
782
+ if (this.currentBinding === null) {
783
+ throw new WorkspaceBindingRequiredError("workspace");
784
+ }
785
+ return this.currentBinding;
786
+ }
787
+
788
+ private requireRepoState(): RepoStateTracker {
789
+ const repoState = this.currentBinding?.slice.repoState;
790
+ if (repoState === null || repoState === undefined) {
791
+ throw new WorkspaceBindingRequiredError("workspace");
792
+ }
793
+ return repoState;
794
+ }
795
+
796
+ private buildCausalContext(
797
+ binding: BoundWorkspace,
798
+ observation: { readonly checkoutEpoch: number },
799
+ ): RuntimeCausalContext {
800
+ return buildRuntimeCausalContext({
801
+ transportSessionId: binding.transportSessionId,
802
+ workspaceSliceId: binding.slice.sliceId,
803
+ repoId: binding.repoId,
804
+ worktreeId: binding.worktreeId,
805
+ checkoutEpoch: observation.checkoutEpoch,
806
+ warpWriterId: binding.warpWriterId,
807
+ });
808
+ }
809
+
810
+ private buildPersistedLocalHistoryContext(
811
+ binding: BoundWorkspace,
812
+ observation: import("./repo-state.js").RepoObservation,
813
+ hookEvent: GitTransitionHookEvent | null = null,
814
+ ): PersistedLocalHistoryContext {
815
+ const context = this.options.persistedLocalHistory.buildContext(
816
+ {
817
+ sessionMode: this.options.mode,
818
+ bindState: "bound",
819
+ repoId: binding.repoId,
820
+ worktreeId: binding.worktreeId,
821
+ worktreeRoot: binding.worktreeRoot,
822
+ gitCommonDir: binding.gitCommonDir,
823
+ graftDir: binding.slice.graftDir,
824
+ capabilityProfile: binding.capabilityProfile,
825
+ },
826
+ this.buildCausalContext(binding, observation),
827
+ observation,
828
+ hookEvent,
829
+ );
830
+ if (context === null) {
831
+ throw new WorkspaceBindingRequiredError("workspace");
832
+ }
833
+ return context;
834
+ }
835
+
836
+ private buildPersistedLocalHistoryContextFromExecution(
837
+ execution: WorkspaceExecutionContext,
838
+ observation: import("./repo-state.js").RepoObservation,
839
+ ): PersistedLocalHistoryContext {
840
+ const context = this.options.persistedLocalHistory.buildContext(
841
+ execution.status,
842
+ execution.getCausalContext(),
843
+ observation,
844
+ );
845
+ if (context === null) {
846
+ throw new WorkspaceBindingRequiredError("workspace");
847
+ }
848
+ return context;
849
+ }
850
+
851
+ private async resolveCheckoutBoundaryHookEvent(
852
+ binding: BoundWorkspace,
853
+ previousObservedAt: string,
854
+ observation: import("./repo-state.js").RepoObservation,
855
+ ): Promise<GitTransitionHookEvent | null> {
856
+ const footing = await buildRuntimeWorkspaceOverlayFooting(
857
+ this.options.fs,
858
+ this.options.git,
859
+ binding.worktreeRoot,
860
+ binding.gitCommonDir,
861
+ observation,
862
+ );
863
+ const latestHookEvent = footing.latestHookEvent;
864
+ if (latestHookEvent === null) {
865
+ return null;
866
+ }
867
+
868
+ const previousObservedAtMs = Date.parse(previousObservedAt);
869
+ const hookObservedAtMs = Date.parse(latestHookEvent.observedAt);
870
+ if (
871
+ Number.isFinite(previousObservedAtMs) &&
872
+ Number.isFinite(hookObservedAtMs) &&
873
+ hookObservedAtMs < previousObservedAtMs
874
+ ) {
875
+ return null;
876
+ }
877
+ return latestHookEvent;
878
+ }
879
+
880
+ private captureCurrentExecutionContext(): WorkspaceExecutionContext | null {
881
+ if (this.currentBinding === null) {
882
+ return null;
883
+ }
884
+ return this.captureExecutionContext();
885
+ }
886
+
887
+ private buildReadObservation(
888
+ execution: WorkspaceExecutionContext,
889
+ toolName: AttributedReadToolName,
890
+ args: Record<string, unknown>,
891
+ result: Record<string, unknown>,
892
+ ): {
893
+ readonly surface: string;
894
+ readonly projection: string;
895
+ readonly sourceLayer: "canonical_structural_truth" | "workspace_overlay";
896
+ readonly reason: string;
897
+ readonly footprint: {
898
+ readonly paths: string[];
899
+ readonly symbols: string[];
900
+ readonly regions: {
901
+ readonly path: string;
902
+ readonly startLine: number;
903
+ readonly endLine: number;
904
+ }[];
905
+ };
906
+ } | null {
907
+ const rawPath = args["path"];
908
+ if (typeof rawPath !== "string") {
909
+ return null;
910
+ }
911
+
912
+ const absolutePath = execution.resolvePath(rawPath);
913
+ const relativePath = path.relative(execution.worktreeRoot, absolutePath);
914
+ const footprintPath = relativePath.startsWith("..") ? absolutePath : relativePath;
915
+ const sourceLayer = execution.repoState.getState().workspaceOverlayId === null
916
+ ? "canonical_structural_truth"
917
+ : "workspace_overlay";
918
+
919
+ if (toolName === "safe_read") {
920
+ const projection = result["projection"];
921
+ if (
922
+ projection !== "content" &&
923
+ projection !== "outline" &&
924
+ projection !== "cache_hit" &&
925
+ projection !== "diff"
926
+ ) {
927
+ return null;
928
+ }
929
+ return {
930
+ surface: "safe_read",
931
+ projection,
932
+ sourceLayer,
933
+ reason: typeof result["reason"] === "string" ? result["reason"] : "SAFE_READ",
934
+ footprint: {
935
+ paths: [footprintPath],
936
+ symbols: [],
937
+ regions: [],
938
+ },
939
+ };
940
+ }
941
+
942
+ if (toolName === "file_outline") {
943
+ if (typeof result["error"] === "string" || result["reason"] === "UNSUPPORTED_LANGUAGE") {
944
+ return null;
945
+ }
946
+ return {
947
+ surface: "file_outline",
948
+ projection: "outline",
949
+ sourceLayer,
950
+ reason: typeof result["reason"] === "string" ? result["reason"] : "FILE_OUTLINE",
951
+ footprint: {
952
+ paths: [footprintPath],
953
+ symbols: [],
954
+ regions: [],
955
+ },
956
+ };
957
+ }
958
+
959
+ const startLine = result["startLine"];
960
+ const endLine = result["endLine"];
961
+ if (
962
+ typeof result["content"] !== "string" ||
963
+ typeof startLine !== "number" ||
964
+ typeof endLine !== "number"
965
+ ) {
966
+ return null;
967
+ }
968
+ return {
969
+ surface: "read_range",
970
+ projection: "content",
971
+ sourceLayer,
972
+ reason: typeof result["reason"] === "string" ? result["reason"] : "READ_RANGE",
973
+ footprint: {
974
+ paths: [footprintPath],
975
+ symbols: [],
976
+ regions: [{
977
+ path: footprintPath,
978
+ startLine,
979
+ endLine,
980
+ }],
981
+ },
982
+ };
983
+ }
984
+ }