@searls/turbocommit 0.15.2 → 0.15.3

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.
package/README.md CHANGED
@@ -25,8 +25,10 @@ turbocommit registers hooks with the harnesses you use:
25
25
 
26
26
  - **PreToolUse** tentatively claims the paths supplied to editing tools while
27
27
  recovery finishes, promotes the claim only when the tool may proceed, and
28
- snapshots the repository before shell commands.
29
- - **PostToolUse** attributes paths that became dirty during a shell command.
28
+ snapshots the repository before shell commands and MCP tool calls.
29
+ - **PostToolUse** attributes paths that became dirty during a shell command or
30
+ MCP tool call, so files a tool rewrites without naming them (an Xcode project
31
+ file, for example) belong to the session that changed them.
30
32
  Concurrent commands in one session share ownership. Commands from different
31
33
  sessions retain hashed overlap evidence instead of claiming each other's
32
34
  paths. Once every involved turn stops and no shell remains active in that
package/lib/install.js CHANGED
@@ -11,11 +11,11 @@ const HOOK_DEFS = {
11
11
  hooks: [{ type: 'command', command: 'turbocommit hook pre-tool-use --harness claude' }]
12
12
  },
13
13
  PostToolUse: {
14
- matcher: 'Bash',
14
+ matcher: 'Bash|mcp__.*',
15
15
  hooks: [{ type: 'command', command: 'turbocommit hook post-tool-use --harness claude' }]
16
16
  },
17
17
  PostToolUseFailure: {
18
- matcher: 'Bash',
18
+ matcher: 'Bash|mcp__.*',
19
19
  hooks: [{ type: 'command', command: 'turbocommit hook post-tool-use --harness claude' }]
20
20
  },
21
21
  SessionStart: {
@@ -35,7 +35,7 @@ const CODEX_HOOK_DEFS = {
35
35
  hooks: [{ type: 'command', command: 'turbocommit hook pre-tool-use --harness codex' }]
36
36
  },
37
37
  PostToolUse: {
38
- matcher: 'Bash',
38
+ matcher: 'Bash|mcp__.*',
39
39
  hooks: [{ type: 'command', command: 'turbocommit hook post-tool-use --harness codex' }]
40
40
  },
41
41
  SessionStart: {
package/lib/session.js CHANGED
@@ -328,11 +328,10 @@ function trackingMayDescribeModifications (file) {
328
328
  } catch {
329
329
  return true
330
330
  }
331
+ const { describesModification } = require('./track')
331
332
  return lines.some(line => {
332
333
  try {
333
- const entry = JSON.parse(line)
334
- return entry.tool !== 'Bash' ||
335
- (entry.phase === 'post' && Array.isArray(entry.files) && entry.files.length > 0)
334
+ return describesModification(JSON.parse(line))
336
335
  } catch {
337
336
  return true
338
337
  }
package/lib/track.js CHANGED
@@ -45,6 +45,8 @@ const FILE_PATH_KEYS = [
45
45
  'notebook_path',
46
46
  'relative_path',
47
47
  'relativePath',
48
+ 'sourcePath',
49
+ 'destinationPath',
48
50
  'uri'
49
51
  ]
50
52
  const FILE_PATH_ARRAY_KEYS = ['file_paths', 'filePaths', 'paths', 'files']
@@ -124,6 +126,15 @@ function extractFilePaths (toolName, toolInput, cwd) {
124
126
  })
125
127
  }
126
128
 
129
+ /**
130
+ * Bash and MCP tools change paths they never name (a shell script, an Xcode
131
+ * project file rewritten as a side effect), so both are attributed by
132
+ * snapshotting the checkout before and after the call.
133
+ */
134
+ function snapshotsRepository (toolName) {
135
+ return toolName === 'Bash' || (typeof toolName === 'string' && toolName.startsWith('mcp__'))
136
+ }
137
+
127
138
  /**
128
139
  * PreToolUse handler. Persists ownership before waiting for overlap recovery.
129
140
  * Returns false when the caller must deny the tool rather than let it run
@@ -158,9 +169,9 @@ function handleTrack (input, root, opts = {}) {
158
169
  let pendingSnapshot = false
159
170
  let preclaim = null
160
171
  try {
161
- if (toolName === 'Bash') {
162
- entry.command = toolInput.command
163
- savePendingBashSnapshot(root, sessionId, toolUseId, cwd)
172
+ if (snapshotsRepository(toolName)) {
173
+ if (toolName === 'Bash') entry.command = toolInput.command
174
+ savePendingBashSnapshot(root, sessionId, toolUseId, cwd, toolName)
164
175
  pendingSnapshot = true
165
176
  appendTracking(root, sessionId, entry)
166
177
  } else {
@@ -210,7 +221,7 @@ function handlePostTrack (input, root) {
210
221
 
211
222
  const sessionId = hookInput.sessionId || hookInput.session_id
212
223
  const toolName = hookInput.toolName || hookInput.tool_name
213
- if (!sessionId || toolName !== 'Bash') return
224
+ if (!sessionId || !snapshotsRepository(toolName)) return
214
225
 
215
226
  const toolUseId = hookInput.toolUseId || hookInput.tool_use_id
216
227
  const release = acquireBashOverlapRecoveryLock(root, 5000)
@@ -239,7 +250,7 @@ function finishBashSnapshot (root, snapshot, { cwd, endedAt }) {
239
250
  const candidates = changed.filter(file => !before.has(file) && !claimed.has(file))
240
251
  const files = overlapping ? [] : candidates
241
252
 
242
- const entry = { tool: 'Bash', phase: 'post', t: endedAt, cwd }
253
+ const entry = { tool: snapshot.toolName || 'Bash', phase: 'post', t: endedAt, cwd }
243
254
  if (overlapping) {
244
255
  entry.overlapping = true
245
256
  entry.overlapEventId = recordBashOverlap(root, snapshot, overlaps, candidates, endedAt)
@@ -268,12 +279,13 @@ function bashSnapshotPath (root, sessionId, toolUseId) {
268
279
  return path.join(dir, key + '.json')
269
280
  }
270
281
 
271
- function savePendingBashSnapshot (root, sessionId, toolUseId, cwd) {
282
+ function savePendingBashSnapshot (root, sessionId, toolUseId, cwd, toolName = 'Bash') {
272
283
  const checkout = canonicalRoot(root)
273
284
  writeBashSnapshot(root, sessionId, toolUseId, {
274
285
  root: checkout,
275
286
  cwd: cwd || checkout,
276
287
  sessionId,
288
+ toolName,
277
289
  toolUseId: toolUseId || null,
278
290
  createdAt: Date.now(),
279
291
  pending: true
@@ -918,12 +930,15 @@ function parseInput (input) {
918
930
  /**
919
931
  * Check whether a session has tracked any file-modifying tool calls.
920
932
  * A Bash pre-hook alone doesn't count because shell commands may be read-only.
921
- * A Bash post-hook counts only when its snapshot found newly dirty paths.
933
+ * A snapshot post-hook counts only when it found newly dirty paths.
922
934
  */
923
935
  function hasTrackedModifications (root, sessionId) {
924
- return readTracking(root, sessionId).some(entry =>
925
- entry.tool !== 'Bash' || (entry.phase === 'post' && Array.isArray(entry.files) && entry.files.length > 0)
926
- )
936
+ return readTracking(root, sessionId).some(describesModification)
937
+ }
938
+
939
+ function describesModification (entry) {
940
+ if (entry.phase === 'post') return Array.isArray(entry.files) && entry.files.length > 0
941
+ return entry.tool !== 'Bash'
927
942
  }
928
943
 
929
944
  function readTracking (root, sessionId) {
@@ -986,6 +1001,7 @@ module.exports = {
986
1001
  fingerprintTrackedPath,
987
1002
  claimedPathsBySessions,
988
1003
  hasTrackedModifications,
1004
+ describesModification,
989
1005
  cleanupTracking,
990
1006
  extractFilePath,
991
1007
  extractFilePaths,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@searls/turbocommit",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
4
4
  "description": "Auto-commit after every AI coding agent turn",
5
5
  "bin": {
6
6
  "turbocommit": "./cli.js"