@link-assistant/hive-mind 1.64.4 → 1.65.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/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/src/github-error-reporter.lib.mjs +1 -1
- package/src/solve.auto-merge.lib.mjs +12 -2
- package/src/solve.auto-pr.lib.mjs +16 -0
- package/src/solve.branch-divergence.lib.mjs +93 -0
- package/src/solve.config.lib.mjs +16 -0
- package/src/solve.error-handlers.lib.mjs +8 -5
- package/src/solve.mjs +9 -2
- package/src/solve.watch.lib.mjs +46 -0
- package/src/telegram-terminal-watch-command.lib.mjs +18 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.65.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d5cd096: Add a solve flag to disable separate error-report issue creation while preserving original issue failure comments, and improve pre-PR branch divergence diagnostics.
|
|
8
|
+
|
|
9
|
+
## 1.65.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 14fe57e: Prevent normal Docker release manifest jobs from downloading DinD digest artifacts.
|
|
14
|
+
- 74ce579: Reduce `/terminal_watch` Telegram edits by updating only when the displayed terminal snapshot changes and count only real terminal snapshot updates.
|
|
15
|
+
- 78ab6e2: Add `--auto-delete-branch-on-merge` option for the `solve` command. When set together with `--watch`, the branch is deleted from the remote after the pull request is merged; when set together with `--auto-merge`, the auto-merge call requests branch deletion as part of the merge. The option is opt-in (default `false`), enables full GitHub Flow automation, avoids temporary auto-restart cleanup, uses the GitHub REST API for watch-mode deletion, and treats "branch already gone" responses as success so it does not warn when GitHub's "Automatically delete head branches" repo setting beats us to it.
|
|
16
|
+
- 152de95: Add a Claude CLI streaming input case study with reproducible experiment scripts.
|
|
17
|
+
|
|
3
18
|
## 1.64.4
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -271,7 +271,7 @@ export const handleErrorWithIssueCreation = async options => {
|
|
|
271
271
|
|
|
272
272
|
// --disable-report-issue takes highest precedence
|
|
273
273
|
if (disableReport) {
|
|
274
|
-
await log('ℹ️
|
|
274
|
+
await log('ℹ️ Error issue creation is disabled by CLI configuration.');
|
|
275
275
|
return null;
|
|
276
276
|
}
|
|
277
277
|
|
|
@@ -67,6 +67,8 @@ const { maybeAttachWorkingSessionSummary } = resultsLib;
|
|
|
67
67
|
const { interruptibleSleep } = await import('./interruptible-sleep.lib.mjs');
|
|
68
68
|
const { formatAutoIterationLimit, hasReachedAutoIterationLimit, normalizeAutoIterationLimit, shouldSyncBeforeRestart } = await import('./auto-iteration-limits.lib.mjs');
|
|
69
69
|
|
|
70
|
+
const shouldDeleteBranchAfterMerge = argv => argv.autoDeleteBranchOnMerge || argv.deleteBranchAfterMerge || false;
|
|
71
|
+
|
|
70
72
|
/**
|
|
71
73
|
* Main function: Watch and restart until PR becomes mergeable
|
|
72
74
|
* This implements --auto-restart-until-mergeable functionality
|
|
@@ -273,7 +275,11 @@ export const watchUntilMergeable = async params => {
|
|
|
273
275
|
if (isAutoMerge) {
|
|
274
276
|
// Attempt to merge the PR
|
|
275
277
|
await log(formatAligned('🔀', 'Auto-merging PR...', ''));
|
|
276
|
-
const
|
|
278
|
+
const deleteAfterMerge = shouldDeleteBranchAfterMerge(argv);
|
|
279
|
+
if (deleteAfterMerge) {
|
|
280
|
+
await log(formatAligned('', 'Branch cleanup:', 'will delete branch after successful merge', 2));
|
|
281
|
+
}
|
|
282
|
+
const mergeResult = await mergePullRequest(owner, repo, prNumber, { squash: argv.squash || false, deleteAfter: deleteAfterMerge }, argv.verbose);
|
|
277
283
|
|
|
278
284
|
if (mergeResult.success) {
|
|
279
285
|
await log(formatAligned('🎉', 'PR MERGED SUCCESSFULLY!', ''));
|
|
@@ -1045,7 +1051,11 @@ export const attemptAutoMerge = async params => {
|
|
|
1045
1051
|
await log(formatAligned('✅', 'PR is mergeable:', 'Attempting to merge...', 2));
|
|
1046
1052
|
|
|
1047
1053
|
// Attempt to merge
|
|
1048
|
-
const
|
|
1054
|
+
const deleteAfterMerge = shouldDeleteBranchAfterMerge(argv);
|
|
1055
|
+
if (deleteAfterMerge) {
|
|
1056
|
+
await log(formatAligned('', 'Branch cleanup:', 'will delete branch after successful merge', 2));
|
|
1057
|
+
}
|
|
1058
|
+
const mergeResult = await mergePullRequest(owner, repo, prNumber, { squash: argv.squash || false, deleteAfter: deleteAfterMerge }, argv.verbose);
|
|
1049
1059
|
|
|
1050
1060
|
if (mergeResult.success) {
|
|
1051
1061
|
await log(formatAligned('🎉', 'PR MERGED SUCCESSFULLY!', ''));
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { closingIssueNumbersContain, parseClosingIssueNumbers } from './pr-issue-linking.lib.mjs';
|
|
7
|
+
import { buildPushRejectionExplanation, getRemoteBranchDivergenceSnapshot, synchronizeExistingIssueBranchBeforeAutoPrCreation } from './solve.branch-divergence.lib.mjs';
|
|
7
8
|
|
|
8
9
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
|
|
10
|
+
|
|
9
11
|
export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNumber, owner, repo, defaultBranch, forkedRepo, isContinueMode, prNumber, log, formatAligned, $, reportError, path, fs }) {
|
|
10
12
|
// Skip auto-PR creation if:
|
|
11
13
|
// 1. Auto-PR creation is disabled AND we're not in continue mode with no PR
|
|
@@ -33,6 +35,16 @@ export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNum
|
|
|
33
35
|
const issueUrl = argv['issue-url'] || argv._[0];
|
|
34
36
|
|
|
35
37
|
try {
|
|
38
|
+
await synchronizeExistingIssueBranchBeforeAutoPrCreation({
|
|
39
|
+
tempDir,
|
|
40
|
+
branchName,
|
|
41
|
+
isContinueMode,
|
|
42
|
+
prNumber,
|
|
43
|
+
log,
|
|
44
|
+
formatAligned,
|
|
45
|
+
$,
|
|
46
|
+
});
|
|
47
|
+
|
|
36
48
|
// Determine which file to create based on CLI flags
|
|
37
49
|
let useClaudeFile = argv.claudeFile !== false;
|
|
38
50
|
const useAutoGitkeepFile = argv.autoGitkeepFile !== false;
|
|
@@ -527,6 +539,7 @@ Proceed.
|
|
|
527
539
|
await log('');
|
|
528
540
|
throw new Error('Permission denied - need fork or collaborator access');
|
|
529
541
|
} else if (errorOutput.includes('non-fast-forward') || errorOutput.includes('rejected') || errorOutput.includes('! [rejected]')) {
|
|
542
|
+
const divergence = await getRemoteBranchDivergenceSnapshot({ $, tempDir, branchName });
|
|
530
543
|
// Push rejected due to conflicts or diverged history
|
|
531
544
|
await log('');
|
|
532
545
|
await log(formatAligned('❌', 'PUSH REJECTED:', 'Branch has diverged from remote'), { level: 'error' });
|
|
@@ -534,6 +547,9 @@ Proceed.
|
|
|
534
547
|
await log(' 🔍 What happened:');
|
|
535
548
|
await log(' The remote branch has changes that conflict with your local changes.');
|
|
536
549
|
await log(' This typically means someone else has pushed to this branch.');
|
|
550
|
+
for (const line of buildPushRejectionExplanation({ branchName, isContinueMode, prNumber, divergence })) {
|
|
551
|
+
await log(line);
|
|
552
|
+
}
|
|
537
553
|
await log('');
|
|
538
554
|
await log(' 💡 Why we cannot fix this automatically:');
|
|
539
555
|
await log(' • We never use force push to preserve history');
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const toCount = value => {
|
|
2
|
+
const parsed = Number.parseInt(String(value || '').trim(), 10);
|
|
3
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const outputOf = result => {
|
|
7
|
+
const stdout = result?.stdout ? result.stdout.toString().trim() : '';
|
|
8
|
+
const stderr = result?.stderr ? result.stderr.toString().trim() : '';
|
|
9
|
+
return stdout || stderr;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function buildPushRejectionExplanation({ branchName, isContinueMode, prNumber, divergence = null }) {
|
|
13
|
+
const lines = [];
|
|
14
|
+
|
|
15
|
+
if (isContinueMode && !prNumber) {
|
|
16
|
+
lines.push(' This run reused an existing issue branch because auto-continue found a matching branch with no PR.');
|
|
17
|
+
lines.push(' It is not a fresh branch created by this run, even though auto-PR creation is running now.');
|
|
18
|
+
} else {
|
|
19
|
+
lines.push(' The remote branch changed after the local branch state used for this push.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (divergence?.remoteExists && divergence.ahead !== null && divergence.behind !== null) {
|
|
23
|
+
lines.push(` Current branch state for ${branchName}: ${divergence.ahead} commit(s) ahead, ${divergence.behind} commit(s) behind origin/${branchName}.`);
|
|
24
|
+
} else if (divergence?.fetchError) {
|
|
25
|
+
lines.push(` Could not inspect origin/${branchName}: ${divergence.fetchError}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return lines;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getRemoteBranchDivergenceSnapshot({ $, tempDir, branchName }) {
|
|
32
|
+
const fetchResult = await $({ cwd: tempDir, silent: true })`git fetch origin refs/heads/${branchName}:refs/remotes/origin/${branchName} 2>&1`;
|
|
33
|
+
if (fetchResult.code !== 0) {
|
|
34
|
+
return {
|
|
35
|
+
remoteExists: false,
|
|
36
|
+
ahead: null,
|
|
37
|
+
behind: null,
|
|
38
|
+
fetchError: outputOf(fetchResult) || 'remote branch not found',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const aheadResult = await $({ cwd: tempDir, silent: true })`git rev-list --count origin/${branchName}..HEAD 2>&1`;
|
|
43
|
+
const behindResult = await $({ cwd: tempDir, silent: true })`git rev-list --count HEAD..origin/${branchName} 2>&1`;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
remoteExists: aheadResult.code === 0 && behindResult.code === 0,
|
|
47
|
+
ahead: aheadResult.code === 0 ? toCount(aheadResult.stdout) : null,
|
|
48
|
+
behind: behindResult.code === 0 ? toCount(behindResult.stdout) : null,
|
|
49
|
+
fetchError: aheadResult.code === 0 && behindResult.code === 0 ? null : outputOf(aheadResult) || outputOf(behindResult) || 'could not compare local and remote branch',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function synchronizeExistingIssueBranchBeforeAutoPrCreation({ tempDir, branchName, isContinueMode, prNumber, log, formatAligned, $ }) {
|
|
54
|
+
if (!(isContinueMode && !prNumber)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await log(formatAligned('🔎', 'Existing branch sync:', branchName));
|
|
59
|
+
const divergence = await getRemoteBranchDivergenceSnapshot({ $, tempDir, branchName });
|
|
60
|
+
if (!divergence.remoteExists) {
|
|
61
|
+
await log(` ⚠️ Could not inspect origin/${branchName}: ${divergence.fetchError || 'unknown error'}`, { level: 'warning' });
|
|
62
|
+
return divergence;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await log(` Branch state before PR bootstrap commit: ${divergence.ahead} commit(s) ahead, ${divergence.behind} commit(s) behind origin/${branchName}`);
|
|
66
|
+
|
|
67
|
+
if (divergence.behind > 0 && divergence.ahead === 0) {
|
|
68
|
+
await log(` Fast-forwarding ${branchName} to origin/${branchName} before creating the PR bootstrap commit...`);
|
|
69
|
+
const mergeResult = await $({ cwd: tempDir })`git merge --ff-only origin/${branchName} 2>&1`;
|
|
70
|
+
if (mergeResult.code !== 0) {
|
|
71
|
+
await log(` ⚠️ Fast-forward failed: ${outputOf(mergeResult) || 'unknown error'}`, {
|
|
72
|
+
level: 'warning',
|
|
73
|
+
});
|
|
74
|
+
throw new Error('Existing issue branch could not be fast-forwarded before PR creation');
|
|
75
|
+
}
|
|
76
|
+
await log(` ✅ Branch fast-forwarded to origin/${branchName}`);
|
|
77
|
+
return await getRemoteBranchDivergenceSnapshot({ $, tempDir, branchName });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (divergence.behind > 0 && divergence.ahead > 0) {
|
|
81
|
+
for (const line of buildPushRejectionExplanation({
|
|
82
|
+
branchName,
|
|
83
|
+
isContinueMode,
|
|
84
|
+
prNumber,
|
|
85
|
+
divergence,
|
|
86
|
+
})) {
|
|
87
|
+
await log(line);
|
|
88
|
+
}
|
|
89
|
+
throw new Error('Existing issue branch has diverged before PR creation; manual resolution required');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return divergence;
|
|
93
|
+
}
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -243,6 +243,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
243
243
|
description: 'Interval in seconds for checking feedback in watch mode (default: 60)',
|
|
244
244
|
default: 60,
|
|
245
245
|
},
|
|
246
|
+
'auto-delete-branch-on-merge': {
|
|
247
|
+
type: 'boolean',
|
|
248
|
+
description: 'Automatically delete the branch after the pull request is merged in --watch mode or by --auto-merge. Enables full GitHub Flow support (issue #401).',
|
|
249
|
+
default: false,
|
|
250
|
+
},
|
|
246
251
|
'min-disk-space': {
|
|
247
252
|
type: 'number',
|
|
248
253
|
description: 'Minimum required disk space in MB (default: 2048)',
|
|
@@ -507,6 +512,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
507
512
|
description: 'Disable error issue creation entirely (no prompt, no automatic creation). Overrides --auto-report-issue if both are specified.',
|
|
508
513
|
default: false,
|
|
509
514
|
},
|
|
515
|
+
'disable-issue-auto-creation-on-error': {
|
|
516
|
+
type: 'boolean',
|
|
517
|
+
description: 'Disable creating a new GitHub error-report issue when solve fails, including the interactive prompt. This does not disable posting failure logs or comments to the original issue or pull request.',
|
|
518
|
+
default: false,
|
|
519
|
+
},
|
|
510
520
|
'attach-solution-summary': {
|
|
511
521
|
type: 'boolean',
|
|
512
522
|
description: 'Attach the AI working session summary (from the result field) as a comment to the PR/issue after every working session. The summary is extracted from the AI tool JSON output and posted under a "Working session summary" header. Applies to the top-level run, auto-restart-until-mergeable iterations, and watch-mode iterations.',
|
|
@@ -713,6 +723,12 @@ export const parseArguments = async (yargs = getLinoYargsFactory(), hideBinFn =
|
|
|
713
723
|
if (argv.toolCheck === false) {
|
|
714
724
|
argv.toolConnectionCheck = false;
|
|
715
725
|
}
|
|
726
|
+
// Issue #1752: new flag is the explicit user-facing switch for disabling
|
|
727
|
+
// creation of separate solver-error issues. Keep the existing internal
|
|
728
|
+
// disableReportIssue path as the single behavior flag.
|
|
729
|
+
if (argv.disableIssueAutoCreationOnError) {
|
|
730
|
+
argv.disableReportIssue = true;
|
|
731
|
+
}
|
|
716
732
|
}
|
|
717
733
|
|
|
718
734
|
// --finalize normalization
|
|
@@ -12,11 +12,14 @@ import { reportError } from './sentry.lib.mjs';
|
|
|
12
12
|
// Import GitHub error reporter
|
|
13
13
|
import { handleErrorWithIssueCreation } from './github-error-reporter.lib.mjs';
|
|
14
14
|
|
|
15
|
+
export const isErrorIssueAutoCreationDisabled = argv => !!(argv?.disableReportIssue || argv?.disableIssueAutoCreationOnError);
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* Handles log attachment and PR closing on failure
|
|
17
19
|
*/
|
|
18
20
|
export const handleFailure = async options => {
|
|
19
21
|
const { error, errorType, shouldAttachLogs, argv, global, owner, repo, log, getLogFile, attachLogToGitHub, cleanErrorMessage, sanitizeLogContent, $ } = options;
|
|
22
|
+
const disableIssueCreation = isErrorIssueAutoCreationDisabled(argv);
|
|
20
23
|
|
|
21
24
|
// Offer to create GitHub issue for the error
|
|
22
25
|
try {
|
|
@@ -30,9 +33,9 @@ export const handleFailure = async options => {
|
|
|
30
33
|
prNumber: global.createdPR?.number,
|
|
31
34
|
errorType,
|
|
32
35
|
},
|
|
33
|
-
skipPrompt: !process.stdin.isTTY || argv.noIssueCreation,
|
|
36
|
+
skipPrompt: !process.stdin.isTTY || argv.noIssueCreation || disableIssueCreation,
|
|
34
37
|
autoReport: argv.autoReportIssue,
|
|
35
|
-
disableReport:
|
|
38
|
+
disableReport: disableIssueCreation,
|
|
36
39
|
});
|
|
37
40
|
} catch (issueError) {
|
|
38
41
|
reportError(issueError, {
|
|
@@ -49,7 +52,7 @@ export const handleFailure = async options => {
|
|
|
49
52
|
const hasIssue = global.issueNumber;
|
|
50
53
|
const targetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
|
|
51
54
|
const targetNumber = hasPR ? global.createdPR.number : hasIssue ? global.issueNumber : null;
|
|
52
|
-
const targetLabel = hasPR ? 'Pull Request' :
|
|
55
|
+
const targetLabel = hasPR ? 'Pull Request' : `original issue #${targetNumber}`;
|
|
53
56
|
|
|
54
57
|
if (targetType && targetNumber) {
|
|
55
58
|
await log(`\n📄 Attempting to attach failure logs to ${targetLabel}...`);
|
|
@@ -70,7 +73,7 @@ export const handleFailure = async options => {
|
|
|
70
73
|
tool: argv.tool || 'claude',
|
|
71
74
|
});
|
|
72
75
|
if (logUploadSuccess) {
|
|
73
|
-
await log(`📎 Failure log
|
|
76
|
+
await log(`📎 Failure log posted to ${targetLabel}`);
|
|
74
77
|
if (!hasPR && hasIssue) global.prePullRequestFailureNotificationPosted = true;
|
|
75
78
|
}
|
|
76
79
|
} catch (attachError) {
|
|
@@ -81,7 +84,7 @@ export const handleFailure = async options => {
|
|
|
81
84
|
errorType,
|
|
82
85
|
operation: `attach_log_to_${targetType}`,
|
|
83
86
|
});
|
|
84
|
-
await log(`⚠️ Could not
|
|
87
|
+
await log(`⚠️ Could not post failure log to ${targetLabel}: ${attachError.message}`, { level: 'warning' });
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
}
|
package/src/solve.mjs
CHANGED
|
@@ -162,6 +162,12 @@ const { isIssueUrl, isPrUrl, normalizedUrl, owner, repo, number: urlNumber } = u
|
|
|
162
162
|
issueUrl = normalizedUrl || issueUrl;
|
|
163
163
|
global.owner = owner;
|
|
164
164
|
global.repo = repo;
|
|
165
|
+
// Issue #1752: failures before PR creation can happen during checks that run
|
|
166
|
+
// before the normal issue-mode setup below. Record the source issue as soon as
|
|
167
|
+
// the URL is validated so the pre-exit notifier can still comment on it.
|
|
168
|
+
if (isIssueUrl) {
|
|
169
|
+
global.issueNumber = urlNumber;
|
|
170
|
+
}
|
|
165
171
|
cleanupContext.owner = owner;
|
|
166
172
|
cleanupContext.repo = repo;
|
|
167
173
|
// Setup unhandled error handlers to ensure log path is always shown
|
|
@@ -331,6 +337,7 @@ if (autoContinueResult.isContinueMode) {
|
|
|
331
337
|
} else {
|
|
332
338
|
// We have a branch but no PR - we'll use the existing branch and create a PR later
|
|
333
339
|
await log(`🔄 Using existing branch: ${prBranch} (no PR yet - will create one)`);
|
|
340
|
+
await log(' This branch was created by an earlier run; this run is reusing it rather than creating a fresh branch.');
|
|
334
341
|
if (argv.verbose) {
|
|
335
342
|
await log(' Branch will be checked out and PR will be created during auto-PR creation phase', {
|
|
336
343
|
verbose: true,
|
|
@@ -1021,7 +1028,7 @@ try {
|
|
|
1021
1028
|
const hasIssue = global.issueNumber;
|
|
1022
1029
|
const logTargetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
|
|
1023
1030
|
const logTargetNumber = hasPR ? global.createdPR.number : hasIssue ? global.issueNumber : null;
|
|
1024
|
-
const logTargetLabel = hasPR ? 'Pull Request' :
|
|
1031
|
+
const logTargetLabel = hasPR ? 'Pull Request' : `original issue #${logTargetNumber}`;
|
|
1025
1032
|
|
|
1026
1033
|
if (shouldAttachLogs && logTargetType && logTargetNumber) {
|
|
1027
1034
|
await log(`\n📄 Attaching failure logs to ${logTargetLabel}...`);
|
|
@@ -1054,7 +1061,7 @@ try {
|
|
|
1054
1061
|
});
|
|
1055
1062
|
|
|
1056
1063
|
if (logUploadSuccess) {
|
|
1057
|
-
await log(` 📎 Failure logs
|
|
1064
|
+
await log(` 📎 Failure logs posted to ${logTargetLabel}`);
|
|
1058
1065
|
} else {
|
|
1059
1066
|
// Issue #1212: Always show log upload failures (not just verbose)
|
|
1060
1067
|
await log(' ⚠️ Failed to upload failure logs');
|
package/src/solve.watch.lib.mjs
CHANGED
|
@@ -107,6 +107,52 @@ export const watchForFeedback = async params => {
|
|
|
107
107
|
await log('');
|
|
108
108
|
await log(formatAligned('🎉', 'PR MERGED!', 'Stopping watch mode'));
|
|
109
109
|
await log(formatAligned('', 'Pull request:', `#${prNumber} has been merged`, 2));
|
|
110
|
+
|
|
111
|
+
// Issue #401: If --auto-delete-branch-on-merge is enabled in --watch mode,
|
|
112
|
+
// delete the branch from the remote after the PR is merged. This enables
|
|
113
|
+
// full GitHub Flow automation. Only applies in --watch mode (not auto-restart),
|
|
114
|
+
// because auto-restart is for completing local work, not finalizing GitHub Flow.
|
|
115
|
+
const shouldAutoDeleteBranch = !isTemporaryWatch && argv.autoDeleteBranchOnMerge && branchName;
|
|
116
|
+
if (shouldAutoDeleteBranch) {
|
|
117
|
+
await log('');
|
|
118
|
+
await log(formatAligned('🗑️', 'AUTO-DELETE:', `Deleting branch ${branchName} after merge`));
|
|
119
|
+
try {
|
|
120
|
+
// Delete the branch from the remote via GitHub REST API.
|
|
121
|
+
// We use `gh api ... -X DELETE` rather than `git push --delete` so we don't
|
|
122
|
+
// require a configured local remote in tempDir at this point in the run.
|
|
123
|
+
const deleteBranchResult = await $`gh api repos/${owner}/${repo}/git/refs/heads/${branchName} -X DELETE`;
|
|
124
|
+
if (deleteBranchResult.code === 0) {
|
|
125
|
+
await log(formatAligned('✅', 'Branch deleted:', `${branchName}`, 2));
|
|
126
|
+
} else {
|
|
127
|
+
const stderrText = deleteBranchResult.stderr?.toString().trim() || 'Unknown error';
|
|
128
|
+
// 422 Reference does not exist -> branch was already deleted (e.g. GitHub's "Automatically delete head branches"
|
|
129
|
+
// setting raced ahead of us). Treat as success rather than warning.
|
|
130
|
+
if (/Reference does not exist|Not Found|422|404/i.test(stderrText)) {
|
|
131
|
+
await log(formatAligned('✅', 'Branch already removed:', `${branchName} (no action needed)`, 2));
|
|
132
|
+
} else {
|
|
133
|
+
await log(formatAligned('⚠️', 'Branch deletion failed:', stderrText, 2));
|
|
134
|
+
reportError(new Error(`Branch deletion returned non-zero exit code: ${stderrText}`), {
|
|
135
|
+
context: 'delete_branch_on_merge_non_zero',
|
|
136
|
+
owner,
|
|
137
|
+
repo,
|
|
138
|
+
branchName,
|
|
139
|
+
exitCode: deleteBranchResult.code,
|
|
140
|
+
operation: 'delete_remote_branch',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (deleteError) {
|
|
145
|
+
reportError(deleteError, {
|
|
146
|
+
context: 'delete_branch_on_merge',
|
|
147
|
+
owner,
|
|
148
|
+
repo,
|
|
149
|
+
branchName,
|
|
150
|
+
operation: 'delete_remote_branch',
|
|
151
|
+
});
|
|
152
|
+
await log(formatAligned('⚠️', 'Branch deletion error:', cleanErrorMessage(deleteError), 2));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
110
156
|
await log('');
|
|
111
157
|
break;
|
|
112
158
|
}
|
|
@@ -173,12 +173,18 @@ async function querySessionStatusWithRetry(querySessionStatus, sessionId, verbos
|
|
|
173
173
|
|
|
174
174
|
// Note: /terminal_watch never uploads the full session log itself (issue #1720).
|
|
175
175
|
// Use /log <uuid> if you want the log file delivered as a document.
|
|
176
|
-
|
|
176
|
+
function getDisplayedTerminalSnapshot(logText, options) {
|
|
177
|
+
return sanitizeCodeBlock(tailTextForTerminal(logText, options));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function watchTerminalLogSession({ bot, chatId, messageId, sessionId, logPath, querySessionStatus, isTerminalSessionStatus, options = {}, repoDescription = null, verbose = false, initialStatusResult = null, initialLogText = null, initialMessage = '' }) {
|
|
177
181
|
const key = `${chatId}:${messageId}:${sessionId}`;
|
|
178
182
|
activeWatches.get(key)?.stop();
|
|
179
183
|
|
|
180
184
|
let stopped = false;
|
|
181
|
-
|
|
185
|
+
const hasInitialLogText = initialLogText !== null && initialLogText !== undefined;
|
|
186
|
+
let lastSnapshot = hasInitialLogText ? getDisplayedTerminalSnapshot(initialLogText, options) : null;
|
|
187
|
+
let lastMessage = initialMessage || (hasInitialLogText ? formatTerminalWatchMessage({ sessionId, statusResult: initialStatusResult, logText: initialLogText, options, updateCount: 0, completed: !!initialStatusResult?.status && isTerminalSessionStatus(initialStatusResult.status), repoDescription }) : '');
|
|
182
188
|
let updateCount = 0;
|
|
183
189
|
let timer = null;
|
|
184
190
|
const intervalMs = options.intervalMs || DEFAULT_INTERVAL_MS;
|
|
@@ -189,11 +195,16 @@ export function watchTerminalLogSession({ bot, chatId, messageId, sessionId, log
|
|
|
189
195
|
const statusResult = await querySessionStatus(sessionId, verbose);
|
|
190
196
|
const completed = !!statusResult?.status && isTerminalSessionStatus(statusResult.status);
|
|
191
197
|
const logText = await readLogFile(logPath);
|
|
192
|
-
const
|
|
193
|
-
|
|
198
|
+
const snapshot = getDisplayedTerminalSnapshot(logText, options);
|
|
199
|
+
const snapshotChanged = snapshot !== lastSnapshot;
|
|
200
|
+
if (snapshotChanged) updateCount++;
|
|
201
|
+
const message = formatTerminalWatchMessage({ sessionId, statusResult, logText, options, updateCount, completed, repoDescription });
|
|
202
|
+
const shouldEdit = !lastMessage || snapshotChanged || (completed && message !== lastMessage);
|
|
203
|
+
if (shouldEdit && message !== lastMessage) {
|
|
194
204
|
await bot.telegram.editMessageText(chatId, messageId, undefined, message, { parse_mode: 'Markdown' });
|
|
195
205
|
lastMessage = message;
|
|
196
206
|
}
|
|
207
|
+
lastSnapshot = snapshot;
|
|
197
208
|
if (completed) {
|
|
198
209
|
stopped = true;
|
|
199
210
|
activeWatches.delete(key);
|
|
@@ -255,14 +266,15 @@ async function startWatchFromResolvedSession({ bot, ctx, sessionId, statusResult
|
|
|
255
266
|
if (!targetChatId) return { started: false, reason: 'Missing target chat id' };
|
|
256
267
|
|
|
257
268
|
const initialLogText = await readLogFile(logPath);
|
|
258
|
-
const
|
|
269
|
+
const initialCompleted = !!statusResult?.status && isTerminalSessionStatus(statusResult.status);
|
|
270
|
+
const initialText = formatTerminalWatchMessage({ sessionId, statusResult, logText: initialLogText, options: watchOptions, completed: initialCompleted, repoDescription });
|
|
259
271
|
let replyToMessageId = ctx.message?.message_id || undefined;
|
|
260
272
|
if (decision.destination === 'dm' && ctx.chat.type !== 'private') {
|
|
261
273
|
replyToMessageId = await forwardOrCopyToDm(ctx, ctx.message?.reply_to_message || ctx.message);
|
|
262
274
|
}
|
|
263
275
|
|
|
264
276
|
const watchMessage = await createWatchMessage({ ctx, targetChatId, replyToMessageId, text: initialText });
|
|
265
|
-
watchTerminalLogSession({ bot, chatId: targetChatId, messageId: watchMessage.message_id, sessionId, logPath, querySessionStatus, isTerminalSessionStatus, options: watchOptions, repoDescription, verbose });
|
|
277
|
+
watchTerminalLogSession({ bot, chatId: targetChatId, messageId: watchMessage.message_id, sessionId, logPath, querySessionStatus, isTerminalSessionStatus, options: watchOptions, repoDescription, verbose, initialStatusResult: statusResult, initialLogText, initialMessage: initialText });
|
|
266
278
|
|
|
267
279
|
if (!auto && decision.destination === 'dm' && ctx.chat.type !== 'private') {
|
|
268
280
|
await ctx.reply(`📬 Started terminal watch for \`${sessionId}\` in your direct messages.`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
|