@searls/turbocommit 0.16.1 → 0.16.2
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 +8 -2
- package/lib/git.js +38 -3
- package/lib/run.js +36 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,8 +80,14 @@ Each touched checkout:
|
|
|
80
80
|
- Gives the configured title agent repository-specific bounded diff context.
|
|
81
81
|
|
|
82
82
|
Turbocommit attempts every commit before pushing every successful commit whose
|
|
83
|
-
repository has `push: true`.
|
|
84
|
-
|
|
83
|
+
repository has `push: true`. Staging and committing are retried a few times
|
|
84
|
+
when Git is momentarily unavailable, for example while Xcode holds the index
|
|
85
|
+
lock. Failures are retained and retried on a later Stop in that checkout by
|
|
86
|
+
any session, along with every transcript accumulated before the commit
|
|
87
|
+
succeeds, and the failed session's claims are released so they do not block
|
|
88
|
+
other sessions meanwhile. Each `fail` monitor event records Git's error. A
|
|
89
|
+
deletion the session already staged with `git rm` is committed with the rest
|
|
90
|
+
of its paths.
|
|
85
91
|
|
|
86
92
|
Separate worktrees are committed independently. Enabled submodules are
|
|
87
93
|
committed and pushed before enabled parent repositories. When one turn creates
|
package/lib/git.js
CHANGED
|
@@ -115,12 +115,46 @@ function stageAll (cwd) {
|
|
|
115
115
|
|
|
116
116
|
function stagePaths (cwd, filePaths) {
|
|
117
117
|
const changed = changedPaths(cwd, filePaths)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
git
|
|
118
|
+
if (changed.length === 0) return []
|
|
119
|
+
// A deletion the session already staged (git rm) matches nothing on disk or
|
|
120
|
+
// in the index, and `git add` aborts on such a pathspec. It is already
|
|
121
|
+
// staged, so only the remaining paths are added.
|
|
122
|
+
const paths = pathspecs(cwd, changed.filter(file => existsOnDisk(file) || isIndexed(cwd, file)))
|
|
123
|
+
if (paths.length > 0) git(`add -A -- ${paths.map(quote).join(' ')}`, { cwd })
|
|
121
124
|
return changed
|
|
122
125
|
}
|
|
123
126
|
|
|
127
|
+
function existsOnDisk (file) {
|
|
128
|
+
try {
|
|
129
|
+
fs.lstatSync(file)
|
|
130
|
+
return true
|
|
131
|
+
} catch {
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isIndexed (cwd, file) {
|
|
137
|
+
const spec = pathspecs(cwd, [file])
|
|
138
|
+
if (spec.length === 0) return false
|
|
139
|
+
return git(`ls-files --cached -z -- ${quote(spec[0])}`, { cwd }) !== ''
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Git refuses to work while another process holds the index (Xcode and other
|
|
144
|
+
* tools take `.git/index.lock` briefly), so a staging or commit step is tried
|
|
145
|
+
* again before its failure is allowed to strand a turn's work.
|
|
146
|
+
*/
|
|
147
|
+
function withGitRetry (operation, attempts = 3, delayMs = 250) {
|
|
148
|
+
for (let attempt = 1; ; attempt++) {
|
|
149
|
+
try {
|
|
150
|
+
return operation()
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (attempt >= attempts) throw err
|
|
153
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
124
158
|
function commitPaths (cwd, filePaths, headline, body, opts = {}) {
|
|
125
159
|
if (opts.pathScoped && hasRepositoryOperation(cwd)) return null
|
|
126
160
|
const changed = stagePaths(cwd, filePaths)
|
|
@@ -271,6 +305,7 @@ function hasRepositoryOperation (cwd) {
|
|
|
271
305
|
}
|
|
272
306
|
|
|
273
307
|
module.exports = {
|
|
308
|
+
withGitRetry,
|
|
274
309
|
git,
|
|
275
310
|
canonicalRoot,
|
|
276
311
|
canonicalTrackedPath,
|
package/lib/run.js
CHANGED
|
@@ -4,7 +4,7 @@ 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, changedPaths, addAndCommit, stagePaths, commitPaths, commitStagedPaths, stagedChangeContext, currentBranch, pushClean, hasRepositoryOperation } = require('./git')
|
|
7
|
+
const { withGitRetry, 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
10
|
const {
|
|
@@ -281,6 +281,8 @@ function runSingle (input, opts = {}) {
|
|
|
281
281
|
const project = path.basename(root)
|
|
282
282
|
const branch = currentBranch(root)
|
|
283
283
|
let context = 0
|
|
284
|
+
// Work a failed commit could not land is retained for a later Stop.
|
|
285
|
+
let retained = null
|
|
284
286
|
try { context = fs.statSync(transcriptPath).size } catch {}
|
|
285
287
|
|
|
286
288
|
const sessionId = hookInput.sessionId
|
|
@@ -344,6 +346,7 @@ function runSingle (input, opts = {}) {
|
|
|
344
346
|
logEvent('start', { harness: hookInput.harness, project, branch, context })
|
|
345
347
|
|
|
346
348
|
const formattedTranscript = formatBody(effectivePairs)
|
|
349
|
+
if (sessionId) retained = { body: formattedTranscript, paths: ownedPaths }
|
|
347
350
|
|
|
348
351
|
// Title: agent by default, transcript if opted out
|
|
349
352
|
let headline
|
|
@@ -397,9 +400,9 @@ function runSingle (input, opts = {}) {
|
|
|
397
400
|
const redactions = buildRedactions()
|
|
398
401
|
const safeHeadline = redact(headline, redactions)
|
|
399
402
|
const safeBody = redact(wrappedBody + tag, redactions)
|
|
400
|
-
const sha = sessionId
|
|
403
|
+
const sha = withGitRetry(() => sessionId
|
|
401
404
|
? commitPaths(root, ownedPaths, safeHeadline, safeBody)
|
|
402
|
-
: addAndCommit(root, safeHeadline, safeBody)
|
|
405
|
+
: addAndCommit(root, safeHeadline, safeBody))
|
|
403
406
|
if (!sha) {
|
|
404
407
|
if (sessionId) cleanupTracking(root, sessionId)
|
|
405
408
|
logEvent('skip', {
|
|
@@ -432,7 +435,8 @@ function runSingle (input, opts = {}) {
|
|
|
432
435
|
}
|
|
433
436
|
cleanupStale(root)
|
|
434
437
|
} catch (err) {
|
|
435
|
-
logEvent('fail', { harness: hookInput.harness, project, branch, context })
|
|
438
|
+
logEvent('fail', { harness: hookInput.harness, project, branch, context, error: failureMessage(err) })
|
|
439
|
+
if (retained) retainFailedWork(root, sessionId, retained, pairs.length)
|
|
436
440
|
throw err
|
|
437
441
|
}
|
|
438
442
|
}
|
|
@@ -534,7 +538,7 @@ function runMulti (hookInput, anchor, currentRepos, manifest, opts = {}) {
|
|
|
534
538
|
|
|
535
539
|
try {
|
|
536
540
|
logEvent('start', { harness: hookInput.harness, project, branch, context })
|
|
537
|
-
const stagedPaths = stagePaths(root, currentPaths)
|
|
541
|
+
const stagedPaths = withGitRetry(() => stagePaths(root, currentPaths))
|
|
538
542
|
if (stagedPaths.length === 0) {
|
|
539
543
|
manifest.repos = manifest.repos.filter(candidate => candidate !== repo)
|
|
540
544
|
logEvent('skip', { harness: hookInput.harness, project, branch, context, reason: 'no-path-changes' })
|
|
@@ -582,7 +586,7 @@ function runMulti (hookInput, anchor, currentRepos, manifest, opts = {}) {
|
|
|
582
586
|
|
|
583
587
|
const safeHeadline = redact(headline, redactions)
|
|
584
588
|
const safeBody = redact(body, redactions)
|
|
585
|
-
const sha = commitStagedPaths(root, stagedPaths, safeHeadline, safeBody)
|
|
589
|
+
const sha = withGitRetry(() => commitStagedPaths(root, stagedPaths, safeHeadline, safeBody))
|
|
586
590
|
if (!sha) throw new Error('Tracked paths changed before commit')
|
|
587
591
|
if (sessionId) saveMultiWatermark(root, sessionId, sha)
|
|
588
592
|
if (root === anchor && sessionId) {
|
|
@@ -597,8 +601,8 @@ function runMulti (hookInput, anchor, currentRepos, manifest, opts = {}) {
|
|
|
597
601
|
context,
|
|
598
602
|
title: safeHeadline
|
|
599
603
|
})
|
|
600
|
-
} catch {
|
|
601
|
-
logEvent('fail', { harness: hookInput.harness, project, branch, context })
|
|
604
|
+
} catch (err) {
|
|
605
|
+
logEvent('fail', { harness: hookInput.harness, project, branch, context, error: failureMessage(err) })
|
|
602
606
|
}
|
|
603
607
|
}
|
|
604
608
|
|
|
@@ -631,6 +635,30 @@ function runMulti (hookInput, anchor, currentRepos, manifest, opts = {}) {
|
|
|
631
635
|
cleanupStale(anchor)
|
|
632
636
|
}
|
|
633
637
|
|
|
638
|
+
/**
|
|
639
|
+
* A single-repository commit that failed joins the retry manifest, which any
|
|
640
|
+
* later Stop in this checkout replays. The session's tracking file is
|
|
641
|
+
* released so its claims stop blocking other sessions in the meantime.
|
|
642
|
+
*/
|
|
643
|
+
function retainFailedWork (root, sessionId, retained, pairCount) {
|
|
644
|
+
try {
|
|
645
|
+
const manifest = loadManifest(root)
|
|
646
|
+
appendWork(manifest, root, {
|
|
647
|
+
key: cryptoKey(`${sessionId}\n${pairCount}\n${retained.body}`),
|
|
648
|
+
body: retained.body,
|
|
649
|
+
sessionIds: [],
|
|
650
|
+
paths: retained.paths
|
|
651
|
+
})
|
|
652
|
+
saveManifest(root, manifest)
|
|
653
|
+
cleanupTracking(root, sessionId)
|
|
654
|
+
} catch {}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function failureMessage (err) {
|
|
658
|
+
const text = err?.stderr?.toString?.().trim() || err?.message || String(err)
|
|
659
|
+
return text.split('\n')[0].slice(0, 200)
|
|
660
|
+
}
|
|
661
|
+
|
|
634
662
|
function transcriptSize (transcriptPath) {
|
|
635
663
|
try {
|
|
636
664
|
return fs.statSync(transcriptPath).size
|