@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,161 @@
1
+ import * as crypto from "node:crypto";
2
+ import type { AttributionSummary, StagedTarget } from "../contracts/causal-ontology.js";
3
+ import type { RepoObservation } from "./repo-state.js";
4
+ import type { RuntimeCausalContext } from "./runtime-causal-context.js";
5
+ import type { WorkspaceStatus } from "./workspace-router.js";
6
+
7
+ const FULL_FILE_SAFE_STATUSES = new Set(["A", "D", "R", "C", "T"]);
8
+
9
+ export interface RuntimeStagedTargetNone {
10
+ readonly availability: "none";
11
+ readonly stability: "runtime_local";
12
+ readonly provenanceLevel: "artifact_history";
13
+ }
14
+
15
+ export interface RuntimeStagedTargetFullFile {
16
+ readonly availability: "full_file";
17
+ readonly stability: "runtime_local";
18
+ readonly provenanceLevel: "artifact_history";
19
+ readonly attribution: AttributionSummary;
20
+ readonly target: StagedTarget;
21
+ }
22
+
23
+ export interface RuntimeStagedTargetAmbiguous {
24
+ readonly availability: "ambiguous";
25
+ readonly stability: "runtime_local";
26
+ readonly provenanceLevel: "artifact_history";
27
+ readonly attribution: AttributionSummary;
28
+ readonly reason:
29
+ | "missing_head_commit"
30
+ | "missing_workspace_overlay"
31
+ | "modified_path_selection_requires_deeper_evidence";
32
+ readonly observedStagedPaths: number;
33
+ readonly ambiguousPaths: readonly string[];
34
+ }
35
+
36
+ export type RuntimeStagedTarget =
37
+ | RuntimeStagedTargetNone
38
+ | RuntimeStagedTargetFullFile
39
+ | RuntimeStagedTargetAmbiguous;
40
+
41
+ interface MergedStatusEntry {
42
+ readonly path: string;
43
+ readonly stagedStatus: string;
44
+ readonly worktreeStatus: string;
45
+ }
46
+
47
+ function stableId(prefix: string, input: string): string {
48
+ return `${prefix}:${crypto.createHash("sha256").update(input).digest("hex").slice(0, 16)}`;
49
+ }
50
+
51
+ function parseMergedStatusLine(line: string): MergedStatusEntry | null {
52
+ if (line.length < 4) return null;
53
+ return {
54
+ stagedStatus: line[0] ?? " ",
55
+ worktreeStatus: line[1] ?? " ",
56
+ path: line.slice(3),
57
+ };
58
+ }
59
+
60
+ function stagedEntries(statusLines: readonly string[]): readonly MergedStatusEntry[] {
61
+ return statusLines
62
+ .map(parseMergedStatusLine)
63
+ .filter((entry): entry is MergedStatusEntry => entry !== null && entry.stagedStatus !== " " && entry.stagedStatus !== "?");
64
+ }
65
+
66
+ function isFullFileSafe(entry: MergedStatusEntry): boolean {
67
+ return entry.worktreeStatus === " " && FULL_FILE_SAFE_STATUSES.has(entry.stagedStatus);
68
+ }
69
+
70
+ export function buildRuntimeStagedTarget(
71
+ status: WorkspaceStatus,
72
+ causalContext: RuntimeCausalContext,
73
+ repoState: RepoObservation,
74
+ attribution: AttributionSummary,
75
+ ): RuntimeStagedTarget {
76
+ const observedEntries = stagedEntries(repoState.statusLines);
77
+ if (observedEntries.length === 0) {
78
+ return {
79
+ availability: "none",
80
+ stability: "runtime_local",
81
+ provenanceLevel: "artifact_history",
82
+ };
83
+ }
84
+
85
+ if (repoState.headSha === null) {
86
+ return {
87
+ availability: "ambiguous",
88
+ stability: "runtime_local",
89
+ provenanceLevel: "artifact_history",
90
+ attribution,
91
+ reason: "missing_head_commit",
92
+ observedStagedPaths: observedEntries.length,
93
+ ambiguousPaths: observedEntries.map((entry) => entry.path),
94
+ };
95
+ }
96
+
97
+ if (repoState.workspaceOverlayId === null || status.repoId === null || status.worktreeId === null) {
98
+ return {
99
+ availability: "ambiguous",
100
+ stability: "runtime_local",
101
+ provenanceLevel: "artifact_history",
102
+ attribution,
103
+ reason: "missing_workspace_overlay",
104
+ observedStagedPaths: observedEntries.length,
105
+ ambiguousPaths: observedEntries.map((entry) => entry.path),
106
+ };
107
+ }
108
+
109
+ const ambiguousEntries = observedEntries.filter((entry) => !isFullFileSafe(entry));
110
+ if (ambiguousEntries.length > 0) {
111
+ return {
112
+ availability: "ambiguous",
113
+ stability: "runtime_local",
114
+ provenanceLevel: "artifact_history",
115
+ attribution,
116
+ reason: "modified_path_selection_requires_deeper_evidence",
117
+ observedStagedPaths: observedEntries.length,
118
+ ambiguousPaths: ambiguousEntries.map((entry) => entry.path),
119
+ };
120
+ }
121
+
122
+ const selectionEntries = observedEntries.map((entry) => ({
123
+ path: entry.path,
124
+ symbols: [],
125
+ regions: [],
126
+ }));
127
+
128
+ const target: StagedTarget = {
129
+ targetId: stableId(
130
+ "target",
131
+ JSON.stringify({
132
+ repoId: status.repoId,
133
+ worktreeId: status.worktreeId,
134
+ checkoutEpochId: causalContext.checkoutEpochId,
135
+ workspaceOverlayId: repoState.workspaceOverlayId,
136
+ headSha: repoState.headSha,
137
+ paths: selectionEntries.map((entry) => entry.path),
138
+ }),
139
+ ),
140
+ targetKind: "staged_target",
141
+ repoId: status.repoId,
142
+ worktreeId: status.worktreeId,
143
+ checkoutEpochId: causalContext.checkoutEpochId,
144
+ workspaceOverlayId: repoState.workspaceOverlayId,
145
+ selectedAt: repoState.observedAt,
146
+ selectionKind: "full_file",
147
+ selectionEntries,
148
+ base: {
149
+ headCommitSha: repoState.headSha,
150
+ indexTreeSha: null,
151
+ },
152
+ };
153
+
154
+ return {
155
+ availability: "full_file",
156
+ stability: "runtime_local",
157
+ provenanceLevel: "artifact_history",
158
+ attribution,
159
+ target,
160
+ };
161
+ }
@@ -0,0 +1,255 @@
1
+ import * as path from "node:path";
2
+ import type { FileSystem } from "../ports/filesystem.js";
3
+ import type { GitClient } from "../ports/git.js";
4
+ import {
5
+ isRecognizedTargetGitHook,
6
+ isTargetGitTransitionHookName,
7
+ resolveGitHooksPath,
8
+ TARGET_GIT_TRANSITION_HOOKS,
9
+ } from "../git/target-git-hook-bootstrap.js";
10
+ import type {
11
+ RepoTransition,
12
+ WorkspaceOverlaySummary,
13
+ } from "./repo-state.js";
14
+
15
+ export type GitHookBootstrapPosture = "absent" | "external_unknown" | "installed";
16
+ export type WorkspaceOverlayObservationMode =
17
+ | "inferred_between_tool_calls"
18
+ | "hook_observed_checkout_boundaries";
19
+ export type WorkspaceOverlayLineagePosture =
20
+ | "stable"
21
+ | "forked_after_transition";
22
+ export type WorkspaceOverlayBoundaryAuthority =
23
+ | "none"
24
+ | "repo_snapshot"
25
+ | "hook_observed";
26
+ export type WorkspaceOverlayDegradedReason =
27
+ | "target_repo_hooks_absent"
28
+ | "target_repo_hooks_unrecognized"
29
+ | "local_edit_watchers_absent";
30
+
31
+ export interface GitTransitionHookEvent {
32
+ readonly hookName: (typeof TARGET_GIT_TRANSITION_HOOKS)[number];
33
+ readonly hookArgs: readonly string[];
34
+ readonly worktreeRoot: string;
35
+ readonly observedAt: string;
36
+ }
37
+
38
+ export interface GitHookBootstrapStatus {
39
+ readonly posture: GitHookBootstrapPosture;
40
+ readonly configuredCoreHooksPath: string | null;
41
+ readonly resolvedHooksPath: string;
42
+ readonly requiredHooks: readonly string[];
43
+ readonly presentHooks: readonly string[];
44
+ readonly missingHooks: readonly string[];
45
+ readonly supportsCheckoutBoundaries: boolean;
46
+ }
47
+
48
+ export interface RuntimeWorkspaceOverlayFooting {
49
+ readonly observationMode: WorkspaceOverlayObservationMode;
50
+ readonly lineagePosture: WorkspaceOverlayLineagePosture;
51
+ readonly boundaryAuthority: WorkspaceOverlayBoundaryAuthority;
52
+ readonly degraded: true;
53
+ readonly degradedReason: WorkspaceOverlayDegradedReason;
54
+ readonly checkoutEpoch: number;
55
+ readonly lastTransition: RepoTransition | null;
56
+ readonly workspaceOverlayId: string | null;
57
+ readonly workspaceOverlay: WorkspaceOverlaySummary | null;
58
+ readonly hookBootstrap: GitHookBootstrapStatus;
59
+ readonly latestHookEvent: GitTransitionHookEvent | null;
60
+ }
61
+
62
+ async function readGitConfig(
63
+ gitClient: GitClient,
64
+ cwd: string,
65
+ key: string,
66
+ ): Promise<string | null> {
67
+ const result = await gitClient.run({
68
+ args: ["config", "--get", key],
69
+ cwd,
70
+ });
71
+ if (result.status !== 0) {
72
+ return null;
73
+ }
74
+ const value = result.stdout.trim();
75
+ return value.length > 0 ? value : null;
76
+ }
77
+
78
+ async function fileExists(fs: FileSystem, targetPath: string): Promise<boolean> {
79
+ try {
80
+ await fs.readFile(targetPath, "utf-8");
81
+ return true;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ function equivalentWorktreeRoots(left: string, right: string): boolean {
88
+ const leftResolved = path.resolve(left);
89
+ const rightResolved = path.resolve(right);
90
+ if (leftResolved === rightResolved) {
91
+ return true;
92
+ }
93
+ return (
94
+ leftResolved === `/private${rightResolved}`
95
+ || rightResolved === `/private${leftResolved}`
96
+ );
97
+ }
98
+
99
+ function parseHookEventLine(
100
+ line: string,
101
+ worktreeRoot: string,
102
+ ): GitTransitionHookEvent | null {
103
+ try {
104
+ const parsed = JSON.parse(line) as Record<string, unknown>;
105
+ const hookName = parsed["hookName"];
106
+ const hookArgs = parsed["hookArgs"];
107
+ const observedAt = parsed["observedAt"];
108
+ const observedWorktreeRoot = parsed["worktreeRoot"];
109
+ if (
110
+ typeof hookName !== "string"
111
+ || !isTargetGitTransitionHookName(hookName)
112
+ || !Array.isArray(hookArgs)
113
+ || !hookArgs.every((value) => typeof value === "string")
114
+ || typeof observedAt !== "string"
115
+ || typeof observedWorktreeRoot !== "string"
116
+ || !equivalentWorktreeRoots(observedWorktreeRoot, worktreeRoot)
117
+ ) {
118
+ return null;
119
+ }
120
+ return {
121
+ hookName,
122
+ hookArgs,
123
+ worktreeRoot: observedWorktreeRoot,
124
+ observedAt,
125
+ };
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+
131
+ async function readLatestHookEvent(
132
+ fs: FileSystem,
133
+ worktreeRoot: string,
134
+ ): Promise<GitTransitionHookEvent | null> {
135
+ const logPath = path.join(worktreeRoot, ".graft", "runtime", "git-transitions.ndjson");
136
+ try {
137
+ const content = await fs.readFile(logPath, "utf-8");
138
+ const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
139
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
140
+ const line = lines[index];
141
+ if (line === undefined) continue;
142
+ const parsed = parseHookEventLine(line, worktreeRoot);
143
+ if (parsed !== null) {
144
+ return parsed;
145
+ }
146
+ }
147
+ return null;
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ export async function inspectGitHookBootstrap(
154
+ fs: FileSystem,
155
+ gitClient: GitClient,
156
+ worktreeRoot: string,
157
+ gitCommonDir: string,
158
+ ): Promise<GitHookBootstrapStatus> {
159
+ const configuredCoreHooksPath = await readGitConfig(
160
+ gitClient,
161
+ worktreeRoot,
162
+ "core.hooksPath",
163
+ );
164
+ const resolvedHooksPath = resolveGitHooksPath(
165
+ worktreeRoot,
166
+ gitCommonDir,
167
+ configuredCoreHooksPath,
168
+ );
169
+
170
+ const presentHooks: string[] = [];
171
+ const missingHooks: string[] = [];
172
+ let recognizedHooks = 0;
173
+ let externalHooks = 0;
174
+ for (const hookName of TARGET_GIT_TRANSITION_HOOKS) {
175
+ const hookPath = path.join(resolvedHooksPath, hookName);
176
+ if (await fileExists(fs, hookPath)) {
177
+ presentHooks.push(hookName);
178
+ try {
179
+ const content = await fs.readFile(hookPath, "utf-8");
180
+ if (isRecognizedTargetGitHook(content, hookName)) {
181
+ recognizedHooks += 1;
182
+ } else {
183
+ externalHooks += 1;
184
+ }
185
+ } catch {
186
+ externalHooks += 1;
187
+ }
188
+ } else {
189
+ missingHooks.push(hookName);
190
+ }
191
+ }
192
+
193
+ const allHooksInstalled = recognizedHooks === TARGET_GIT_TRANSITION_HOOKS.length;
194
+ return {
195
+ posture: allHooksInstalled
196
+ ? "installed"
197
+ : (presentHooks.length === 0 ? "absent" : "external_unknown"),
198
+ configuredCoreHooksPath,
199
+ resolvedHooksPath,
200
+ requiredHooks: [...TARGET_GIT_TRANSITION_HOOKS],
201
+ presentHooks,
202
+ missingHooks,
203
+ supportsCheckoutBoundaries: allHooksInstalled && externalHooks === 0,
204
+ };
205
+ }
206
+
207
+ export async function buildRuntimeWorkspaceOverlayFooting(
208
+ fs: FileSystem,
209
+ gitClient: GitClient,
210
+ worktreeRoot: string,
211
+ gitCommonDir: string,
212
+ repoState: {
213
+ readonly checkoutEpoch: number;
214
+ readonly lastTransition: RepoTransition | null;
215
+ readonly workspaceOverlayId: string | null;
216
+ readonly workspaceOverlay: WorkspaceOverlaySummary | null;
217
+ },
218
+ ): Promise<RuntimeWorkspaceOverlayFooting> {
219
+ const hookBootstrap = await inspectGitHookBootstrap(
220
+ fs,
221
+ gitClient,
222
+ worktreeRoot,
223
+ gitCommonDir,
224
+ );
225
+ const latestHookEvent = hookBootstrap.supportsCheckoutBoundaries
226
+ ? await readLatestHookEvent(fs, worktreeRoot)
227
+ : null;
228
+ const lineagePosture = repoState.checkoutEpoch > 0 || latestHookEvent !== null || repoState.lastTransition !== null
229
+ ? "forked_after_transition"
230
+ : "stable";
231
+ const boundaryAuthority = latestHookEvent !== null
232
+ ? "hook_observed"
233
+ : (repoState.lastTransition !== null || repoState.checkoutEpoch > 0
234
+ ? "repo_snapshot"
235
+ : "none");
236
+ return {
237
+ observationMode: latestHookEvent === null
238
+ ? "inferred_between_tool_calls"
239
+ : "hook_observed_checkout_boundaries",
240
+ lineagePosture,
241
+ boundaryAuthority,
242
+ degraded: true,
243
+ degradedReason: hookBootstrap.supportsCheckoutBoundaries
244
+ ? "local_edit_watchers_absent"
245
+ : (hookBootstrap.posture === "absent"
246
+ ? "target_repo_hooks_absent"
247
+ : "target_repo_hooks_unrecognized"),
248
+ checkoutEpoch: repoState.checkoutEpoch,
249
+ lastTransition: repoState.lastTransition,
250
+ workspaceOverlayId: repoState.workspaceOverlayId,
251
+ workspaceOverlay: repoState.workspaceOverlay,
252
+ hookBootstrap,
253
+ latestHookEvent,
254
+ };
255
+ }
@@ -0,0 +1,60 @@
1
+ import type { CausalSurfaceNextAction } from "../contracts/causal-surface-next-action.js";
2
+ import type { PersistedLocalHistorySummary } from "./persisted-local-history.js";
3
+ import type { RepoConcurrencySummary } from "./repo-concurrency.js";
4
+ import type { RepoObservation } from "./repo-state.js";
5
+
6
+ export function deriveCausalSurfaceNextAction(
7
+ summaryNextAction: PersistedLocalHistorySummary["nextAction"],
8
+ semanticTransition: RepoObservation["semanticTransition"],
9
+ repoConcurrency: RepoConcurrencySummary | null,
10
+ ): CausalSurfaceNextAction {
11
+ if (
12
+ summaryNextAction === "bind_workspace_to_begin_local_history"
13
+ || summaryNextAction === "inspect_or_resume_local_history"
14
+ ) {
15
+ return summaryNextAction;
16
+ }
17
+
18
+ if (semanticTransition !== null) {
19
+ switch (semanticTransition.kind) {
20
+ case "conflict_resolution":
21
+ return "resolve_conflicts_before_continuing";
22
+ case "merge_phase":
23
+ return semanticTransition.phase === "completed_or_cleared"
24
+ ? summaryNextAction
25
+ : "complete_merge_phase_before_continuing";
26
+ case "rebase_phase":
27
+ return semanticTransition.phase === "completed_or_cleared"
28
+ ? summaryNextAction
29
+ : "continue_rebase_phase_before_continuing";
30
+ case "bulk_transition":
31
+ return "inspect_bulk_transition_scope_before_continuing";
32
+ case "index_update":
33
+ case "unknown":
34
+ break;
35
+ }
36
+ }
37
+
38
+ if (repoConcurrency === null) {
39
+ return summaryNextAction;
40
+ }
41
+
42
+ switch (repoConcurrency.posture) {
43
+ case "divergent_checkout":
44
+ return "review_divergent_checkout_before_continuing";
45
+ case "shared_worktree":
46
+ if (summaryNextAction === "review_transition_boundary_before_continuing") {
47
+ return summaryNextAction;
48
+ }
49
+ return "coordinate_shared_worktree_before_continuing";
50
+ case "overlapping_actors":
51
+ if (summaryNextAction === "review_transition_boundary_before_continuing") {
52
+ return summaryNextAction;
53
+ }
54
+ return "inspect_overlapping_actor_activity_before_continuing";
55
+ case "exclusive":
56
+ case "shared_repo_only":
57
+ case "unknown":
58
+ return summaryNextAction;
59
+ }
60
+ }
@@ -0,0 +1,130 @@
1
+ interface SemanticTransitionSummaryEvidence {
2
+ readonly totalPaths: number;
3
+ readonly stagedPaths: number;
4
+ readonly changedPaths: number;
5
+ readonly untrackedPaths: number;
6
+ readonly unmergedPaths: number;
7
+ readonly mergeInProgress: boolean;
8
+ readonly rebaseInProgress: boolean;
9
+ readonly rebaseStep: number | null;
10
+ readonly rebaseTotalSteps: number | null;
11
+ readonly lastTransitionKind: "checkout" | "reset" | "merge" | "rebase" | null;
12
+ readonly reflogSubject: string | null;
13
+ }
14
+
15
+ type SemanticTransitionKind =
16
+ | "index_update"
17
+ | "conflict_resolution"
18
+ | "merge_phase"
19
+ | "rebase_phase"
20
+ | "bulk_transition"
21
+ | "unknown";
22
+
23
+ type SemanticTransitionPhase =
24
+ | "started"
25
+ | "conflicted"
26
+ | "resolved_waiting_commit"
27
+ | "continued"
28
+ | "completed_or_cleared"
29
+ | null;
30
+
31
+ function buildConflictResolutionSummary(
32
+ evidence: SemanticTransitionSummaryEvidence,
33
+ previousUnmergedPaths: number | null,
34
+ ): string {
35
+ if (evidence.unmergedPaths === 0) {
36
+ if (previousUnmergedPaths !== null && previousUnmergedPaths > 0) {
37
+ return `Conflict posture cleared from ${String(previousUnmergedPaths)} conflicted path(s).`;
38
+ }
39
+ return "Conflict posture cleared after an explicit conflict-resolution movement.";
40
+ }
41
+
42
+ if (previousUnmergedPaths === null) {
43
+ return `Conflict posture is active across ${String(evidence.unmergedPaths)} path(s).`;
44
+ }
45
+
46
+ if (previousUnmergedPaths === 0) {
47
+ return `Conflict posture emerged across ${String(evidence.unmergedPaths)} path(s).`;
48
+ }
49
+
50
+ if (evidence.unmergedPaths < previousUnmergedPaths) {
51
+ return `Conflict posture is shrinking from ${String(previousUnmergedPaths)} to ${String(evidence.unmergedPaths)} path(s).`;
52
+ }
53
+
54
+ if (evidence.unmergedPaths > previousUnmergedPaths) {
55
+ return `Conflict posture widened from ${String(previousUnmergedPaths)} to ${String(evidence.unmergedPaths)} path(s).`;
56
+ }
57
+
58
+ return `Conflict posture remains active across ${String(evidence.unmergedPaths)} path(s).`;
59
+ }
60
+
61
+ function buildBulkTransitionSummary(evidence: SemanticTransitionSummaryEvidence): string {
62
+ if (
63
+ evidence.stagedPaths === evidence.totalPaths
64
+ && evidence.changedPaths === 0
65
+ && evidence.untrackedPaths === 0
66
+ ) {
67
+ return `Bulk staging spans ${String(evidence.totalPaths)} path(s) at the index boundary.`;
68
+ }
69
+ if (
70
+ evidence.changedPaths === evidence.totalPaths
71
+ && evidence.stagedPaths === 0
72
+ && evidence.untrackedPaths === 0
73
+ ) {
74
+ return `Bulk edit sweep spans ${String(evidence.totalPaths)} unstaged path(s).`;
75
+ }
76
+ return `Bulk transition movement spans ${String(evidence.totalPaths)} path(s) with ${String(evidence.stagedPaths)} staged and ${String(evidence.changedPaths)} unstaged changes.`;
77
+ }
78
+
79
+ export function buildSemanticTransitionSummary(input: {
80
+ readonly kind: SemanticTransitionKind;
81
+ readonly evidence: SemanticTransitionSummaryEvidence;
82
+ readonly phase: SemanticTransitionPhase;
83
+ readonly previousUnmergedPaths?: number | null;
84
+ }): string {
85
+ const { kind, evidence, phase, previousUnmergedPaths = null } = input;
86
+ switch (kind) {
87
+ case "merge_phase":
88
+ switch (phase) {
89
+ case "conflicted":
90
+ return `Merge is in conflict across ${String(evidence.unmergedPaths)} path(s).`;
91
+ case "resolved_waiting_commit":
92
+ return "Merge conflicts are cleared and the merge is waiting for commit/admission.";
93
+ case "completed_or_cleared":
94
+ return "Merge transition completed or merge state has cleared.";
95
+ case "started":
96
+ return "Merge started and is now inspectable as active repo state.";
97
+ case "continued":
98
+ return "Merge progressed and remains active.";
99
+ default:
100
+ return "Merge state is active.";
101
+ }
102
+ case "rebase_phase":
103
+ switch (phase) {
104
+ case "conflicted":
105
+ return `Rebase is in conflict across ${String(evidence.unmergedPaths)} path(s).`;
106
+ case "continued":
107
+ return evidence.rebaseStep !== null && evidence.rebaseTotalSteps !== null
108
+ ? `Rebase continued at step ${String(evidence.rebaseStep)} of ${String(evidence.rebaseTotalSteps)}.`
109
+ : "Rebase continued and remains active.";
110
+ case "completed_or_cleared":
111
+ return "Rebase transition completed or rebase state has cleared.";
112
+ case "started":
113
+ return "Rebase started and is now inspectable as active repo state.";
114
+ case "resolved_waiting_commit":
115
+ return "Rebase conflicts are cleared and the rebase is waiting for the next step.";
116
+ default:
117
+ return "Rebase state is active.";
118
+ }
119
+ case "conflict_resolution":
120
+ return buildConflictResolutionSummary(evidence, previousUnmergedPaths);
121
+ case "index_update":
122
+ return evidence.totalPaths > 1
123
+ ? `Index/staging boundary changed across ${String(evidence.totalPaths)} path(s).`
124
+ : "Index/staging boundary changed.";
125
+ case "bulk_transition":
126
+ return buildBulkTransitionSummary(evidence);
127
+ case "unknown":
128
+ return "Repo/workspace movement is inspectable, but the semantic transition meaning remains unknown.";
129
+ }
130
+ }