@mnemonik/shared 7.99.12 → 7.99.14

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.
@@ -35,6 +35,18 @@
35
35
  * work" that is the right signal.
36
36
  * - Only the repo containing cwd is observed. Not a git repo -> report nothing
37
37
  * rather than guess.
38
+ *
39
+ * Two additions (2026-09-19), both found by a session that made eleven
40
+ * commits in a worktree and was never asked to checkpoint:
41
+ * - The snapshot holds one dirty set PER repository root. A session that
42
+ * moves between the main checkout and a worktree used to reset the snapshot
43
+ * on every move, absorbing whatever was dirty at that moment as pre-existing
44
+ * and never crediting it.
45
+ * - A shell call whose command contains `git commit` is credited with the
46
+ * files that commit changed. An edit-and-commit in one command leaves the
47
+ * tree clean by the time it is observed, so the dirty diff sees nothing.
48
+ * HEAD is recorded per root at every observation; only commits made since
49
+ * the last observation count, and never a merge.
38
50
  */
39
51
  /**
40
52
  * The current dirty set as absolute paths, or null when cwd is not inside a
@@ -48,16 +60,18 @@ export declare function listGitDirtyPaths(cwd: string): {
48
60
  /** Session start: baseline the dirty set so pre-existing dirt is never reported. */
49
61
  export declare function captureGitDirtyBaseline(snapshotFile: string, cwd: string): void;
50
62
  /**
51
- * After a shell call: paths newly dirty since the last observation, capped.
52
- * Leaves additions pending until the caller confirms them with
53
- * addPathsToGitDirtySnapshot. Without a usable baseline it establishes one and
63
+ * After a shell call: paths newly dirty since the last observation of this
64
+ * repo, plus, when `command` contains `git commit`, the paths that commit
65
+ * changed. Capped. Leaves additions pending until the caller confirms them
66
+ * with addPathsToGitDirtySnapshot. A repo with no baseline yet gets one and
54
67
  * reports nothing - degrading toward silence, never toward a false edit.
55
68
  */
56
- export declare function diffGitDirtySnapshot(snapshotFile: string, cwd: string): string[];
69
+ export declare function diffGitDirtySnapshot(snapshotFile: string, cwd: string, command?: string): string[];
57
70
  /**
58
71
  * After an edit tool: fold tool-reported edits into the snapshot so the next
59
- * shell diff does not re-credit them. Only when a baseline exists - seeding a
60
- * partial snapshot would make later diffs report pre-existing dirt.
72
+ * shell diff does not re-credit them. Only when a baseline exists for the
73
+ * repo that holds the path - seeding a partial snapshot would make later diffs
74
+ * report pre-existing dirt.
61
75
  */
62
76
  export declare function addPathsToGitDirtySnapshot(snapshotFile: string, paths: string[]): void;
63
77
  //# sourceMappingURL=gitEditDetector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gitEditDetector.d.ts","sourceRoot":"","sources":["../src/gitEditDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAsCH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CAuBvF;AA0BD,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAK/E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBhF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAQtF"}
1
+ {"version":3,"file":"gitEditDetector.d.ts","sourceRoot":"","sources":["../src/gitEditDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAiDH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CAuBvF;AA+ED,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAK/E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,MAAM,EAAE,CAqB9F;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAWtF"}
@@ -35,6 +35,18 @@
35
35
  * work" that is the right signal.
36
36
  * - Only the repo containing cwd is observed. Not a git repo -> report nothing
37
37
  * rather than guess.
38
+ *
39
+ * Two additions (2026-09-19), both found by a session that made eleven
40
+ * commits in a worktree and was never asked to checkpoint:
41
+ * - The snapshot holds one dirty set PER repository root. A session that
42
+ * moves between the main checkout and a worktree used to reset the snapshot
43
+ * on every move, absorbing whatever was dirty at that moment as pre-existing
44
+ * and never crediting it.
45
+ * - A shell call whose command contains `git commit` is credited with the
46
+ * files that commit changed. An edit-and-commit in one command leaves the
47
+ * tree clean by the time it is observed, so the dirty diff sees nothing.
48
+ * HEAD is recorded per root at every observation; only commits made since
49
+ * the last observation count, and never a merge.
38
50
  */
39
51
  import { execFileSync } from 'node:child_process';
40
52
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
@@ -94,30 +106,77 @@ export function listGitDirtyPaths(cwd) {
94
106
  }
95
107
  return { root, paths };
96
108
  }
109
+ /** HEAD of the repo at cwd, or undefined before the first commit. */
110
+ function currentHead(cwd) {
111
+ return (runGit(cwd, ['rev-parse', '--verify', '-q', 'HEAD'], GIT_REV_PARSE_TIMEOUT_MS)?.trim() ||
112
+ undefined);
113
+ }
114
+ /**
115
+ * Files changed by the commits made between two heads, or by HEAD alone when
116
+ * there is no earlier head. Empty when any of those commits is a merge: a
117
+ * merge brings in other people's files and none of them is this session's
118
+ * edit. `git commit --amend` orphans the old head, so the range is still the
119
+ * one new commit and the diff is the amended change.
120
+ */
121
+ function committedPaths(cwd, root, from, to) {
122
+ const range = from && from !== to ? `${from}..${to}` : to;
123
+ const parents = runGit(cwd, from ? ['log', '--format=%P', range] : ['log', '--format=%P', '-1', to], GIT_STATUS_TIMEOUT_MS);
124
+ if (parents === null)
125
+ return [];
126
+ const lines = parents.split('\n').filter(Boolean);
127
+ if (lines.length === 0 || lines.some((line) => line.includes(' ')))
128
+ return [];
129
+ const names = from
130
+ ? runGit(cwd, ['diff', '--name-only', '-z', from, to], GIT_STATUS_TIMEOUT_MS)
131
+ : runGit(cwd, ['show', '--name-only', '-z', '--format=', to], GIT_STATUS_TIMEOUT_MS);
132
+ if (names === null)
133
+ return [];
134
+ return names
135
+ .split('\0')
136
+ .filter(Boolean)
137
+ .map((name) => join(root, name));
138
+ }
97
139
  function readSnapshot(snapshotFile) {
98
140
  try {
99
141
  const parsed = JSON.parse(readFileSync(snapshotFile, 'utf8'));
100
- if (parsed?.v !== 1 || typeof parsed.root !== 'string' || !Array.isArray(parsed.paths)) {
101
- return null;
142
+ if (parsed?.v === 1 && typeof parsed.root === 'string' && Array.isArray(parsed.paths)) {
143
+ return { v: 2, roots: { [parsed.root]: { paths: parsed.paths } } };
102
144
  }
145
+ if (parsed?.v !== 2 || typeof parsed.roots !== 'object' || parsed.roots === null)
146
+ return null;
103
147
  return parsed;
104
148
  }
105
149
  catch {
106
150
  return null;
107
151
  }
108
152
  }
109
- function writeSnapshot(snapshotFile, root, paths) {
153
+ /**
154
+ * Persist one root's list into the snapshot, keeping every other root. The
155
+ * whole file stays under MAX_SNAPSHOT_PATHS: when the total would exceed it,
156
+ * only the root being written survives.
157
+ */
158
+ function writeRoot(snapshotFile, root, entry) {
110
159
  try {
160
+ const roots = { ...(readSnapshot(snapshotFile)?.roots ?? {}), [root]: entry };
161
+ const total = Object.values(roots).reduce((sum, r) => sum + r.paths.length, 0);
162
+ const snapshot = {
163
+ v: 2,
164
+ roots: total > MAX_SNAPSHOT_PATHS ? { [root]: entry } : roots,
165
+ };
111
166
  mkdirSync(dirname(snapshotFile), { recursive: true, mode: 0o700 });
112
- writeFileSync(snapshotFile, JSON.stringify({ v: 1, root, paths }), {
113
- mode: 0o600,
114
- });
167
+ writeFileSync(snapshotFile, JSON.stringify(snapshot), { mode: 0o600 });
115
168
  }
116
169
  catch {
117
170
  // Best-effort: a missing snapshot degrades to "report nothing", never to a
118
171
  // wrong report.
119
172
  }
120
173
  }
174
+ /** The recorded root whose directory contains this absolute path, if any. */
175
+ function rootFor(snapshot, path) {
176
+ return Object.keys(snapshot.roots)
177
+ .filter((root) => path === root || path.startsWith(root.endsWith('/') ? root : `${root}/`))
178
+ .sort((x, y) => y.length - x.length)[0];
179
+ }
121
180
  /** Session start: baseline the dirty set so pre-existing dirt is never reported. */
122
181
  export function captureGitDirtyBaseline(snapshotFile, cwd) {
123
182
  if (!snapshotFile)
@@ -125,34 +184,45 @@ export function captureGitDirtyBaseline(snapshotFile, cwd) {
125
184
  const current = listGitDirtyPaths(cwd);
126
185
  if (!current)
127
186
  return;
128
- writeSnapshot(snapshotFile, current.root, current.paths);
187
+ writeRoot(snapshotFile, current.root, { paths: current.paths, head: currentHead(cwd) });
129
188
  }
130
189
  /**
131
- * After a shell call: paths newly dirty since the last observation, capped.
132
- * Leaves additions pending until the caller confirms them with
133
- * addPathsToGitDirtySnapshot. Without a usable baseline it establishes one and
190
+ * After a shell call: paths newly dirty since the last observation of this
191
+ * repo, plus, when `command` contains `git commit`, the paths that commit
192
+ * changed. Capped. Leaves additions pending until the caller confirms them
193
+ * with addPathsToGitDirtySnapshot. A repo with no baseline yet gets one and
134
194
  * reports nothing - degrading toward silence, never toward a false edit.
135
195
  */
136
- export function diffGitDirtySnapshot(snapshotFile, cwd) {
196
+ export function diffGitDirtySnapshot(snapshotFile, cwd, command = '') {
137
197
  if (!snapshotFile)
138
198
  return [];
139
199
  const current = listGitDirtyPaths(cwd);
140
200
  if (!current)
141
201
  return [];
142
- const baseline = readSnapshot(snapshotFile);
143
- if (!baseline || baseline.root !== current.root) {
144
- writeSnapshot(snapshotFile, current.root, current.paths);
202
+ const head = currentHead(cwd);
203
+ const baseline = readSnapshot(snapshotFile)?.roots[current.root];
204
+ if (!baseline) {
205
+ writeRoot(snapshotFile, current.root, { paths: current.paths, head });
145
206
  return [];
146
207
  }
147
208
  const known = new Set(baseline.paths);
148
209
  const additions = current.paths.filter((path) => !known.has(path));
149
- writeSnapshot(snapshotFile, current.root, current.paths.filter((path) => known.has(path)));
210
+ if (head && head !== baseline.head && /\bgit\s+commit\b/.test(command)) {
211
+ for (const path of committedPaths(cwd, current.root, baseline.head, head))
212
+ if (!known.has(path) && !additions.includes(path))
213
+ additions.push(path);
214
+ }
215
+ writeRoot(snapshotFile, current.root, {
216
+ paths: current.paths.filter((path) => known.has(path)),
217
+ head,
218
+ });
150
219
  return additions.slice(0, MAX_REPORTED_PATHS_PER_CALL);
151
220
  }
152
221
  /**
153
222
  * After an edit tool: fold tool-reported edits into the snapshot so the next
154
- * shell diff does not re-credit them. Only when a baseline exists - seeding a
155
- * partial snapshot would make later diffs report pre-existing dirt.
223
+ * shell diff does not re-credit them. Only when a baseline exists for the
224
+ * repo that holds the path - seeding a partial snapshot would make later diffs
225
+ * report pre-existing dirt.
156
226
  */
157
227
  export function addPathsToGitDirtySnapshot(snapshotFile, paths) {
158
228
  if (!snapshotFile || paths.length === 0)
@@ -160,10 +230,13 @@ export function addPathsToGitDirtySnapshot(snapshotFile, paths) {
160
230
  const snapshot = readSnapshot(snapshotFile);
161
231
  if (!snapshot)
162
232
  return;
163
- const known = new Set(snapshot.paths);
164
- const additions = paths.filter((p) => !known.has(p));
165
- if (additions.length === 0)
166
- return;
167
- writeSnapshot(snapshotFile, snapshot.root, [...snapshot.paths, ...additions]);
233
+ for (const path of paths) {
234
+ const root = rootFor(snapshot, path);
235
+ const entry = root ? snapshot.roots[root] : undefined;
236
+ if (!root || !entry || entry.paths.includes(path))
237
+ continue;
238
+ entry.paths = [...entry.paths, path];
239
+ writeRoot(snapshotFile, root, entry);
240
+ }
168
241
  }
169
242
  //# sourceMappingURL=gitEditDetector.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gitEditDetector.js","sourceRoot":"","sources":["../src/gitEditDetector.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gitEditDetector.js","sourceRoot":"","sources":["../src/gitEditDetector.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemonik/shared",
3
- "version": "7.99.12",
3
+ "version": "7.99.14",
4
4
  "description": "Shared constants and utilities for Mnemonik packages",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,6 +35,18 @@
35
35
  * work" that is the right signal.
36
36
  * - Only the repo containing cwd is observed. Not a git repo -> report nothing
37
37
  * rather than guess.
38
+ *
39
+ * Two additions (2026-09-19), both found by a session that made eleven
40
+ * commits in a worktree and was never asked to checkpoint:
41
+ * - The snapshot holds one dirty set PER repository root. A session that
42
+ * moves between the main checkout and a worktree used to reset the snapshot
43
+ * on every move, absorbing whatever was dirty at that moment as pre-existing
44
+ * and never crediting it.
45
+ * - A shell call whose command contains `git commit` is credited with the
46
+ * files that commit changed. An edit-and-commit in one command leaves the
47
+ * tree clean by the time it is observed, so the dirty diff sees nothing.
48
+ * HEAD is recorded per root at every observation; only commits made since
49
+ * the last observation count, and never a merge.
38
50
  */
39
51
 
40
52
  import { execFileSync } from 'node:child_process';
@@ -52,7 +64,18 @@ const MAX_REPORTED_PATHS_PER_CALL = 25;
52
64
  */
53
65
  const MAX_SNAPSHOT_PATHS = 2000;
54
66
 
67
+ interface RootSnapshot {
68
+ paths: string[];
69
+ head?: string;
70
+ }
71
+
55
72
  interface DirtySnapshot {
73
+ v: 2;
74
+ roots: Record<string, RootSnapshot>;
75
+ }
76
+
77
+ /** The pre-2026-09-19 shape: one root, one list. Read once, written as v2. */
78
+ interface LegacyDirtySnapshot {
56
79
  v: 1;
57
80
  root: string;
58
81
  paths: string[];
@@ -103,74 +126,136 @@ export function listGitDirtyPaths(cwd: string): { root: string; paths: string[]
103
126
  return { root, paths };
104
127
  }
105
128
 
129
+ /** HEAD of the repo at cwd, or undefined before the first commit. */
130
+ function currentHead(cwd: string): string | undefined {
131
+ return (
132
+ runGit(cwd, ['rev-parse', '--verify', '-q', 'HEAD'], GIT_REV_PARSE_TIMEOUT_MS)?.trim() ||
133
+ undefined
134
+ );
135
+ }
136
+
137
+ /**
138
+ * Files changed by the commits made between two heads, or by HEAD alone when
139
+ * there is no earlier head. Empty when any of those commits is a merge: a
140
+ * merge brings in other people's files and none of them is this session's
141
+ * edit. `git commit --amend` orphans the old head, so the range is still the
142
+ * one new commit and the diff is the amended change.
143
+ */
144
+ function committedPaths(cwd: string, root: string, from: string | undefined, to: string): string[] {
145
+ const range = from && from !== to ? `${from}..${to}` : to;
146
+ const parents = runGit(
147
+ cwd,
148
+ from ? ['log', '--format=%P', range] : ['log', '--format=%P', '-1', to],
149
+ GIT_STATUS_TIMEOUT_MS
150
+ );
151
+ if (parents === null) return [];
152
+ const lines = parents.split('\n').filter(Boolean);
153
+ if (lines.length === 0 || lines.some((line) => line.includes(' '))) return [];
154
+ const names = from
155
+ ? runGit(cwd, ['diff', '--name-only', '-z', from, to], GIT_STATUS_TIMEOUT_MS)
156
+ : runGit(cwd, ['show', '--name-only', '-z', '--format=', to], GIT_STATUS_TIMEOUT_MS);
157
+ if (names === null) return [];
158
+ return names
159
+ .split('\0')
160
+ .filter(Boolean)
161
+ .map((name) => join(root, name));
162
+ }
163
+
106
164
  function readSnapshot(snapshotFile: string): DirtySnapshot | null {
107
165
  try {
108
- const parsed = JSON.parse(readFileSync(snapshotFile, 'utf8')) as DirtySnapshot;
109
- if (parsed?.v !== 1 || typeof parsed.root !== 'string' || !Array.isArray(parsed.paths)) {
110
- return null;
166
+ const parsed = JSON.parse(readFileSync(snapshotFile, 'utf8')) as
167
+ DirtySnapshot | LegacyDirtySnapshot;
168
+ if (parsed?.v === 1 && typeof parsed.root === 'string' && Array.isArray(parsed.paths)) {
169
+ return { v: 2, roots: { [parsed.root]: { paths: parsed.paths } } };
111
170
  }
171
+ if (parsed?.v !== 2 || typeof parsed.roots !== 'object' || parsed.roots === null) return null;
112
172
  return parsed;
113
173
  } catch {
114
174
  return null;
115
175
  }
116
176
  }
117
177
 
118
- function writeSnapshot(snapshotFile: string, root: string, paths: string[]): void {
178
+ /**
179
+ * Persist one root's list into the snapshot, keeping every other root. The
180
+ * whole file stays under MAX_SNAPSHOT_PATHS: when the total would exceed it,
181
+ * only the root being written survives.
182
+ */
183
+ function writeRoot(snapshotFile: string, root: string, entry: RootSnapshot): void {
119
184
  try {
185
+ const roots = { ...(readSnapshot(snapshotFile)?.roots ?? {}), [root]: entry };
186
+ const total = Object.values(roots).reduce((sum, r) => sum + r.paths.length, 0);
187
+ const snapshot: DirtySnapshot = {
188
+ v: 2,
189
+ roots: total > MAX_SNAPSHOT_PATHS ? { [root]: entry } : roots,
190
+ };
120
191
  mkdirSync(dirname(snapshotFile), { recursive: true, mode: 0o700 });
121
- writeFileSync(snapshotFile, JSON.stringify({ v: 1, root, paths } satisfies DirtySnapshot), {
122
- mode: 0o600,
123
- });
192
+ writeFileSync(snapshotFile, JSON.stringify(snapshot), { mode: 0o600 });
124
193
  } catch {
125
194
  // Best-effort: a missing snapshot degrades to "report nothing", never to a
126
195
  // wrong report.
127
196
  }
128
197
  }
129
198
 
199
+ /** The recorded root whose directory contains this absolute path, if any. */
200
+ function rootFor(snapshot: DirtySnapshot, path: string): string | undefined {
201
+ return Object.keys(snapshot.roots)
202
+ .filter((root) => path === root || path.startsWith(root.endsWith('/') ? root : `${root}/`))
203
+ .sort((x, y) => y.length - x.length)[0];
204
+ }
205
+
130
206
  /** Session start: baseline the dirty set so pre-existing dirt is never reported. */
131
207
  export function captureGitDirtyBaseline(snapshotFile: string, cwd: string): void {
132
208
  if (!snapshotFile) return;
133
209
  const current = listGitDirtyPaths(cwd);
134
210
  if (!current) return;
135
- writeSnapshot(snapshotFile, current.root, current.paths);
211
+ writeRoot(snapshotFile, current.root, { paths: current.paths, head: currentHead(cwd) });
136
212
  }
137
213
 
138
214
  /**
139
- * After a shell call: paths newly dirty since the last observation, capped.
140
- * Leaves additions pending until the caller confirms them with
141
- * addPathsToGitDirtySnapshot. Without a usable baseline it establishes one and
215
+ * After a shell call: paths newly dirty since the last observation of this
216
+ * repo, plus, when `command` contains `git commit`, the paths that commit
217
+ * changed. Capped. Leaves additions pending until the caller confirms them
218
+ * with addPathsToGitDirtySnapshot. A repo with no baseline yet gets one and
142
219
  * reports nothing - degrading toward silence, never toward a false edit.
143
220
  */
144
- export function diffGitDirtySnapshot(snapshotFile: string, cwd: string): string[] {
221
+ export function diffGitDirtySnapshot(snapshotFile: string, cwd: string, command = ''): string[] {
145
222
  if (!snapshotFile) return [];
146
223
  const current = listGitDirtyPaths(cwd);
147
224
  if (!current) return [];
148
- const baseline = readSnapshot(snapshotFile);
149
- if (!baseline || baseline.root !== current.root) {
150
- writeSnapshot(snapshotFile, current.root, current.paths);
225
+ const head = currentHead(cwd);
226
+ const baseline = readSnapshot(snapshotFile)?.roots[current.root];
227
+ if (!baseline) {
228
+ writeRoot(snapshotFile, current.root, { paths: current.paths, head });
151
229
  return [];
152
230
  }
153
231
  const known = new Set(baseline.paths);
154
232
  const additions = current.paths.filter((path) => !known.has(path));
155
- writeSnapshot(
156
- snapshotFile,
157
- current.root,
158
- current.paths.filter((path) => known.has(path))
159
- );
233
+ if (head && head !== baseline.head && /\bgit\s+commit\b/.test(command)) {
234
+ for (const path of committedPaths(cwd, current.root, baseline.head, head))
235
+ if (!known.has(path) && !additions.includes(path)) additions.push(path);
236
+ }
237
+ writeRoot(snapshotFile, current.root, {
238
+ paths: current.paths.filter((path) => known.has(path)),
239
+ head,
240
+ });
160
241
  return additions.slice(0, MAX_REPORTED_PATHS_PER_CALL);
161
242
  }
162
243
 
163
244
  /**
164
245
  * After an edit tool: fold tool-reported edits into the snapshot so the next
165
- * shell diff does not re-credit them. Only when a baseline exists - seeding a
166
- * partial snapshot would make later diffs report pre-existing dirt.
246
+ * shell diff does not re-credit them. Only when a baseline exists for the
247
+ * repo that holds the path - seeding a partial snapshot would make later diffs
248
+ * report pre-existing dirt.
167
249
  */
168
250
  export function addPathsToGitDirtySnapshot(snapshotFile: string, paths: string[]): void {
169
251
  if (!snapshotFile || paths.length === 0) return;
170
252
  const snapshot = readSnapshot(snapshotFile);
171
253
  if (!snapshot) return;
172
- const known = new Set(snapshot.paths);
173
- const additions = paths.filter((p) => !known.has(p));
174
- if (additions.length === 0) return;
175
- writeSnapshot(snapshotFile, snapshot.root, [...snapshot.paths, ...additions]);
254
+ for (const path of paths) {
255
+ const root = rootFor(snapshot, path);
256
+ const entry = root ? snapshot.roots[root] : undefined;
257
+ if (!root || !entry || entry.paths.includes(path)) continue;
258
+ entry.paths = [...entry.paths, path];
259
+ writeRoot(snapshotFile, root, entry);
260
+ }
176
261
  }