@link-assistant/hive-mind 2.19.1 → 2.19.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/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/src/pull-request-changes.lib.mjs +147 -18
- package/src/solve.auto-merge-guards.lib.mjs +28 -0
- package/src/solve.auto-merge.lib.mjs +13 -1
- package/src/solve.mjs +14 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.19.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 57b8258: Never merge hive-mind's own placeholder file, and restart when a pull request changes nothing.
|
|
8
|
+
|
|
9
|
+
`konard/audio-decomposer#1` was solved twice, both pull requests were auto-merged, and the complete diff of both was the `.gitkeep` hive-mind writes only so that an empty branch has something to open a pull request from. Two defects had to line up for that, and both are fixed here.
|
|
10
|
+
|
|
11
|
+
- **The placeholder is reverted before anything can merge it.** `cleanupClaudeFile()` ran after `startAutoRestartUntilMergeable()`, so with `--auto-merge` it was structurally guaranteed to be too late: on that pull request the revert commit is timestamped four seconds after the merge commit. It now runs before the watch loop and still after `verifyResults()`, which is the ordering issue #1516 actually asked for. As defense in depth, the watch loop reverts a placeholder that survived into its own diff — a crashed session, a resumed run — instead of merging it.
|
|
12
|
+
- **An appended placeholder is recognised as a placeholder.** The empty-pull-request detector matched an added `# .gitkeep file auto-generated at …` line, which the solver only writes when it _creates_ the file; when the file already exists it appends `# Updated: <timestamp>` instead, and that reads as an ordinary modification. The diff was counted as real work, so the pull request looked mergeable and the auto-restart from issue #2119 never fired. The measurement now reconstructs both sides of the file and compares them with hive-mind's own generated lines removed, so created, appended and re-appended placeholders are all caught — while a genuine edit to a `.gitkeep` or `CLAUDE.md` the repository owns still counts as work.
|
|
13
|
+
|
|
14
|
+
The leak was not a one-off: `.gitkeep` on the default branch of `link-foundation/rust-ai-driven-development-pipeline-template` had accumulated eight solver-generated lines from eight merged pull requests, one of which also carried real changes. Each miss makes the next one certain, because a surviving file forces the append path that the detector could not see. The full reconstruction is in `docs/case-studies/issue-2211`.
|
|
15
|
+
|
|
3
16
|
## 2.19.1
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -48,20 +48,140 @@ import { quietProbe } from './quiet-probe.lib.mjs';
|
|
|
48
48
|
const LARGE_DIFF_WARNING_BYTES = 8 * 1024 * 1024;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* The solver's own scaffolding files
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* `.gitkeep`
|
|
51
|
+
* The solver's own scaffolding files (`src/solve.auto-pr.lib.mjs`). A pull
|
|
52
|
+
* request whose whole diff is one of these contains no solution: the
|
|
53
|
+
* placeholder exists only to give an empty branch something to open a pull
|
|
54
|
+
* request from, and is reverted once the AI commits real work.
|
|
55
|
+
*
|
|
56
|
+
* Issue #2211: asking "did the diff *add* the auto-generated header line?" only
|
|
57
|
+
* recognised the case where the solver created the file. When the repository
|
|
58
|
+
* already tracks a `.gitkeep` - the normal state of every repository generated
|
|
59
|
+
* from a template whose own solver run leaked one - `solve.auto-pr.lib.mjs`
|
|
60
|
+
* appends `# Updated: <timestamp>` instead, so the header is a context line, no
|
|
61
|
+
* pattern matched, and a pull request whose entire diff was that one timestamp
|
|
62
|
+
* measured as one changed file and was auto-merged:
|
|
63
|
+
*
|
|
64
|
+
* https://github.com/konard/audio-decomposer/pull/3
|
|
65
|
+
* .gitkeep | 2 +-
|
|
66
|
+
*
|
|
67
|
+
* The question asked here is therefore the one that actually decides it: once
|
|
68
|
+
* the solver's own generated lines are removed from both sides of the diff, is
|
|
69
|
+
* the file unchanged? That answers "created", "appended to" and "appended to
|
|
70
|
+
* again" with one rule, and it still counts a repository's own `.gitkeep` or
|
|
71
|
+
* `CLAUDE.md` edits as the real changes they are - a change to any line the
|
|
72
|
+
* solver did not write makes the two sides differ.
|
|
59
73
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
|
|
75
|
+
/** Lines `solve.auto-pr.lib.mjs` writes into `.gitkeep`. */
|
|
76
|
+
const GITKEEP_GENERATED_LINE_PATTERNS = [/^#\s*\.gitkeep file auto-generated at \S+ for PR creation at branch \S+ for issue \S+\s*$/, /^#\s*Updated: \d{4}-\d{2}-\d{2}T[\d:.]+Z?\s*$/];
|
|
77
|
+
|
|
78
|
+
/** The line that opens the task block `solve.auto-pr.lib.mjs` writes into `CLAUDE.md`. */
|
|
79
|
+
const CLAUDE_MD_TASK_BLOCK_HEAD = /^Issue to solve: \S+\s*$/;
|
|
80
|
+
|
|
81
|
+
/** Lines of that task block, including the ones only `--fork` runs emit. */
|
|
82
|
+
const CLAUDE_MD_GENERATED_LINE_PATTERNS = [CLAUDE_MD_TASK_BLOCK_HEAD, /^Your prepared branch: \S+\s*$/, /^Your prepared working directory: \S+\s*$/, /^Your forked repository: \S+\s*$/, /^Original repository \(upstream\): \S+\s*$/, /^Proceed\.\s*$/, /^Run timestamp: \d{4}-\d{2}-\d{2}T[\d:.]+Z?\s*$/];
|
|
83
|
+
|
|
84
|
+
const PLACEHOLDER_GENERATED_LINES = new Map([
|
|
85
|
+
['.gitkeep', GITKEEP_GENERATED_LINE_PATTERNS],
|
|
86
|
+
['CLAUDE.md', CLAUDE_MD_GENERATED_LINE_PATTERNS],
|
|
63
87
|
]);
|
|
64
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Rebuild both sides of one file's unified-diff section.
|
|
91
|
+
*
|
|
92
|
+
* Only the hunks are read: everything before the first `@@` is git's own header
|
|
93
|
+
* (`index`, `new file mode`, `--- a/x`, `+++ b/x`) and belongs to neither side,
|
|
94
|
+
* and `` is a note about the previous line, not a
|
|
95
|
+
* line of the file.
|
|
96
|
+
*
|
|
97
|
+
* Context outside the hunks is missing from both sides equally, which is all the
|
|
98
|
+
* caller needs: it compares the two reconstructions against each other.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} body - the section text, hunk headers included.
|
|
101
|
+
* @returns {{oldLines: string[], newLines: string[]}}
|
|
102
|
+
*/
|
|
103
|
+
const reconstructSides = body => {
|
|
104
|
+
const oldLines = [];
|
|
105
|
+
const newLines = [];
|
|
106
|
+
let inHunk = false;
|
|
107
|
+
for (const line of body.split('\n')) {
|
|
108
|
+
if (line.startsWith('@@')) {
|
|
109
|
+
inHunk = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!inHunk || line.startsWith('\\')) continue;
|
|
113
|
+
const text = line.slice(1);
|
|
114
|
+
if (line[0] === '-') oldLines.push(text);
|
|
115
|
+
else if (line[0] === '+') newLines.push(text);
|
|
116
|
+
else {
|
|
117
|
+
// ' ' is a context line; a completely empty line is a context line whose
|
|
118
|
+
// content is empty (git omits the trailing space on some diffs).
|
|
119
|
+
oldLines.push(text);
|
|
120
|
+
newLines.push(text);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { oldLines, newLines };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* True when the first non-blank line at or after `from` opens the CLAUDE.md
|
|
128
|
+
* task block, i.e. the preceding `---` is the separator the solver writes
|
|
129
|
+
* rather than a horizontal rule a human wrote.
|
|
130
|
+
*/
|
|
131
|
+
const opensClaudeTaskBlock = (lines, from) => {
|
|
132
|
+
for (let i = from; i < lines.length; i++) {
|
|
133
|
+
if (lines[i].trim() === '') continue;
|
|
134
|
+
return CLAUDE_MD_TASK_BLOCK_HEAD.test(lines[i]);
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Drop every line the solver generated, leaving whatever the repository owns.
|
|
141
|
+
*
|
|
142
|
+
* @returns {{kept: string[], removed: number}}
|
|
143
|
+
*/
|
|
144
|
+
const stripGeneratedLines = (path, lines) => {
|
|
145
|
+
const patterns = PLACEHOLDER_GENERATED_LINES.get(path) || [];
|
|
146
|
+
const kept = [];
|
|
147
|
+
let removed = 0;
|
|
148
|
+
for (let i = 0; i < lines.length; i++) {
|
|
149
|
+
const line = lines[i];
|
|
150
|
+
if (patterns.some(pattern => pattern.test(line))) {
|
|
151
|
+
removed += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// The CLAUDE.md append path writes "\n\n---\n\n" ahead of the task block;
|
|
155
|
+
// that separator is generated too, but only in that position.
|
|
156
|
+
if (path === 'CLAUDE.md' && /^-{3,}\s*$/.test(line) && opensClaudeTaskBlock(lines, i + 1)) {
|
|
157
|
+
removed += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
kept.push(line);
|
|
161
|
+
}
|
|
162
|
+
return { kept, removed };
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Is this section nothing but the solver's placeholder bookkeeping?
|
|
167
|
+
*
|
|
168
|
+
* @param {string|null} path - the file's path, or null when it is not a
|
|
169
|
+
* placeholder candidate.
|
|
170
|
+
* @param {string} body - the section text.
|
|
171
|
+
* @returns {boolean}
|
|
172
|
+
*/
|
|
173
|
+
const isPlaceholderSection = (path, body) => {
|
|
174
|
+
if (!PLACEHOLDER_GENERATED_LINES.has(path)) return false;
|
|
175
|
+
const { oldLines, newLines } = reconstructSides(body);
|
|
176
|
+
const before = stripGeneratedLines(path, oldLines);
|
|
177
|
+
const after = stripGeneratedLines(path, newLines);
|
|
178
|
+
// Nothing generated on either side means this diff is not the solver's doing.
|
|
179
|
+
if (before.removed === 0 && after.removed === 0) return false;
|
|
180
|
+
// Trailing blank lines are what the append path leaves behind; they are not a
|
|
181
|
+
// change anyone made.
|
|
182
|
+
return before.kept.join('\n').trimEnd() === after.kept.join('\n').trimEnd();
|
|
183
|
+
};
|
|
184
|
+
|
|
65
185
|
/**
|
|
66
186
|
* Measure a unified diff in a single pass.
|
|
67
187
|
*
|
|
@@ -96,8 +216,7 @@ const measureDiff = diff => {
|
|
|
96
216
|
|
|
97
217
|
const closeSection = () => {
|
|
98
218
|
if (!section) return;
|
|
99
|
-
|
|
100
|
-
if (isPlaceholder) placeholderSections += 1;
|
|
219
|
+
if (isPlaceholderSection(section.path, section.body)) placeholderSections += 1;
|
|
101
220
|
else {
|
|
102
221
|
filesChanged += 1;
|
|
103
222
|
additions += section.additions;
|
|
@@ -116,14 +235,13 @@ const measureDiff = diff => {
|
|
|
116
235
|
closeSection();
|
|
117
236
|
const match = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
|
|
118
237
|
const path = match ? match[2] : '';
|
|
119
|
-
|
|
120
|
-
section = { patterns, body: '', additions: 0, deletions: 0 };
|
|
238
|
+
section = { path, candidate: PLACEHOLDER_GENERATED_LINES.has(path), body: '', additions: 0, deletions: 0 };
|
|
121
239
|
continue;
|
|
122
240
|
}
|
|
123
241
|
if (!section) continue;
|
|
124
242
|
// Only a placeholder candidate needs its text kept; every other file is
|
|
125
243
|
// reduced to two counters as it streams past.
|
|
126
|
-
if (section.
|
|
244
|
+
if (section.candidate) section.body += `${line}\n`;
|
|
127
245
|
if (line.length > 1) {
|
|
128
246
|
if (line[0] === '+' && line[1] !== '+') section.additions += 1;
|
|
129
247
|
else if (line[0] === '-' && line[1] !== '-') section.deletions += 1;
|
|
@@ -147,7 +265,7 @@ const measureDiff = diff => {
|
|
|
147
265
|
* @param {number} params.prNumber
|
|
148
266
|
* @param {Function} params.$ command-stream tagged-template executor
|
|
149
267
|
* @param {Function} [params.log] - optional logger for the size diagnostic
|
|
150
|
-
* @returns {Promise<{hasChanges: boolean, filesChanged: number, additions: number, deletions: number, placeholderOnly: boolean, measured: boolean, diffBytes: number}>}
|
|
268
|
+
* @returns {Promise<{hasChanges: boolean, filesChanged: number, additions: number, deletions: number, placeholderOnly: boolean, placeholderSections: number, measured: boolean, diffBytes: number}>}
|
|
151
269
|
* The counts cover the AI's own work: the solver's placeholder file is
|
|
152
270
|
* excluded and reported through `placeholderOnly` instead. `measured` is
|
|
153
271
|
* false when the diff could not be fetched, in which case callers must not
|
|
@@ -187,6 +305,11 @@ export const getPullRequestChangeStats = async ({ owner, repo, prNumber, $, log
|
|
|
187
305
|
additions,
|
|
188
306
|
deletions,
|
|
189
307
|
placeholderOnly: filesChanged === 0 && placeholderSections > 0,
|
|
308
|
+
// Issue #2211: a pull request that has real changes *and* still carries the
|
|
309
|
+
// solver's placeholder is not empty, but it must not be merged with the
|
|
310
|
+
// placeholder in it either. Reported separately so the merge watcher can
|
|
311
|
+
// clean it up before merging instead of shipping it to the default branch.
|
|
312
|
+
placeholderSections,
|
|
190
313
|
measured,
|
|
191
314
|
diffBytes,
|
|
192
315
|
};
|
|
@@ -233,4 +356,10 @@ export const EMPTY_PULL_REQUEST_BLOCKER = 'The pull request contains no changes
|
|
|
233
356
|
*/
|
|
234
357
|
export const buildEmptyPullRequestBlocker = (stats = null) => (stats?.placeholderOnly ? 'The pull request contains only the placeholder file the solver commits to open a pull request, so there is nothing to merge' : EMPTY_PULL_REQUEST_BLOCKER);
|
|
235
358
|
|
|
236
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Exported for tests and for `experiments/issue-2211`: measuring a diff without
|
|
361
|
+
* a GitHub round trip is the only way to replay an archived pull request.
|
|
362
|
+
*/
|
|
363
|
+
export const __measureDiffForTests = measureDiff;
|
|
364
|
+
|
|
365
|
+
export default { getPullRequestChangeStats, formatChangeSummary, EMPTY_PULL_REQUEST_BLOCKER, buildEmptyPullRequestBlocker, __measureDiffForTests: measureDiff };
|
|
@@ -150,3 +150,31 @@ export default {
|
|
|
150
150
|
resolveDraftBlocker,
|
|
151
151
|
resolveMergeFailure,
|
|
152
152
|
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Issue #2211: revert a solver placeholder that survived into the pull request diff.
|
|
156
|
+
*
|
|
157
|
+
* `src/solve.mjs` reverts the placeholder before this loop starts, so reaching
|
|
158
|
+
* here means the session that created it crashed, was resumed from a different
|
|
159
|
+
* working directory, or was restarted inside the loop. Merging anyway publishes
|
|
160
|
+
* hive-mind's own scaffolding to the default branch, which is what happened to
|
|
161
|
+
* https://github.com/konard/audio-decomposer/pull/3 - and, eight times over, to
|
|
162
|
+
* the `.gitkeep` on the default branch of the template repository it was
|
|
163
|
+
* generated from. See docs/case-studies/issue-2211.
|
|
164
|
+
*
|
|
165
|
+
* Failure is not fatal: the caller re-measures the diff, and a placeholder that
|
|
166
|
+
* could not be reverted still keeps the pull request from looking mergeable.
|
|
167
|
+
*
|
|
168
|
+
* @returns {Promise<boolean>} whether a cleanup was attempted
|
|
169
|
+
*/
|
|
170
|
+
export const revertPlaceholderBeforeMerge = async ({ changeStats, tempDir, branchName, argv, log, formatAligned, cleanErrorMessage }) => {
|
|
171
|
+
if (!tempDir || !(changeStats?.placeholderSections > 0)) return false;
|
|
172
|
+
await log(formatAligned('🧹', 'Placeholder in diff:', 'reverting the solver placeholder file before it can be merged', 2), { level: 'warning' });
|
|
173
|
+
try {
|
|
174
|
+
const { cleanupClaudeFile } = await import('./solve.results.lib.mjs');
|
|
175
|
+
await cleanupClaudeFile(tempDir, branchName, null, argv);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
await log(formatAligned('⚠️', 'Placeholder cleanup failed:', cleanErrorMessage(error), 2), { level: 'warning' });
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
};
|
|
@@ -34,7 +34,7 @@ const { mergePullRequest, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDet
|
|
|
34
34
|
// Issue #2182: guard rails for this loop (wall-clock ceiling, draft self-heal,
|
|
35
35
|
// classified merge failures). See solve.auto-merge-guards.lib.mjs.
|
|
36
36
|
const autoMergeGuards = await import('./solve.auto-merge-guards.lib.mjs');
|
|
37
|
-
const { DRAFT_RECHECK_DELAY_MS, evaluateWatchTimeout, resolveDraftBlocker, resolveMergeFailure } = autoMergeGuards;
|
|
37
|
+
const { DRAFT_RECHECK_DELAY_MS, evaluateWatchTimeout, resolveDraftBlocker, resolveMergeFailure, revertPlaceholderBeforeMerge } = autoMergeGuards;
|
|
38
38
|
// Re-exported so callers and tests keep a single entry point for the watch loop.
|
|
39
39
|
export const { DEFAULT_WATCH_TIMEOUT_HOURS, normalizeWatchTimeoutHours } = autoMergeGuards;
|
|
40
40
|
// Import GitHub functions for log attachment
|
|
@@ -131,6 +131,8 @@ export const watchUntilMergeable = async params => {
|
|
|
131
131
|
// Issue #1503: Track consecutive "no workflow runs" checks per-SHA (reset on new push)
|
|
132
132
|
let consecutiveNoRunsChecks = 0;
|
|
133
133
|
let lastKnownHeadSha = null;
|
|
134
|
+
// Issue #2211: revert a leftover solver placeholder at most once per watch.
|
|
135
|
+
let placeholderCleanupAttempted = false;
|
|
134
136
|
// Issue #1567: Initial cooldown to let CI register and solution logs post
|
|
135
137
|
const INITIAL_COOLDOWN_SECONDS = MIN_CI_CHECK_INTERVAL_SECONDS;
|
|
136
138
|
// Issue #2182: this loop used to be `while (true)` with no wall-clock ceiling
|
|
@@ -330,6 +332,16 @@ export const watchUntilMergeable = async params => {
|
|
|
330
332
|
if (isEmptyPullRequest) {
|
|
331
333
|
await log(formatAligned('⚠️', 'PR is empty:', changeStats.placeholderOnly ? 'only the solver placeholder file is in the diff - not treating it as mergeable' : 'net diff contains no files - not treating it as mergeable', 2), { level: 'warning' });
|
|
332
334
|
}
|
|
335
|
+
// Issue #2211: defense in depth - never merge the solver's own placeholder.
|
|
336
|
+
if (changeStats.placeholderSections > 0 && !placeholderCleanupAttempted) {
|
|
337
|
+
placeholderCleanupAttempted = true;
|
|
338
|
+
if (await revertPlaceholderBeforeMerge({ changeStats, tempDir, branchName: prBranch || branchName, argv, log, formatAligned, cleanErrorMessage })) {
|
|
339
|
+
// Give the revert push a moment to register, then re-measure so the
|
|
340
|
+
// merge decision is made on the post-cleanup diff.
|
|
341
|
+
await interruptibleSleep(DRAFT_RECHECK_DELAY_MS);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
333
345
|
// If PR is mergeable, no blockers, no new comments, no issue metadata
|
|
334
346
|
// edits, no uncommitted changes and it actually changes something
|
|
335
347
|
if (blockers.length === 0 && !hasNewComments && !hasIssueMetadataChanges && !hasUncommittedChanges && !isEmptyPullRequest) {
|
package/src/solve.mjs
CHANGED
|
@@ -1197,6 +1197,20 @@ try {
|
|
|
1197
1197
|
logsAttached = true;
|
|
1198
1198
|
}
|
|
1199
1199
|
}
|
|
1200
|
+
// Issue #1516: Cleanup after all completion signals (it was before verifyResults, which
|
|
1201
|
+
// caused premature commits). Issue #2211: but strictly BEFORE the auto-merge watch loop.
|
|
1202
|
+
// It used to run after it, and `--auto-merge` therefore merged the placeholder into the
|
|
1203
|
+
// default branch and only then reverted it on a branch nobody would look at again:
|
|
1204
|
+
//
|
|
1205
|
+
// 19:20:07 Initial commit with task details (.gitkeep touched)
|
|
1206
|
+
// 19:28:09 Merge pull request #3 (.gitkeep leaked into main)
|
|
1207
|
+
// 19:28:13 Revert "Initial commit with task details" <- 4 seconds too late
|
|
1208
|
+
//
|
|
1209
|
+
// https://github.com/konard/audio-decomposer/pull/3, docs/case-studies/issue-2211.
|
|
1210
|
+
// Reverting first also lets the loop see the pull request as it really is: with the
|
|
1211
|
+
// placeholder gone, a pull request that implemented nothing has an empty diff and the
|
|
1212
|
+
// loop restarts the AI instead of merging an empty change.
|
|
1213
|
+
await cleanupClaudeFile(tempDir, branchName, claudeCommitHash, argv);
|
|
1200
1214
|
// Issue #2182: the AI working session is over at this point — everything below is
|
|
1201
1215
|
// monitoring and merging, not working. The pull request must therefore be back in
|
|
1202
1216
|
// "ready for review" BEFORE the auto-merge watch loop starts, because that loop can
|
|
@@ -1237,8 +1251,6 @@ try {
|
|
|
1237
1251
|
}
|
|
1238
1252
|
// Issue #1952: Final --attach-logs safety net + logsAttached reconciliation. See attach-logs-guarantee.lib.mjs.
|
|
1239
1253
|
logsAttached = (await attachFinalLogIfMissing({ shouldAttachLogs, prNumber, owner, repo, $, log, sanitizeLogContent, getLogFile, attachLogToGitHub, argv, sessionId, tempDir, anthropicTotalCostUSD, resultModelUsage })) || logsAttached;
|
|
1240
|
-
// Issue #1516: Cleanup after all signals (was before verifyResults, caused premature commits)
|
|
1241
|
-
await cleanupClaudeFile(tempDir, branchName, claudeCommitHash, argv);
|
|
1242
1254
|
await finalizeDevelopmentLog(); // Issue #1596/#2048: idempotent no-op on the success path (already committed before readiness signal); still preserves late/error work.
|
|
1243
1255
|
await endWorkSession({ isContinueMode, prNumber, argv, log, formatAligned, $, logsAttached });
|
|
1244
1256
|
} catch (error) {
|