@searls/turbocommit 0.14.1 → 0.15.1
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 +59 -14
- package/cli.js +250 -6
- package/lib/agent.js +10 -6
- package/lib/git.js +161 -7
- package/lib/harness.js +2 -0
- package/lib/install.js +21 -6
- package/lib/multi.js +106 -30
- package/lib/rescue.js +308 -0
- package/lib/run.js +215 -35
- package/lib/session.js +40 -1
- package/lib/track.js +849 -27
- package/package.json +1 -1
package/lib/run.js
CHANGED
|
@@ -4,10 +4,25 @@ const path = require('path')
|
|
|
4
4
|
const { loadJson } = require('./io')
|
|
5
5
|
const { parseTranscript, parseCodexTranscript, formatBody, formatTitleTranscript, extractHeadline, extractModel, extractCodexModel } = require('./transcript')
|
|
6
6
|
const { runTitleAgent, runBodyAgent } = require('./agent')
|
|
7
|
-
const { gitRoot, hasChanges, addAndCommit,
|
|
7
|
+
const { gitRoot, hasChanges, changedPaths, addAndCommit, stagePaths, commitPaths, commitStagedPaths, stagedChangeContext, currentBranch, pushClean, hasRepositoryOperation } = require('./git')
|
|
8
8
|
const { logEvent } = require('./log')
|
|
9
9
|
const { wrapText } = require('./wrap')
|
|
10
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
hasTrackedModifications,
|
|
12
|
+
cleanupTracking,
|
|
13
|
+
readTracking,
|
|
14
|
+
finalizeBashSnapshots,
|
|
15
|
+
recordBashSessionStop,
|
|
16
|
+
readReadyBashOverlaps,
|
|
17
|
+
pruneBashSnapshots,
|
|
18
|
+
compactBashOverlapEvents,
|
|
19
|
+
resolveBashOverlap,
|
|
20
|
+
invalidateBashOverlap,
|
|
21
|
+
hasActiveBashSnapshot,
|
|
22
|
+
acquireBashOverlapRecoveryLock,
|
|
23
|
+
fingerprintTrackedPath,
|
|
24
|
+
claimedPathsBySessions
|
|
25
|
+
} = require('./track')
|
|
11
26
|
const { redact, buildRedactions } = require('./redact')
|
|
12
27
|
const { handleSessionEnd, getAncestors, savePending, collectPending, cleanupConsumed, cleanupStale, readWatermark, saveWatermark, resolveParentCommit } = require('./session')
|
|
13
28
|
const { activeConfig } = require('./config')
|
|
@@ -19,7 +34,7 @@ const {
|
|
|
19
34
|
dependencyOrder,
|
|
20
35
|
loadManifest,
|
|
21
36
|
readMultiWatermark,
|
|
22
|
-
|
|
37
|
+
trackedChangesFromEntries,
|
|
23
38
|
saveManifest,
|
|
24
39
|
saveMultiWatermark
|
|
25
40
|
} = require('./multi')
|
|
@@ -97,7 +112,7 @@ function resolveCoauthor (config, transcriptPath, root, opts = {}) {
|
|
|
97
112
|
* Always exits 0 — never blocks Claude, never outputs to stdout.
|
|
98
113
|
*
|
|
99
114
|
* Skip/commit decision is based on PreToolUse tracking:
|
|
100
|
-
* - If tracking file
|
|
115
|
+
* - If tracking file contains changed paths → commit only those paths
|
|
101
116
|
* - If tracking file missing/empty → skip, buffer transcript for later pickup
|
|
102
117
|
*/
|
|
103
118
|
function run (input, opts = {}) {
|
|
@@ -108,19 +123,130 @@ function run (input, opts = {}) {
|
|
|
108
123
|
if (!hookInput) return
|
|
109
124
|
|
|
110
125
|
const root = gitRoot(hookInput.cwd || process.cwd())
|
|
111
|
-
if (!root
|
|
126
|
+
if (!root) return
|
|
127
|
+
const { config } = activeConfig(root)
|
|
128
|
+
if (config.enabled !== true) return
|
|
129
|
+
const recoveryDeadline = opts.recoveryDeadline ?? recoveryDeadlineFromWait(opts.recoveryWaitMs ?? 5000)
|
|
112
130
|
|
|
131
|
+
if (hookInput.sessionId) {
|
|
132
|
+
const finalized = finalizeBashSnapshots(root, hookInput.sessionId, Date.now(), remainingRecoveryWait(recoveryDeadline))
|
|
133
|
+
if (finalized == null) return
|
|
134
|
+
recordBashSessionStop(root, hookInput.sessionId)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
return runTrackedStop(hookInput, root, opts)
|
|
139
|
+
} finally {
|
|
140
|
+
const recoveryConfig = opts.deferPush ? { ...config, push: false } : config
|
|
141
|
+
recoverBashOverlaps(root, hookInput, recoveryConfig, remainingRecoveryWait(recoveryDeadline))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function recoveryDeadlineFromWait (waitMs) {
|
|
146
|
+
return waitMs === Infinity ? Infinity : Date.now() + waitMs
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function remainingRecoveryWait (deadline) {
|
|
150
|
+
return deadline === Infinity ? Infinity : Math.max(0, deadline - Date.now())
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function runTrackedStop (hookInput, root, opts) {
|
|
113
154
|
const entries = hookInput.sessionId ? readTracking(root, hookInput.sessionId) : []
|
|
114
|
-
const
|
|
155
|
+
const tracked = trackedChangesFromEntries(root, entries)
|
|
115
156
|
const manifest = loadManifest(root)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
157
|
+
if (manifest.discardedLegacyWork > 0) {
|
|
158
|
+
logEvent('skip', {
|
|
159
|
+
harness: hookInput.harness,
|
|
160
|
+
project: path.basename(root),
|
|
161
|
+
branch: currentBranch(root),
|
|
162
|
+
reason: 'legacy-manifest',
|
|
163
|
+
discarded: manifest.discardedLegacyWork
|
|
164
|
+
})
|
|
165
|
+
manifest.discardedLegacyWork = 0
|
|
166
|
+
deleteManifestIfEmpty(root, manifest)
|
|
167
|
+
}
|
|
168
|
+
const hasCrossRootPath = tracked.repos.length > 0 &&
|
|
169
|
+
(tracked.repos.length !== 1 || tracked.repos[0].root !== root)
|
|
170
|
+
|
|
171
|
+
if (tracked.ambiguous.length > 0) {
|
|
172
|
+
return runSingle(hookInput, { paths: [], trackingReason: 'ambiguous-tracking', operationDeadline: opts.operationDeadline })
|
|
173
|
+
}
|
|
174
|
+
if (manifest.repos.length > 0 || tracked.repos.length > 1 || hasCrossRootPath) {
|
|
175
|
+
return runMulti(hookInput, root, tracked.repos, manifest, opts)
|
|
176
|
+
}
|
|
177
|
+
return runSingle(hookInput, { paths: tracked.repos[0]?.paths || [], operationDeadline: opts.operationDeadline })
|
|
178
|
+
}
|
|
119
179
|
|
|
120
|
-
|
|
121
|
-
|
|
180
|
+
function recoverBashOverlaps (root, hookInput, config, waitMs = 5000) {
|
|
181
|
+
if (process.env.TURBOCOMMIT_DISABLED || !root) return
|
|
182
|
+
hookInput = hookInput || {}
|
|
183
|
+
config = config || activeConfig(root).config
|
|
184
|
+
if (config.enabled !== true) return
|
|
185
|
+
const release = acquireBashOverlapRecoveryLock(root, waitMs)
|
|
186
|
+
if (!release) return
|
|
187
|
+
let shouldPush = false
|
|
188
|
+
try {
|
|
189
|
+
pruneBashSnapshots(root)
|
|
190
|
+
if (hasActiveBashSnapshot(root) || hasRepositoryOperation(root)) return
|
|
191
|
+
for (const overlap of readReadyBashOverlaps(root)) {
|
|
192
|
+
const claimed = claimedPathsBySessions(root)
|
|
193
|
+
const changedFingerprint = overlap.paths.some(item =>
|
|
194
|
+
item.fingerprint == null || fingerprintTrackedPath(item.path) !== item.fingerprint)
|
|
195
|
+
const claimedPath = overlap.paths.some(item => claimed.has(item.path))
|
|
196
|
+
if (changedFingerprint || claimedPath) {
|
|
197
|
+
logEvent('skip', {
|
|
198
|
+
harness: hookInput.harness,
|
|
199
|
+
project: path.basename(root),
|
|
200
|
+
branch: currentBranch(root),
|
|
201
|
+
reason: changedFingerprint ? 'bash-overlap-changed' : 'bash-overlap-claimed',
|
|
202
|
+
paths: overlap.paths.length
|
|
203
|
+
})
|
|
204
|
+
if (changedFingerprint) {
|
|
205
|
+
invalidateBashOverlap(root, overlap.eventIds, 'changed-fingerprint')
|
|
206
|
+
}
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const paths = changedPaths(root, overlap.paths.map(item => item.path))
|
|
211
|
+
if (paths.length === 0) {
|
|
212
|
+
resolveBashOverlap(root, overlap.eventIds)
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const title = 'Recover overlapping shell changes'
|
|
217
|
+
const body = 'Recovered unchanged paths observed during overlapping shell commands.\n\n' +
|
|
218
|
+
overlap.sessionIds.map(sessionId => `Session: ${sessionId}`).join('\n')
|
|
219
|
+
const commit = commitPaths(root, paths, title, body, { pathScoped: true })
|
|
220
|
+
if (!commit) continue
|
|
221
|
+
|
|
222
|
+
logEvent('success', {
|
|
223
|
+
harness: hookInput.harness,
|
|
224
|
+
project: path.basename(root),
|
|
225
|
+
branch: currentBranch(root),
|
|
226
|
+
title,
|
|
227
|
+
recovered: paths.length
|
|
228
|
+
})
|
|
229
|
+
if (config.push === true) shouldPush = true
|
|
230
|
+
resolveBashOverlap(root, overlap.eventIds)
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
logEvent('fail', {
|
|
234
|
+
harness: hookInput.harness,
|
|
235
|
+
project: path.basename(root),
|
|
236
|
+
branch: currentBranch(root),
|
|
237
|
+
reason: 'bash-overlap-recovery'
|
|
238
|
+
})
|
|
239
|
+
} finally {
|
|
240
|
+
try { compactBashOverlapEvents(root) } catch {}
|
|
241
|
+
release()
|
|
242
|
+
}
|
|
243
|
+
if (shouldPush) {
|
|
244
|
+
logEvent(pushClean(root) ? 'push' : 'push-fail', {
|
|
245
|
+
harness: hookInput.harness,
|
|
246
|
+
project: path.basename(root),
|
|
247
|
+
branch: currentBranch(root)
|
|
248
|
+
})
|
|
122
249
|
}
|
|
123
|
-
return runSingle(hookInput)
|
|
124
250
|
}
|
|
125
251
|
|
|
126
252
|
function runSingle (input, opts = {}) {
|
|
@@ -153,6 +279,7 @@ function runSingle (input, opts = {}) {
|
|
|
153
279
|
try { context = fs.statSync(transcriptPath).size } catch {}
|
|
154
280
|
|
|
155
281
|
const sessionId = hookInput.sessionId
|
|
282
|
+
const ownedPaths = opts.paths || []
|
|
156
283
|
|
|
157
284
|
// Watermark slicing: only include new pairs since last commit in this session
|
|
158
285
|
const watermark = sessionId ? readWatermark(root, sessionId) : null
|
|
@@ -172,21 +299,39 @@ function runSingle (input, opts = {}) {
|
|
|
172
299
|
if (!(precompactWatermark && newPairs.length === 0 && selfPrecompactPending.length > 0)) {
|
|
173
300
|
savePending(root, sessionId, formatBody(effectivePairs))
|
|
174
301
|
}
|
|
175
|
-
|
|
302
|
+
cleanupTracking(root, sessionId)
|
|
303
|
+
logEvent('skip', { harness: hookInput.harness, project, branch, context, reason: 'no-tracking' })
|
|
304
|
+
cleanupStale(root)
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (sessionId && (opts.trackingReason || ownedPaths.length === 0)) {
|
|
309
|
+
if (!(precompactWatermark && newPairs.length === 0 && selfPrecompactPending.length > 0)) {
|
|
310
|
+
savePending(root, sessionId, formatBody(effectivePairs))
|
|
311
|
+
}
|
|
312
|
+
cleanupTracking(root, sessionId)
|
|
313
|
+
logEvent('skip', {
|
|
314
|
+
harness: hookInput.harness,
|
|
315
|
+
project,
|
|
316
|
+
branch,
|
|
317
|
+
context,
|
|
318
|
+
reason: opts.trackingReason || 'no-path-changes'
|
|
319
|
+
})
|
|
176
320
|
cleanupStale(root)
|
|
177
321
|
return
|
|
178
322
|
}
|
|
179
323
|
|
|
180
324
|
try {
|
|
181
325
|
// Early exit: tracking fired but all changes were reverted
|
|
182
|
-
|
|
326
|
+
const hasOwnedChanges = sessionId ? changedPaths(root, ownedPaths).length > 0 : hasChanges(root)
|
|
327
|
+
if (!hasOwnedChanges) {
|
|
183
328
|
if (sessionId) {
|
|
184
329
|
if (!(precompactWatermark && newPairs.length === 0 && selfPrecompactPending.length > 0)) {
|
|
185
330
|
savePending(root, sessionId, formatBody(effectivePairs))
|
|
186
331
|
}
|
|
187
332
|
cleanupTracking(root, sessionId)
|
|
188
333
|
}
|
|
189
|
-
logEvent('skip', { harness: hookInput.harness, project, branch, context })
|
|
334
|
+
logEvent('skip', { harness: hookInput.harness, project, branch, context, reason: 'no-path-changes' })
|
|
190
335
|
cleanupStale(root)
|
|
191
336
|
return
|
|
192
337
|
}
|
|
@@ -199,14 +344,14 @@ function runSingle (input, opts = {}) {
|
|
|
199
344
|
let headline
|
|
200
345
|
if (config.title?.type !== 'transcript') {
|
|
201
346
|
const titleTranscript = formatTitleTranscript(effectivePairs)
|
|
202
|
-
headline = runTitleAgent(root, config.title || {}, titleTranscript, hookInput.harness)
|
|
347
|
+
headline = runTitleAgent(root, config.title || {}, titleTranscript, hookInput.harness, { deadline: opts.operationDeadline })
|
|
203
348
|
}
|
|
204
349
|
headline = headline || extractHeadline(effectivePairs)
|
|
205
350
|
|
|
206
351
|
// Body: transcript by default, agent if opted in
|
|
207
352
|
let body
|
|
208
353
|
if (config.body?.type === 'agent') {
|
|
209
|
-
body = runBodyAgent(root, config.body, formattedTranscript, hookInput.harness)
|
|
354
|
+
body = runBodyAgent(root, config.body, formattedTranscript, hookInput.harness, { deadline: opts.operationDeadline })
|
|
210
355
|
}
|
|
211
356
|
body = body || formattedTranscript
|
|
212
357
|
|
|
@@ -247,11 +392,25 @@ function runSingle (input, opts = {}) {
|
|
|
247
392
|
const redactions = buildRedactions()
|
|
248
393
|
const safeHeadline = redact(headline, redactions)
|
|
249
394
|
const safeBody = redact(wrappedBody + tag, redactions)
|
|
250
|
-
const sha =
|
|
395
|
+
const sha = sessionId
|
|
396
|
+
? commitPaths(root, ownedPaths, safeHeadline, safeBody)
|
|
397
|
+
: addAndCommit(root, safeHeadline, safeBody)
|
|
398
|
+
if (!sha) {
|
|
399
|
+
if (sessionId) cleanupTracking(root, sessionId)
|
|
400
|
+
logEvent('skip', {
|
|
401
|
+
harness: hookInput.harness,
|
|
402
|
+
project,
|
|
403
|
+
branch,
|
|
404
|
+
context,
|
|
405
|
+
reason: 'no-path-changes'
|
|
406
|
+
})
|
|
407
|
+
cleanupStale(root)
|
|
408
|
+
return
|
|
409
|
+
}
|
|
251
410
|
|
|
252
411
|
logEvent('success', { harness: hookInput.harness, project, branch, context, title: safeHeadline })
|
|
253
412
|
|
|
254
|
-
if (config.push === true) {
|
|
413
|
+
if (config.push === true && !opts.deferPush) {
|
|
255
414
|
if (pushClean(root)) {
|
|
256
415
|
logEvent('push', { harness: hookInput.harness, project, branch })
|
|
257
416
|
} else {
|
|
@@ -273,7 +432,7 @@ function runSingle (input, opts = {}) {
|
|
|
273
432
|
}
|
|
274
433
|
}
|
|
275
434
|
|
|
276
|
-
function runMulti (hookInput, anchor,
|
|
435
|
+
function runMulti (hookInput, anchor, currentRepos, manifest, opts = {}) {
|
|
277
436
|
recordCodexStop(hookInput, anchor)
|
|
278
437
|
|
|
279
438
|
const transcriptPath = resolveTranscriptPath(hookInput)
|
|
@@ -305,12 +464,12 @@ function runMulti (hookInput, anchor, currentRoots, manifest) {
|
|
|
305
464
|
'\n\n## Implementation\n\n' + formattedTranscript
|
|
306
465
|
: formattedTranscript
|
|
307
466
|
|
|
308
|
-
const
|
|
309
|
-
const isMultiTurn =
|
|
467
|
+
const dirtyRepos = currentRepos.filter(repo => changedPaths(repo.root, repo.paths).length > 0)
|
|
468
|
+
const isMultiTurn = currentRepos.length > 1
|
|
310
469
|
const turnKey = hookInput.raw?.turn_id ||
|
|
311
470
|
cryptoKey(`${sessionId || 'no-session'}\n${pairs.length}\n${turnBody}`)
|
|
312
471
|
|
|
313
|
-
if (
|
|
472
|
+
if (dirtyRepos.length === 0 && manifest.repos.length === 0) {
|
|
314
473
|
if (sessionId) {
|
|
315
474
|
savePending(anchor, sessionId, formattedTranscript)
|
|
316
475
|
cleanupTracking(anchor, sessionId)
|
|
@@ -319,16 +478,18 @@ function runMulti (hookInput, anchor, currentRoots, manifest) {
|
|
|
319
478
|
harness: hookInput.harness,
|
|
320
479
|
project: path.basename(anchor),
|
|
321
480
|
branch: currentBranch(anchor),
|
|
322
|
-
context: transcriptSize(transcriptPath)
|
|
481
|
+
context: transcriptSize(transcriptPath),
|
|
482
|
+
reason: 'no-path-changes'
|
|
323
483
|
})
|
|
324
484
|
return
|
|
325
485
|
}
|
|
326
486
|
|
|
327
|
-
for (const
|
|
328
|
-
appendWork(manifest, root, {
|
|
487
|
+
for (const repo of currentRepos) {
|
|
488
|
+
appendWork(manifest, repo.root, {
|
|
329
489
|
key: turnKey,
|
|
330
490
|
body: turnBody,
|
|
331
|
-
sessionIds: isMultiTurn && sessionId ? [sessionId] : []
|
|
491
|
+
sessionIds: isMultiTurn && sessionId ? [sessionId] : [],
|
|
492
|
+
paths: repo.paths
|
|
332
493
|
})
|
|
333
494
|
}
|
|
334
495
|
|
|
@@ -342,36 +503,53 @@ function runMulti (hookInput, anchor, currentRoots, manifest) {
|
|
|
342
503
|
const project = path.basename(root)
|
|
343
504
|
const branch = currentBranch(root)
|
|
344
505
|
const context = transcriptSize(transcriptPath)
|
|
506
|
+
const ownedPaths = [...new Set(repo.work.flatMap(item => item.paths || []))]
|
|
345
507
|
|
|
346
508
|
if (config.enabled !== true) {
|
|
347
509
|
manifest.repos = manifest.repos.filter(candidate => candidate !== repo)
|
|
348
510
|
logEvent('skip', { harness: hookInput.harness, project, branch, context })
|
|
349
511
|
continue
|
|
350
512
|
}
|
|
351
|
-
if (
|
|
513
|
+
if (ownedPaths.length === 0) {
|
|
514
|
+
logEvent('skip', {
|
|
515
|
+
harness: hookInput.harness,
|
|
516
|
+
project,
|
|
517
|
+
branch,
|
|
518
|
+
context,
|
|
519
|
+
reason: 'missing-manifest-paths'
|
|
520
|
+
})
|
|
521
|
+
continue
|
|
522
|
+
}
|
|
523
|
+
const currentPaths = changedPaths(root, ownedPaths)
|
|
524
|
+
if (currentPaths.length === 0) {
|
|
352
525
|
manifest.repos = manifest.repos.filter(candidate => candidate !== repo)
|
|
353
|
-
logEvent('skip', { harness: hookInput.harness, project, branch, context })
|
|
526
|
+
logEvent('skip', { harness: hookInput.harness, project, branch, context, reason: 'no-path-changes' })
|
|
354
527
|
continue
|
|
355
528
|
}
|
|
356
529
|
|
|
357
530
|
try {
|
|
358
531
|
logEvent('start', { harness: hookInput.harness, project, branch, context })
|
|
359
|
-
|
|
532
|
+
const stagedPaths = stagePaths(root, currentPaths)
|
|
533
|
+
if (stagedPaths.length === 0) {
|
|
534
|
+
manifest.repos = manifest.repos.filter(candidate => candidate !== repo)
|
|
535
|
+
logEvent('skip', { harness: hookInput.harness, project, branch, context, reason: 'no-path-changes' })
|
|
536
|
+
continue
|
|
537
|
+
}
|
|
360
538
|
|
|
361
539
|
const rawBody = repo.work.map(item => item.body).join('\n\n---\n\n')
|
|
362
540
|
const redactions = buildRedactions()
|
|
363
|
-
const changeContext = redact(stagedChangeContext(root, 10000), redactions)
|
|
541
|
+
const changeContext = redact(stagedChangeContext(root, 10000, stagedPaths), redactions)
|
|
364
542
|
const titleInput = `${changeContext}\n\nTranscript:\n${rawBody}`.slice(0, 20000)
|
|
365
543
|
|
|
366
544
|
let headline
|
|
367
545
|
if (config.title?.type !== 'transcript') {
|
|
368
|
-
headline = runTitleAgent(root, config.title || {}, titleInput, hookInput.harness)
|
|
546
|
+
headline = runTitleAgent(root, config.title || {}, titleInput, hookInput.harness, { deadline: opts.operationDeadline })
|
|
369
547
|
}
|
|
370
548
|
headline = headline || extractHeadline(effectivePairs)
|
|
371
549
|
|
|
372
550
|
let body
|
|
373
551
|
if (config.body?.type === 'agent') {
|
|
374
|
-
body = runBodyAgent(root, config.body, rawBody, hookInput.harness)
|
|
552
|
+
body = runBodyAgent(root, config.body, rawBody, hookInput.harness, { deadline: opts.operationDeadline })
|
|
375
553
|
}
|
|
376
554
|
body = body || rawBody
|
|
377
555
|
|
|
@@ -399,7 +577,8 @@ function runMulti (hookInput, anchor, currentRoots, manifest) {
|
|
|
399
577
|
|
|
400
578
|
const safeHeadline = redact(headline, redactions)
|
|
401
579
|
const safeBody = redact(body, redactions)
|
|
402
|
-
const sha =
|
|
580
|
+
const sha = commitStagedPaths(root, stagedPaths, safeHeadline, safeBody)
|
|
581
|
+
if (!sha) throw new Error('Tracked paths changed before commit')
|
|
403
582
|
if (sessionId) saveMultiWatermark(root, sessionId, sha)
|
|
404
583
|
if (root === anchor && sessionId) {
|
|
405
584
|
saveWatermark(anchor, sessionId, pairs.length, sha, { source: 'commit' })
|
|
@@ -420,6 +599,7 @@ function runMulti (hookInput, anchor, currentRoots, manifest) {
|
|
|
420
599
|
|
|
421
600
|
for (const success of successes) {
|
|
422
601
|
if (success.config.push !== true) continue
|
|
602
|
+
if (opts.deferPush && success.root === anchor) continue
|
|
423
603
|
if (pushClean(success.root)) {
|
|
424
604
|
logEvent('push', {
|
|
425
605
|
harness: hookInput.harness,
|
|
@@ -500,4 +680,4 @@ function recordCodexStop (hookInput, root) {
|
|
|
500
680
|
if (hookInput.harness === 'codex' && hookInput.event === 'Stop') handleSessionEnd(hookInput, root)
|
|
501
681
|
}
|
|
502
682
|
|
|
503
|
-
module.exports = { run, runPreCompact, formatModelName, resolveCoauthor, readClaudeAttribution, resolveTranscriptPath, recordCodexStop }
|
|
683
|
+
module.exports = { run, runPreCompact, recoverBashOverlaps, formatModelName, resolveCoauthor, readClaudeAttribution, resolveTranscriptPath, recordCodexStop }
|
package/lib/session.js
CHANGED
|
@@ -257,7 +257,7 @@ function cleanupStale (root, maxAgeMs) {
|
|
|
257
257
|
const base = turbocommitDir(root)
|
|
258
258
|
if (!base) return
|
|
259
259
|
|
|
260
|
-
for (const sub of ['breadcrumbs', 'chains', '
|
|
260
|
+
for (const sub of ['breadcrumbs', 'chains', 'watermarks']) {
|
|
261
261
|
const dir = path.join(base, sub)
|
|
262
262
|
let files
|
|
263
263
|
try {
|
|
@@ -276,6 +276,8 @@ function cleanupStale (root, maxAgeMs) {
|
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
cleanupStaleTracking(root, now, ttl)
|
|
280
|
+
|
|
279
281
|
// Clean stale pending directories
|
|
280
282
|
const pDir = path.join(base, 'pending')
|
|
281
283
|
let pdirs
|
|
@@ -300,6 +302,43 @@ function cleanupStale (root, maxAgeMs) {
|
|
|
300
302
|
}
|
|
301
303
|
}
|
|
302
304
|
|
|
305
|
+
function cleanupStaleTracking (root, now, ttl) {
|
|
306
|
+
const dir = path.join(turbocommitDir(root), 'tracking')
|
|
307
|
+
let files
|
|
308
|
+
try {
|
|
309
|
+
files = fs.readdirSync(dir)
|
|
310
|
+
} catch {
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
for (const file of files) {
|
|
314
|
+
const fullPath = path.join(dir, file)
|
|
315
|
+
try {
|
|
316
|
+
const stat = fs.statSync(fullPath)
|
|
317
|
+
if (now - stat.mtimeMs <= ttl || trackingMayDescribeModifications(fullPath)) continue
|
|
318
|
+
fs.unlinkSync(fullPath)
|
|
319
|
+
} catch {}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function trackingMayDescribeModifications (file) {
|
|
324
|
+
let lines
|
|
325
|
+
try {
|
|
326
|
+
const data = fs.readFileSync(file, 'utf8').trim()
|
|
327
|
+
lines = data ? data.split('\n') : []
|
|
328
|
+
} catch {
|
|
329
|
+
return true
|
|
330
|
+
}
|
|
331
|
+
return lines.some(line => {
|
|
332
|
+
try {
|
|
333
|
+
const entry = JSON.parse(line)
|
|
334
|
+
return entry.tool !== 'Bash' ||
|
|
335
|
+
(entry.phase === 'post' && Array.isArray(entry.files) && entry.files.length > 0)
|
|
336
|
+
} catch {
|
|
337
|
+
return true
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
303
342
|
module.exports = {
|
|
304
343
|
handleSessionEnd,
|
|
305
344
|
handleSessionStart,
|