@link-assistant/hive-mind 1.56.15 → 1.56.16
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 +7 -0
- package/package.json +2 -2
- package/src/github-merge-ci-signals.lib.mjs +182 -0
- package/src/github-merge.lib.mjs +50 -166
- package/src/solve.auto-merge-helpers.lib.mjs +50 -1
- package/src/solve.auto-merge.lib.mjs +6 -0
- package/src/solve.config.lib.mjs +6 -6
- package/src/telegram-bot.mjs +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.56.16
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2e2d9e6: Fix `/merge` and `--auto-restart-until-mergeable` getting stuck forever waiting for check-runs that never arrive when a target repo's GitHub Actions workflow file is invalid (e.g. YAML syntax error or `Unrecognized named-value` expression error). GitHub creates a `status=completed, conclusion=failure` workflow run with zero jobs and zero check-runs in this case; the new `getWorkflowRunJobsCount` helper detects the zero-jobs signal and surfaces the broken workflow as a `ci_failure` blocker so the auto-restart loop fires and the AI solver receives the actionable error (workflow file path + run URL) instead of looping silently. See `docs/case-studies/issue-1690/`.
|
|
8
|
+
- a0a25de: Make four stabilized options enabled by default (issue #1694): `--auto-accept-invite`, `--tokens-budget-stats`, and `--auto-attach-solution-summary` now default to `true` for `solve` and `hive` (use `--no-…` to disable), and the `hive-telegram-bot`'s `--isolation` defaults to `screen` (set `TELEGRAM_ISOLATION=` or pass `--isolation ''` to disable). The Telegram `/solve` auto-accept-invite pre-check now reads the parsed `argv` so the new default fires without an explicit `--auto-accept-invite` and `--no-auto-accept-invite` works as a real opt-out.
|
|
9
|
+
|
|
3
10
|
## 1.56.15
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.56.
|
|
3
|
+
"version": "1.56.16",
|
|
4
4
|
"description": "AI-powered issue solver and hive mind for collaborative problem solving",
|
|
5
5
|
"main": "src/hive.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"hive-telegram-bot": "./src/telegram-bot.mjs"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-auto-restart-limits-1664.mjs && node tests/test-log-upload-output-1678.mjs && node tests/test-log-upload-output-1682.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-telegram-options-before-url.mjs && node tests/test-telegram-bot-configuration-isolation-links-notation.mjs && node tests/test-extract-isolation-from-args.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-issue-1670-screen-status-monitoring.mjs && node tests/test-issue-1680-session-monitoring.mjs && node tests/test-issue-1684-message-formatting.mjs && node tests/test-telegram-bot-launcher.mjs",
|
|
18
|
+
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-auto-restart-limits-1664.mjs && node tests/test-log-upload-output-1678.mjs && node tests/test-log-upload-output-1682.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-telegram-options-before-url.mjs && node tests/test-telegram-bot-configuration-isolation-links-notation.mjs && node tests/test-extract-isolation-from-args.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-issue-1670-screen-status-monitoring.mjs && node tests/test-issue-1680-session-monitoring.mjs && node tests/test-issue-1684-message-formatting.mjs && node tests/test-issue-1694-stabilized-defaults.mjs && node tests/test-telegram-bot-launcher.mjs",
|
|
19
19
|
"test:queue": "node tests/solve-queue.test.mjs",
|
|
20
20
|
"test:limits-display": "node tests/limits-display.test.mjs",
|
|
21
21
|
"test:usage-limit": "node tests/test-usage-limit.mjs",
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Merge Queue CI Signal Helpers
|
|
4
|
+
*
|
|
5
|
+
* Helpers for distinguishing genuine "CI not triggered" from race conditions in
|
|
6
|
+
* the auto-merge loop. Split from github-merge.lib.mjs to keep that file under
|
|
7
|
+
* the 1500-line CI limit (issue #1690 push).
|
|
8
|
+
*
|
|
9
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1480
|
|
10
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1503
|
|
11
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1690
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { promisify } from 'util';
|
|
15
|
+
import { exec as execCallback } from 'child_process';
|
|
16
|
+
|
|
17
|
+
const exec = promisify(execCallback);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the committed date of a specific commit from GitHub API
|
|
21
|
+
* Issue #1480: Used to determine how recently a commit was pushed, to distinguish between
|
|
22
|
+
* "CI not yet registered in API" (race condition) and "CI definitively not triggered"
|
|
23
|
+
* @param {string} owner - Repository owner
|
|
24
|
+
* @param {string} repo - Repository name
|
|
25
|
+
* @param {string} sha - Commit SHA
|
|
26
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
27
|
+
* @returns {Promise<{date: Date|null, ageSeconds: number|null}>}
|
|
28
|
+
*/
|
|
29
|
+
export async function getCommitDate(owner, repo, sha, verbose = false) {
|
|
30
|
+
try {
|
|
31
|
+
const { stdout } = await exec(`gh api repos/${owner}/${repo}/commits/${sha} --jq '.commit.committer.date'`);
|
|
32
|
+
const dateStr = stdout.trim();
|
|
33
|
+
if (!dateStr) {
|
|
34
|
+
return { date: null, ageSeconds: null };
|
|
35
|
+
}
|
|
36
|
+
const commitDate = new Date(dateStr);
|
|
37
|
+
const ageSeconds = Math.floor((Date.now() - commitDate.getTime()) / 1000);
|
|
38
|
+
if (verbose) {
|
|
39
|
+
console.log(`[VERBOSE] /merge: Commit ${sha.substring(0, 7)} date: ${dateStr} (${ageSeconds}s ago)`);
|
|
40
|
+
}
|
|
41
|
+
return { date: commitDate, ageSeconds };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (verbose) {
|
|
44
|
+
console.log(`[VERBOSE] /merge: Error fetching commit date for ${sha}: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
return { date: null, ageSeconds: null };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if any previous commits in a PR had workflow runs triggered.
|
|
52
|
+
* Issue #1480: If earlier commits in the same PR triggered CI, we should expect CI
|
|
53
|
+
* for the HEAD commit too (unless conditions changed). This provides an additional
|
|
54
|
+
* signal that CI should be expected and avoids false "CI not triggered" conclusions.
|
|
55
|
+
* @param {string} owner - Repository owner
|
|
56
|
+
* @param {string} repo - Repository name
|
|
57
|
+
* @param {number} prNumber - Pull request number
|
|
58
|
+
* @param {string} headSha - Current HEAD SHA (to exclude from check)
|
|
59
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
60
|
+
* @returns {Promise<{hadPreviousCI: boolean, previousCommitsWithCI: number, totalPreviousCommits: number}>}
|
|
61
|
+
*/
|
|
62
|
+
export async function checkPreviousPRCommitsHadCI(owner, repo, prNumber, headSha, verbose = false) {
|
|
63
|
+
try {
|
|
64
|
+
// Get all commits in the PR
|
|
65
|
+
const { stdout: commitsJson } = await exec(`gh api "repos/${owner}/${repo}/pulls/${prNumber}/commits" --paginate --jq '[.[].sha]'`);
|
|
66
|
+
const allShas = JSON.parse(commitsJson.trim() || '[]');
|
|
67
|
+
|
|
68
|
+
// Exclude the current HEAD SHA
|
|
69
|
+
const previousShas = allShas.filter(sha => sha !== headSha);
|
|
70
|
+
|
|
71
|
+
if (previousShas.length === 0) {
|
|
72
|
+
if (verbose) {
|
|
73
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no previous commits to check for CI history`);
|
|
74
|
+
}
|
|
75
|
+
return { hadPreviousCI: false, previousCommitsWithCI: 0, totalPreviousCommits: 0 };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check the most recent previous commits (limit to last 3 to avoid excessive API calls)
|
|
79
|
+
const commitsToCheck = previousShas.slice(-3);
|
|
80
|
+
let commitsWithCI = 0;
|
|
81
|
+
|
|
82
|
+
for (const sha of commitsToCheck) {
|
|
83
|
+
try {
|
|
84
|
+
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=100" --paginate --slurp`);
|
|
85
|
+
const count = JSON.parse(stdout.trim() || '[]').reduce((sum, page) => sum + (page.workflow_runs?.length || 0), 0);
|
|
86
|
+
if (count > 0) {
|
|
87
|
+
commitsWithCI++;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// Skip errors for individual commits
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hadPreviousCI = commitsWithCI > 0;
|
|
95
|
+
|
|
96
|
+
if (verbose) {
|
|
97
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} previous CI history: ${commitsWithCI}/${commitsToCheck.length} checked commits had workflow runs (total PR commits: ${allShas.length})`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { hadPreviousCI, previousCommitsWithCI: commitsWithCI, totalPreviousCommits: previousShas.length };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (verbose) {
|
|
103
|
+
console.log(`[VERBOSE] /merge: Error checking previous PR commits CI history: ${error.message}`);
|
|
104
|
+
}
|
|
105
|
+
return { hadPreviousCI: false, previousCommitsWithCI: 0, totalPreviousCommits: 0 };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if any workflow files in the repository have PR-related triggers
|
|
111
|
+
* Issue #1480: Used as additional signal to determine if CI should run on PRs.
|
|
112
|
+
* Parses .github/workflows/*.yml files from the repository content API.
|
|
113
|
+
* @param {string} owner - Repository owner
|
|
114
|
+
* @param {string} repo - Repository name
|
|
115
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
116
|
+
* @returns {Promise<{hasPRTriggers: boolean, hasWorkflowFiles: boolean, workflows: Array<{name: string, triggers: string[]}>}>}
|
|
117
|
+
*/
|
|
118
|
+
export async function checkWorkflowsHavePRTriggers(owner, repo, verbose = false, ref = null) {
|
|
119
|
+
try {
|
|
120
|
+
// Issue #1503: Support querying workflow files from a specific branch (ref)
|
|
121
|
+
const refParam = ref ? `?ref=${encodeURIComponent(ref)}` : '';
|
|
122
|
+
// List workflow files in .github/workflows/ (uses ref if provided, otherwise default branch)
|
|
123
|
+
const { stdout: listJson } = await exec(`gh api "repos/${owner}/${repo}/contents/.github/workflows${refParam}" --paginate --jq '[.[] | select(.name | test("\\\\.(yml|yaml)$")) | {name: .name, download_url: .download_url, path: .path}]' 2>/dev/null`);
|
|
124
|
+
const files = JSON.parse(listJson.trim() || '[]');
|
|
125
|
+
|
|
126
|
+
if (files.length === 0) {
|
|
127
|
+
if (verbose) console.log(`[VERBOSE] /merge: No workflow files in ${owner}/${repo}/.github/workflows/`);
|
|
128
|
+
return { hasPRTriggers: false, hasWorkflowFiles: false, workflows: [] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const prTriggerPatterns = [/\bon:\s*\n\s+pull_request/m, /\bon:\s*\[.*pull_request.*\]/m, /\bon:\s*pull_request\b/m, /\bpull_request_target\b/m];
|
|
132
|
+
const pushTriggerPatterns = [/\bon:\s*\n\s+push/m, /\bon:\s*\[.*push.*\]/m, /\bon:\s*push\b/m];
|
|
133
|
+
// Issue #1503: Non-PR triggers for diagnostics (won't produce check-runs on PRs)
|
|
134
|
+
const nonPROnlyTriggerPatterns = [/\bworkflow_dispatch\b/m, /\bschedule\b/m, /\brepository_dispatch\b/m, /\bworkflow_call\b/m];
|
|
135
|
+
|
|
136
|
+
const results = [];
|
|
137
|
+
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
try {
|
|
140
|
+
// Issue #1503: Fetch file content using same ref parameter for branch-specific workflows
|
|
141
|
+
const { stdout: contentJson } = await exec(`gh api "repos/${owner}/${repo}/contents/${file.path}${refParam}" --jq '.content'`);
|
|
142
|
+
const content = Buffer.from(contentJson.trim().replace(/"/g, ''), 'base64').toString('utf-8');
|
|
143
|
+
|
|
144
|
+
const triggers = [];
|
|
145
|
+
if (prTriggerPatterns.some(p => p.test(content))) {
|
|
146
|
+
triggers.push('pull_request');
|
|
147
|
+
}
|
|
148
|
+
if (pushTriggerPatterns.some(p => p.test(content))) {
|
|
149
|
+
triggers.push('push');
|
|
150
|
+
}
|
|
151
|
+
// Issue #1503: Track non-PR triggers for diagnostics
|
|
152
|
+
const nonPRTriggers = nonPROnlyTriggerPatterns.filter(p => p.test(content)).map(p => p.source.replace(/\\b/g, ''));
|
|
153
|
+
|
|
154
|
+
if (triggers.length > 0) {
|
|
155
|
+
results.push({ name: file.name, triggers });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (verbose) {
|
|
159
|
+
console.log(`[VERBOSE] /merge: Workflow ${file.name}: pr_triggers=[${triggers.join(', ')}], non_pr_triggers=[${nonPRTriggers.join(', ')}]`);
|
|
160
|
+
}
|
|
161
|
+
} catch (fileError) {
|
|
162
|
+
if (verbose) {
|
|
163
|
+
console.log(`[VERBOSE] /merge: Error reading workflow file ${file.name}: ${fileError.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const hasPRTriggers = results.length > 0;
|
|
169
|
+
|
|
170
|
+
if (verbose) {
|
|
171
|
+
console.log(`[VERBOSE] /merge: ${results.length}/${files.length} workflow files have PR/push triggers`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { hasPRTriggers, hasWorkflowFiles: true, workflows: results };
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (verbose) {
|
|
177
|
+
console.log(`[VERBOSE] /merge: Error checking workflow PR triggers: ${error.message}`);
|
|
178
|
+
}
|
|
179
|
+
// On error, assume workflows might have PR triggers (safer: avoids false positives)
|
|
180
|
+
return { hasPRTriggers: true, hasWorkflowFiles: true, workflows: [] };
|
|
181
|
+
}
|
|
182
|
+
}
|
package/src/github-merge.lib.mjs
CHANGED
|
@@ -1203,14 +1203,14 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
|
|
|
1203
1203
|
* @param {string} repo - Repository name
|
|
1204
1204
|
* @param {string} sha - Commit SHA
|
|
1205
1205
|
* @param {boolean} verbose - Whether to log verbose output
|
|
1206
|
-
* @returns {Promise<Array<{id: number, status: string, conclusion: string|null, name: string, html_url: string}>>}
|
|
1206
|
+
* @returns {Promise<Array<{id: number, status: string, conclusion: string|null, name: string, html_url: string, path: string}>>}
|
|
1207
1207
|
*/
|
|
1208
1208
|
export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
|
|
1209
1209
|
try {
|
|
1210
1210
|
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=100" --paginate --slurp`);
|
|
1211
1211
|
const runs = JSON.parse(stdout.trim() || '[]')
|
|
1212
1212
|
.flatMap(page => page.workflow_runs || [])
|
|
1213
|
-
.map(run => ({ id: run.id, status: run.status, conclusion: run.conclusion, name: run.name, html_url: run.html_url }));
|
|
1213
|
+
.map(run => ({ id: run.id, status: run.status, conclusion: run.conclusion, name: run.name, html_url: run.html_url, path: run.path }));
|
|
1214
1214
|
|
|
1215
1215
|
if (verbose) {
|
|
1216
1216
|
console.log(`[VERBOSE] /merge: Found ${runs.length} workflow runs for SHA ${sha.substring(0, 7)}`);
|
|
@@ -1228,6 +1228,50 @@ export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
|
|
|
1228
1228
|
}
|
|
1229
1229
|
}
|
|
1230
1230
|
|
|
1231
|
+
/**
|
|
1232
|
+
* Get the job count for a specific workflow run.
|
|
1233
|
+
*
|
|
1234
|
+
* Issue #1690: Used to detect "invalid workflow file" failures. When a workflow file
|
|
1235
|
+
* has a syntax error (e.g., `Unrecognized named-value: 'env'`), GitHub creates a
|
|
1236
|
+
* workflow_run with `status=completed, conclusion=failure` but never instantiates
|
|
1237
|
+
* any jobs — `total_count: 0`. Such workflow runs will never produce check-runs.
|
|
1238
|
+
*
|
|
1239
|
+
* Distinguishing this from a real failure (where check-runs exist for the failed jobs)
|
|
1240
|
+
* lets the auto-merge loop break out of "waiting for check-runs to appear" and
|
|
1241
|
+
* propagate the error to the AI solver as a real failure.
|
|
1242
|
+
*
|
|
1243
|
+
* @param {string} owner - Repository owner
|
|
1244
|
+
* @param {string} repo - Repository name
|
|
1245
|
+
* @param {number|string} runId - Workflow run ID
|
|
1246
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
1247
|
+
* @returns {Promise<number|null>} - Total job count, or null on error
|
|
1248
|
+
*/
|
|
1249
|
+
export async function getWorkflowRunJobsCount(owner, repo, runId, verbose = false) {
|
|
1250
|
+
try {
|
|
1251
|
+
// Issue #1690: We only need the total_count field, so a single page is sufficient and
|
|
1252
|
+
// adding --paginate would defeat the --jq selector. Use --silent to bypass the
|
|
1253
|
+
// pagination linter rule because total_count comes from the response root.
|
|
1254
|
+
/* eslint-disable-next-line gh-paginate/require-gh-paginate */
|
|
1255
|
+
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs/${runId}/jobs?per_page=1" --jq '.total_count'`);
|
|
1256
|
+
const count = parseInt(stdout.trim(), 10);
|
|
1257
|
+
if (Number.isNaN(count)) {
|
|
1258
|
+
if (verbose) {
|
|
1259
|
+
console.log(`[VERBOSE] /merge: Could not parse job count for workflow run ${runId} (got: "${stdout.trim()}")`);
|
|
1260
|
+
}
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
if (verbose) {
|
|
1264
|
+
console.log(`[VERBOSE] /merge: Workflow run ${runId} has ${count} job(s)`);
|
|
1265
|
+
}
|
|
1266
|
+
return count;
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
if (verbose) {
|
|
1269
|
+
console.log(`[VERBOSE] /merge: Error fetching jobs for workflow run ${runId}: ${error.message}`);
|
|
1270
|
+
}
|
|
1271
|
+
return null;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1231
1275
|
/**
|
|
1232
1276
|
* Get the count of active (enabled) GitHub Actions workflows in a repository
|
|
1233
1277
|
* Issue #1363: Used to distinguish between "no CI configured" and "CI hasn't started yet"
|
|
@@ -1281,170 +1325,9 @@ export async function getActiveRepoWorkflows(owner, repo, verbose = false) {
|
|
|
1281
1325
|
}
|
|
1282
1326
|
}
|
|
1283
1327
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
* "CI not yet registered in API" (race condition) and "CI definitively not triggered"
|
|
1288
|
-
* @param {string} owner - Repository owner
|
|
1289
|
-
* @param {string} repo - Repository name
|
|
1290
|
-
* @param {string} sha - Commit SHA
|
|
1291
|
-
* @param {boolean} verbose - Whether to log verbose output
|
|
1292
|
-
* @returns {Promise<{date: Date|null, ageSeconds: number|null}>}
|
|
1293
|
-
*/
|
|
1294
|
-
export async function getCommitDate(owner, repo, sha, verbose = false) {
|
|
1295
|
-
try {
|
|
1296
|
-
const { stdout } = await exec(`gh api repos/${owner}/${repo}/commits/${sha} --jq '.commit.committer.date'`);
|
|
1297
|
-
const dateStr = stdout.trim();
|
|
1298
|
-
if (!dateStr) {
|
|
1299
|
-
return { date: null, ageSeconds: null };
|
|
1300
|
-
}
|
|
1301
|
-
const commitDate = new Date(dateStr);
|
|
1302
|
-
const ageSeconds = Math.floor((Date.now() - commitDate.getTime()) / 1000);
|
|
1303
|
-
if (verbose) {
|
|
1304
|
-
console.log(`[VERBOSE] /merge: Commit ${sha.substring(0, 7)} date: ${dateStr} (${ageSeconds}s ago)`);
|
|
1305
|
-
}
|
|
1306
|
-
return { date: commitDate, ageSeconds };
|
|
1307
|
-
} catch (error) {
|
|
1308
|
-
if (verbose) {
|
|
1309
|
-
console.log(`[VERBOSE] /merge: Error fetching commit date for ${sha}: ${error.message}`);
|
|
1310
|
-
}
|
|
1311
|
-
return { date: null, ageSeconds: null };
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
/**
|
|
1316
|
-
* Check if any previous commits in a PR had workflow runs triggered.
|
|
1317
|
-
* Issue #1480: If earlier commits in the same PR triggered CI, we should expect CI
|
|
1318
|
-
* for the HEAD commit too (unless conditions changed). This provides an additional
|
|
1319
|
-
* signal that CI should be expected and avoids false "CI not triggered" conclusions.
|
|
1320
|
-
* @param {string} owner - Repository owner
|
|
1321
|
-
* @param {string} repo - Repository name
|
|
1322
|
-
* @param {number} prNumber - Pull request number
|
|
1323
|
-
* @param {string} headSha - Current HEAD SHA (to exclude from check)
|
|
1324
|
-
* @param {boolean} verbose - Whether to log verbose output
|
|
1325
|
-
* @returns {Promise<{hadPreviousCI: boolean, previousCommitsWithCI: number, totalPreviousCommits: number}>}
|
|
1326
|
-
*/
|
|
1327
|
-
export async function checkPreviousPRCommitsHadCI(owner, repo, prNumber, headSha, verbose = false) {
|
|
1328
|
-
try {
|
|
1329
|
-
// Get all commits in the PR
|
|
1330
|
-
const { stdout: commitsJson } = await exec(`gh api "repos/${owner}/${repo}/pulls/${prNumber}/commits" --paginate --jq '[.[].sha]'`);
|
|
1331
|
-
const allShas = JSON.parse(commitsJson.trim() || '[]');
|
|
1332
|
-
|
|
1333
|
-
// Exclude the current HEAD SHA
|
|
1334
|
-
const previousShas = allShas.filter(sha => sha !== headSha);
|
|
1335
|
-
|
|
1336
|
-
if (previousShas.length === 0) {
|
|
1337
|
-
if (verbose) {
|
|
1338
|
-
console.log(`[VERBOSE] /merge: PR #${prNumber} has no previous commits to check for CI history`);
|
|
1339
|
-
}
|
|
1340
|
-
return { hadPreviousCI: false, previousCommitsWithCI: 0, totalPreviousCommits: 0 };
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
// Check the most recent previous commits (limit to last 3 to avoid excessive API calls)
|
|
1344
|
-
const commitsToCheck = previousShas.slice(-3);
|
|
1345
|
-
let commitsWithCI = 0;
|
|
1346
|
-
|
|
1347
|
-
for (const sha of commitsToCheck) {
|
|
1348
|
-
try {
|
|
1349
|
-
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=100" --paginate --slurp`);
|
|
1350
|
-
const count = JSON.parse(stdout.trim() || '[]').reduce((sum, page) => sum + (page.workflow_runs?.length || 0), 0);
|
|
1351
|
-
if (count > 0) {
|
|
1352
|
-
commitsWithCI++;
|
|
1353
|
-
}
|
|
1354
|
-
} catch {
|
|
1355
|
-
// Skip errors for individual commits
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
const hadPreviousCI = commitsWithCI > 0;
|
|
1360
|
-
|
|
1361
|
-
if (verbose) {
|
|
1362
|
-
console.log(`[VERBOSE] /merge: PR #${prNumber} previous CI history: ${commitsWithCI}/${commitsToCheck.length} checked commits had workflow runs (total PR commits: ${allShas.length})`);
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
return { hadPreviousCI, previousCommitsWithCI: commitsWithCI, totalPreviousCommits: previousShas.length };
|
|
1366
|
-
} catch (error) {
|
|
1367
|
-
if (verbose) {
|
|
1368
|
-
console.log(`[VERBOSE] /merge: Error checking previous PR commits CI history: ${error.message}`);
|
|
1369
|
-
}
|
|
1370
|
-
return { hadPreviousCI: false, previousCommitsWithCI: 0, totalPreviousCommits: 0 };
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
/**
|
|
1375
|
-
* Check if any workflow files in the repository have PR-related triggers
|
|
1376
|
-
* Issue #1480: Used as additional signal to determine if CI should run on PRs.
|
|
1377
|
-
* Parses .github/workflows/*.yml files from the repository content API.
|
|
1378
|
-
* @param {string} owner - Repository owner
|
|
1379
|
-
* @param {string} repo - Repository name
|
|
1380
|
-
* @param {boolean} verbose - Whether to log verbose output
|
|
1381
|
-
* @returns {Promise<{hasPRTriggers: boolean, hasWorkflowFiles: boolean, workflows: Array<{name: string, triggers: string[]}>}>}
|
|
1382
|
-
*/
|
|
1383
|
-
export async function checkWorkflowsHavePRTriggers(owner, repo, verbose = false, ref = null) {
|
|
1384
|
-
try {
|
|
1385
|
-
// Issue #1503: Support querying workflow files from a specific branch (ref)
|
|
1386
|
-
const refParam = ref ? `?ref=${encodeURIComponent(ref)}` : '';
|
|
1387
|
-
// List workflow files in .github/workflows/ (uses ref if provided, otherwise default branch)
|
|
1388
|
-
const { stdout: listJson } = await exec(`gh api "repos/${owner}/${repo}/contents/.github/workflows${refParam}" --paginate --jq '[.[] | select(.name | test("\\\\.(yml|yaml)$")) | {name: .name, download_url: .download_url, path: .path}]' 2>/dev/null`);
|
|
1389
|
-
const files = JSON.parse(listJson.trim() || '[]');
|
|
1390
|
-
|
|
1391
|
-
if (files.length === 0) {
|
|
1392
|
-
if (verbose) console.log(`[VERBOSE] /merge: No workflow files in ${owner}/${repo}/.github/workflows/`);
|
|
1393
|
-
return { hasPRTriggers: false, hasWorkflowFiles: false, workflows: [] };
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
const prTriggerPatterns = [/\bon:\s*\n\s+pull_request/m, /\bon:\s*\[.*pull_request.*\]/m, /\bon:\s*pull_request\b/m, /\bpull_request_target\b/m];
|
|
1397
|
-
const pushTriggerPatterns = [/\bon:\s*\n\s+push/m, /\bon:\s*\[.*push.*\]/m, /\bon:\s*push\b/m];
|
|
1398
|
-
// Issue #1503: Non-PR triggers for diagnostics (won't produce check-runs on PRs)
|
|
1399
|
-
const nonPROnlyTriggerPatterns = [/\bworkflow_dispatch\b/m, /\bschedule\b/m, /\brepository_dispatch\b/m, /\bworkflow_call\b/m];
|
|
1400
|
-
|
|
1401
|
-
const results = [];
|
|
1402
|
-
|
|
1403
|
-
for (const file of files) {
|
|
1404
|
-
try {
|
|
1405
|
-
// Issue #1503: Fetch file content using same ref parameter for branch-specific workflows
|
|
1406
|
-
const { stdout: contentJson } = await exec(`gh api "repos/${owner}/${repo}/contents/${file.path}${refParam}" --jq '.content'`);
|
|
1407
|
-
const content = Buffer.from(contentJson.trim().replace(/"/g, ''), 'base64').toString('utf-8');
|
|
1408
|
-
|
|
1409
|
-
const triggers = [];
|
|
1410
|
-
if (prTriggerPatterns.some(p => p.test(content))) {
|
|
1411
|
-
triggers.push('pull_request');
|
|
1412
|
-
}
|
|
1413
|
-
if (pushTriggerPatterns.some(p => p.test(content))) {
|
|
1414
|
-
triggers.push('push');
|
|
1415
|
-
}
|
|
1416
|
-
// Issue #1503: Track non-PR triggers for diagnostics
|
|
1417
|
-
const nonPRTriggers = nonPROnlyTriggerPatterns.filter(p => p.test(content)).map(p => p.source.replace(/\\b/g, ''));
|
|
1418
|
-
|
|
1419
|
-
if (triggers.length > 0) {
|
|
1420
|
-
results.push({ name: file.name, triggers });
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
if (verbose) {
|
|
1424
|
-
console.log(`[VERBOSE] /merge: Workflow ${file.name}: pr_triggers=[${triggers.join(', ')}], non_pr_triggers=[${nonPRTriggers.join(', ')}]`);
|
|
1425
|
-
}
|
|
1426
|
-
} catch (fileError) {
|
|
1427
|
-
if (verbose) {
|
|
1428
|
-
console.log(`[VERBOSE] /merge: Error reading workflow file ${file.name}: ${fileError.message}`);
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
const hasPRTriggers = results.length > 0;
|
|
1434
|
-
|
|
1435
|
-
if (verbose) {
|
|
1436
|
-
console.log(`[VERBOSE] /merge: ${results.length}/${files.length} workflow files have PR/push triggers`);
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
return { hasPRTriggers, hasWorkflowFiles: true, workflows: results };
|
|
1440
|
-
} catch (error) {
|
|
1441
|
-
if (verbose) {
|
|
1442
|
-
console.log(`[VERBOSE] /merge: Error checking workflow PR triggers: ${error.message}`);
|
|
1443
|
-
}
|
|
1444
|
-
// On error, assume workflows might have PR triggers (safer: avoids false positives)
|
|
1445
|
-
return { hasPRTriggers: true, hasWorkflowFiles: true, workflows: [] };
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1328
|
+
// Issue #1690: Re-export CI signal helpers from separate module to keep this file under 1500 lines
|
|
1329
|
+
import { getCommitDate, checkPreviousPRCommitsHadCI, checkWorkflowsHavePRTriggers } from './github-merge-ci-signals.lib.mjs';
|
|
1330
|
+
export { getCommitDate, checkPreviousPRCommitsHadCI, checkWorkflowsHavePRTriggers };
|
|
1448
1331
|
|
|
1449
1332
|
// Issue #1341: Re-export post-merge CI functions from separate module
|
|
1450
1333
|
import { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha } from './github-merge-ci.lib.mjs';
|
|
@@ -1481,6 +1364,7 @@ export default {
|
|
|
1481
1364
|
rerunWorkflowRun,
|
|
1482
1365
|
rerunFailedJobs,
|
|
1483
1366
|
getWorkflowRunsForSha,
|
|
1367
|
+
getWorkflowRunJobsCount, // Issue #1690: detect invalid workflow files (no jobs created)
|
|
1484
1368
|
waitForCommitCI,
|
|
1485
1369
|
checkBranchCIHealth,
|
|
1486
1370
|
getMergeCommitSha,
|
|
@@ -33,7 +33,7 @@ const { reportError } = sentryLib;
|
|
|
33
33
|
|
|
34
34
|
// Import GitHub merge functions
|
|
35
35
|
const githubMergeLib = await import('./github-merge.lib.mjs');
|
|
36
|
-
const { checkPRMergeable, checkForBillingLimitError, getDetailedCIStatus, getWorkflowRunsForSha, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers, checkPreviousPRCommitsHadCI } = githubMergeLib;
|
|
36
|
+
const { checkPRMergeable, checkForBillingLimitError, getDetailedCIStatus, getWorkflowRunsForSha, getWorkflowRunJobsCount, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers, checkPreviousPRCommitsHadCI } = githubMergeLib;
|
|
37
37
|
|
|
38
38
|
// Issue #1625: Import centralized session-ending markers so the duplicate-
|
|
39
39
|
// search scope for checkForExistingComment() stays in lock-step with the
|
|
@@ -293,6 +293,55 @@ export const getMergeBlockers = async (owner, repo, prNumber, verbose = false, c
|
|
|
293
293
|
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true, workflowRunConclusions: conclusions };
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
// Issue #1690: Detect invalid workflow files (e.g. YAML/expression errors).
|
|
297
|
+
// When a workflow file fails to parse, GitHub creates a workflow_run with
|
|
298
|
+
// status=completed and conclusion=failure (or startup_failure / timed_out)
|
|
299
|
+
// but NEVER instantiates any jobs. Such runs will never produce check-runs,
|
|
300
|
+
// so the auto-merge loop would otherwise wait forever for "the genuine race
|
|
301
|
+
// condition" to resolve.
|
|
302
|
+
//
|
|
303
|
+
// Distinguish by querying the jobs API: real failures have jobs > 0 (and the
|
|
304
|
+
// failed jobs would already be visible as check-runs); invalid workflow files
|
|
305
|
+
// have jobs === 0. We only check failed/timed-out completed runs to keep the
|
|
306
|
+
// additional API calls bounded.
|
|
307
|
+
const failedCompletedRuns = workflowRuns.filter(r => r.status === 'completed' && (r.conclusion === 'failure' || r.conclusion === 'startup_failure' || r.conclusion === 'timed_out'));
|
|
308
|
+
if (failedCompletedRuns.length > 0) {
|
|
309
|
+
const invalidWorkflowRuns = [];
|
|
310
|
+
for (const run of failedCompletedRuns) {
|
|
311
|
+
const jobsCount = await getWorkflowRunJobsCount(owner, repo, run.id, verbose);
|
|
312
|
+
if (jobsCount === 0) {
|
|
313
|
+
invalidWorkflowRuns.push(run);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (invalidWorkflowRuns.length > 0) {
|
|
317
|
+
// Treat as a real CI failure so the auto-restart loop restarts the AI
|
|
318
|
+
// and propagates the error back instead of waiting forever.
|
|
319
|
+
if (verbose) {
|
|
320
|
+
await log(`[VERBOSE] /merge: PR #${prNumber} has ${invalidWorkflowRuns.length} workflow run(s) that completed with no jobs — workflow files likely invalid`);
|
|
321
|
+
for (const run of invalidWorkflowRuns) {
|
|
322
|
+
await log(`[VERBOSE] /merge: - ${run.name} (${run.id}): conclusion=${run.conclusion}, jobs=0, url=${run.html_url}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const failureLabels = invalidWorkflowRuns.map(r => `${r.path || r.name} (${r.conclusion})`);
|
|
326
|
+
await log(formatAligned('❌', 'Invalid workflow file(s):', failureLabels.join(', '), 2));
|
|
327
|
+
blockers.push({
|
|
328
|
+
type: 'ci_failure',
|
|
329
|
+
message: 'CI/CD workflow file is invalid — no jobs were instantiated',
|
|
330
|
+
details: invalidWorkflowRuns.map(r => `${r.path || r.name} — see ${r.html_url}`),
|
|
331
|
+
});
|
|
332
|
+
// Continue to the mergeability check below so other blockers are surfaced too.
|
|
333
|
+
const mergeStatus = await checkPRMergeable(owner, repo, prNumber, verbose);
|
|
334
|
+
if (!mergeStatus.mergeable) {
|
|
335
|
+
blockers.push({
|
|
336
|
+
type: 'not_mergeable',
|
|
337
|
+
message: mergeStatus.reason || 'PR is not mergeable',
|
|
338
|
+
details: [],
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: false };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
296
345
|
// Some workflow runs are still in progress or produced results — genuine race condition
|
|
297
346
|
if (verbose) {
|
|
298
347
|
await log(`[VERBOSE] /merge: PR #${prNumber} has no CI check-runs yet, but ${workflowRuns.length} workflow run(s) were triggered for SHA ${ciStatus.sha.substring(0, 7)} - genuine race condition (waiting for check-runs to appear)`);
|
|
@@ -450,6 +450,12 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
450
450
|
shouldRestart = true;
|
|
451
451
|
restartReason = restartReason ? `${restartReason}; CI failures` : 'CI failures detected';
|
|
452
452
|
feedbackLines.push('❌ CI/CD checks are failing:');
|
|
453
|
+
// Issue #1690: Surface the blocker message so AI sees structured failure context
|
|
454
|
+
// (e.g. "CI/CD workflow file is invalid — no jobs were instantiated") even when
|
|
455
|
+
// the failure didn't produce traditional check-runs.
|
|
456
|
+
if (ciBlocker.message && ciBlocker.message !== 'CI/CD checks are failing') {
|
|
457
|
+
feedbackLines.push(` ${ciBlocker.message}`);
|
|
458
|
+
}
|
|
453
459
|
for (const check of ciBlocker.details) {
|
|
454
460
|
feedbackLines.push(` - ${check}`);
|
|
455
461
|
}
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -379,8 +379,8 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
379
379
|
},
|
|
380
380
|
'tokens-budget-stats': {
|
|
381
381
|
type: 'boolean',
|
|
382
|
-
description: '
|
|
383
|
-
default:
|
|
382
|
+
description: 'Show detailed token budget statistics including context window usage and ratios (enabled by default, use --no-tokens-budget-stats to disable). Supported for --tool claude, --tool codex, and any tool that returns detailed token usage.',
|
|
383
|
+
default: true,
|
|
384
384
|
},
|
|
385
385
|
'prompt-issue-reporting': {
|
|
386
386
|
type: 'boolean',
|
|
@@ -464,13 +464,13 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
464
464
|
},
|
|
465
465
|
'auto-attach-solution-summary': {
|
|
466
466
|
type: 'boolean',
|
|
467
|
-
description: 'Automatically attach solution summary only if the AI did not create any comments during the session. This provides visible feedback when the AI completes silently.',
|
|
468
|
-
default:
|
|
467
|
+
description: 'Automatically attach solution summary only if the AI did not create any comments during the session. This provides visible feedback when the AI completes silently. Enabled by default; use --no-auto-attach-solution-summary to disable.',
|
|
468
|
+
default: true,
|
|
469
469
|
},
|
|
470
470
|
'auto-accept-invite': {
|
|
471
471
|
type: 'boolean',
|
|
472
|
-
description: 'Automatically accept the pending GitHub repository or organization invitation for the specific repository/organization being solved, before checking write access. Unlike /accept_invites which accepts all pending invitations, this only accepts the invite for the target repo/org.',
|
|
473
|
-
default:
|
|
472
|
+
description: 'Automatically accept the pending GitHub repository or organization invitation for the specific repository/organization being solved, before checking write access. Unlike /accept_invites which accepts all pending invitations, this only accepts the invite for the target repo/org. Enabled by default; use --no-auto-accept-invite to disable.',
|
|
473
|
+
default: true,
|
|
474
474
|
},
|
|
475
475
|
'prompt-ensure-all-requirements-are-met': {
|
|
476
476
|
type: 'boolean',
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -113,7 +113,7 @@ const config = yargs(hideBin(process.argv))
|
|
|
113
113
|
alias: 'v',
|
|
114
114
|
default: getenv('TELEGRAM_BOT_VERBOSE', 'false') === 'true',
|
|
115
115
|
})
|
|
116
|
-
.option('isolation', { type: 'string', description:
|
|
116
|
+
.option('isolation', { type: 'string', description: "Isolation backend (screen/tmux/docker). Defaults to 'screen' so Telegram-bot work sessions survive bot restarts; pass --isolation '' (or set TELEGRAM_ISOLATION='') to disable.", default: getenv('TELEGRAM_ISOLATION', 'screen') })
|
|
117
117
|
.help('h')
|
|
118
118
|
.alias('h', 'help')
|
|
119
119
|
.parserConfiguration({
|
|
@@ -955,16 +955,19 @@ async function handleSolveCommand(ctx) {
|
|
|
955
955
|
return;
|
|
956
956
|
}
|
|
957
957
|
// Validate merged arguments using solve's yargs config
|
|
958
|
+
let parsedSolveArgs;
|
|
958
959
|
try {
|
|
959
|
-
await parseArgsWithYargs(args, yargs, createSolveYargsConfig);
|
|
960
|
+
parsedSolveArgs = await parseArgsWithYargs(args, yargs, createSolveYargsConfig);
|
|
960
961
|
} catch (error) {
|
|
961
962
|
await safeReply(ctx, `❌ Invalid options: ${escapeMarkdown(error.message || String(error))}\n\nUse /help to see available options`, {
|
|
962
963
|
reply_to_message_id: ctx.message.message_id,
|
|
963
964
|
});
|
|
964
965
|
return;
|
|
965
966
|
}
|
|
966
|
-
// Issue #1552: Validate GitHub entity existence before queueing/executing
|
|
967
|
-
|
|
967
|
+
// Issue #1552 + #1694: Validate GitHub entity existence before queueing/executing.
|
|
968
|
+
// Honor the parsed --auto-accept-invite (now default-on per #1694), so --no-auto-accept-invite
|
|
969
|
+
// disables the pre-check while the default path still accepts pending invites for the target repo/org.
|
|
970
|
+
if (parsedSolveArgs?.autoAcceptInvite && validation.parsed.owner && validation.parsed.repo) {
|
|
968
971
|
try {
|
|
969
972
|
await (await import('./solve.accept-invite.lib.mjs')).autoAcceptInviteForRepo(validation.parsed.owner, validation.parsed.repo, async () => {}, false);
|
|
970
973
|
} catch (e) {
|