@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 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.15",
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
+ }
@@ -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
- * Get the committed date of a specific commit from GitHub API
1286
- * Issue #1480: Used to determine how recently a commit was pushed, to distinguish between
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
  }
@@ -379,8 +379,8 @@ export const SOLVE_OPTION_DEFINITIONS = {
379
379
  },
380
380
  'tokens-budget-stats': {
381
381
  type: 'boolean',
382
- description: '[EXPERIMENTAL] Show detailed token budget statistics including context window usage and ratios. Supported for --tool claude, --tool codex, and any tool that returns detailed token usage.',
383
- default: false,
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: false,
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: false,
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',
@@ -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: 'Experimental: isolation backend (screen/tmux/docker)', default: getenv('TELEGRAM_ISOLATION', '') })
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
- if (args.some(a => a === '--auto-accept-invite') && validation.parsed.owner && validation.parsed.repo) {
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) {