@searls/turbocommit 0.15.1 → 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 +7 -3
- package/lib/install.js +3 -3
- package/lib/session.js +2 -3
- package/lib/track.js +34 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,15 +25,19 @@ 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
|
|
33
35
|
checkout, unchanged and otherwise-unclaimed paths are committed separately
|
|
34
36
|
as recovered shell changes. Evidence is scoped to its exact worktree and is
|
|
35
37
|
permanently discarded if file contents or executable modes change, or if an
|
|
36
|
-
unfinished shell snapshot expires.
|
|
38
|
+
unfinished shell snapshot expires. Claims from a session whose turn has been
|
|
39
|
+
idle for over an hour no longer block shell attribution, so a turn that never
|
|
40
|
+
stopped cannot leave its paths unowned forever.
|
|
37
41
|
- **PostToolUseFailure**, **Stop**, and **SessionEnd** finalize shell snapshots
|
|
38
42
|
whose normal post hook did not arrive. Claude normally finishes within its
|
|
39
43
|
SessionEnd hook. If its internal deadline expires, Turbocommit stores the
|
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
|
-
|
|
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
|
@@ -8,6 +8,11 @@ const { turbocommitDir } = require('./session')
|
|
|
8
8
|
const { canonicalRoot, canonicalTrackedPath, changedPathsInRepository, gitCommonDir } = require('./git')
|
|
9
9
|
|
|
10
10
|
const BASH_SNAPSHOT_TTL_MS = 60 * 60 * 1000
|
|
11
|
+
// A session's tracking file is rewritten on every tool call and deleted when
|
|
12
|
+
// its turn stops. One that has been idle this long belongs to a turn that
|
|
13
|
+
// never stopped, so its claims must not block other sessions from owning
|
|
14
|
+
// paths they change with shell commands.
|
|
15
|
+
const CLAIM_TTL_MS = 60 * 60 * 1000
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Directory under the git common dir where turbocommit stores tracking state.
|
|
@@ -40,6 +45,8 @@ const FILE_PATH_KEYS = [
|
|
|
40
45
|
'notebook_path',
|
|
41
46
|
'relative_path',
|
|
42
47
|
'relativePath',
|
|
48
|
+
'sourcePath',
|
|
49
|
+
'destinationPath',
|
|
43
50
|
'uri'
|
|
44
51
|
]
|
|
45
52
|
const FILE_PATH_ARRAY_KEYS = ['file_paths', 'filePaths', 'paths', 'files']
|
|
@@ -119,6 +126,15 @@ function extractFilePaths (toolName, toolInput, cwd) {
|
|
|
119
126
|
})
|
|
120
127
|
}
|
|
121
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
|
+
|
|
122
138
|
/**
|
|
123
139
|
* PreToolUse handler. Persists ownership before waiting for overlap recovery.
|
|
124
140
|
* Returns false when the caller must deny the tool rather than let it run
|
|
@@ -153,9 +169,9 @@ function handleTrack (input, root, opts = {}) {
|
|
|
153
169
|
let pendingSnapshot = false
|
|
154
170
|
let preclaim = null
|
|
155
171
|
try {
|
|
156
|
-
if (toolName
|
|
157
|
-
entry.command = toolInput.command
|
|
158
|
-
savePendingBashSnapshot(root, sessionId, toolUseId, cwd)
|
|
172
|
+
if (snapshotsRepository(toolName)) {
|
|
173
|
+
if (toolName === 'Bash') entry.command = toolInput.command
|
|
174
|
+
savePendingBashSnapshot(root, sessionId, toolUseId, cwd, toolName)
|
|
159
175
|
pendingSnapshot = true
|
|
160
176
|
appendTracking(root, sessionId, entry)
|
|
161
177
|
} else {
|
|
@@ -205,7 +221,7 @@ function handlePostTrack (input, root) {
|
|
|
205
221
|
|
|
206
222
|
const sessionId = hookInput.sessionId || hookInput.session_id
|
|
207
223
|
const toolName = hookInput.toolName || hookInput.tool_name
|
|
208
|
-
if (!sessionId || toolName
|
|
224
|
+
if (!sessionId || !snapshotsRepository(toolName)) return
|
|
209
225
|
|
|
210
226
|
const toolUseId = hookInput.toolUseId || hookInput.tool_use_id
|
|
211
227
|
const release = acquireBashOverlapRecoveryLock(root, 5000)
|
|
@@ -234,7 +250,7 @@ function finishBashSnapshot (root, snapshot, { cwd, endedAt }) {
|
|
|
234
250
|
const candidates = changed.filter(file => !before.has(file) && !claimed.has(file))
|
|
235
251
|
const files = overlapping ? [] : candidates
|
|
236
252
|
|
|
237
|
-
const entry = { tool: 'Bash', phase: 'post', t: endedAt, cwd }
|
|
253
|
+
const entry = { tool: snapshot.toolName || 'Bash', phase: 'post', t: endedAt, cwd }
|
|
238
254
|
if (overlapping) {
|
|
239
255
|
entry.overlapping = true
|
|
240
256
|
entry.overlapEventId = recordBashOverlap(root, snapshot, overlaps, candidates, endedAt)
|
|
@@ -263,12 +279,13 @@ function bashSnapshotPath (root, sessionId, toolUseId) {
|
|
|
263
279
|
return path.join(dir, key + '.json')
|
|
264
280
|
}
|
|
265
281
|
|
|
266
|
-
function savePendingBashSnapshot (root, sessionId, toolUseId, cwd) {
|
|
282
|
+
function savePendingBashSnapshot (root, sessionId, toolUseId, cwd, toolName = 'Bash') {
|
|
267
283
|
const checkout = canonicalRoot(root)
|
|
268
284
|
writeBashSnapshot(root, sessionId, toolUseId, {
|
|
269
285
|
root: checkout,
|
|
270
286
|
cwd: cwd || checkout,
|
|
271
287
|
sessionId,
|
|
288
|
+
toolName,
|
|
272
289
|
toolUseId: toolUseId || null,
|
|
273
290
|
createdAt: Date.now(),
|
|
274
291
|
pending: true
|
|
@@ -431,9 +448,11 @@ function claimedPathsBySessions (root, excludedSessionId) {
|
|
|
431
448
|
} catch {
|
|
432
449
|
files = []
|
|
433
450
|
}
|
|
451
|
+
const cutoff = Date.now() - CLAIM_TTL_MS
|
|
434
452
|
for (const file of files) {
|
|
435
453
|
if (file === excludedSessionId + '.jsonl' || !file.endsWith('.jsonl')) continue
|
|
436
454
|
try {
|
|
455
|
+
if (fs.statSync(path.join(dir, file)).mtimeMs < cutoff) continue
|
|
437
456
|
const entries = fs.readFileSync(path.join(dir, file), 'utf8').trim().split('\n')
|
|
438
457
|
for (const line of entries) {
|
|
439
458
|
const entry = JSON.parse(line)
|
|
@@ -452,6 +471,7 @@ function claimedPathsBySessions (root, excludedSessionId) {
|
|
|
452
471
|
for (const file of claims) {
|
|
453
472
|
if (!file.endsWith('.json')) continue
|
|
454
473
|
try {
|
|
474
|
+
if (fs.statSync(path.join(claimsDir, file)).mtimeMs < cutoff) continue
|
|
455
475
|
const claim = JSON.parse(fs.readFileSync(path.join(claimsDir, file), 'utf8'))
|
|
456
476
|
if (claim.sessionId === excludedSessionId || !Array.isArray(claim.entry?.files)) continue
|
|
457
477
|
for (const claimed of claim.entry.files) result.add(canonicalTrackedPath(claimed))
|
|
@@ -910,12 +930,15 @@ function parseInput (input) {
|
|
|
910
930
|
/**
|
|
911
931
|
* Check whether a session has tracked any file-modifying tool calls.
|
|
912
932
|
* A Bash pre-hook alone doesn't count because shell commands may be read-only.
|
|
913
|
-
* A
|
|
933
|
+
* A snapshot post-hook counts only when it found newly dirty paths.
|
|
914
934
|
*/
|
|
915
935
|
function hasTrackedModifications (root, sessionId) {
|
|
916
|
-
return readTracking(root, sessionId).some(
|
|
917
|
-
|
|
918
|
-
|
|
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'
|
|
919
942
|
}
|
|
920
943
|
|
|
921
944
|
function readTracking (root, sessionId) {
|
|
@@ -978,6 +1001,7 @@ module.exports = {
|
|
|
978
1001
|
fingerprintTrackedPath,
|
|
979
1002
|
claimedPathsBySessions,
|
|
980
1003
|
hasTrackedModifications,
|
|
1004
|
+
describesModification,
|
|
981
1005
|
cleanupTracking,
|
|
982
1006
|
extractFilePath,
|
|
983
1007
|
extractFilePaths,
|