@link-assistant/hive-mind 1.56.15 → 1.56.17

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,18 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.56.17
4
+
5
+ ### Patch Changes
6
+
7
+ - b693172: Improve the repository-not-accessible error message in `/solve` (issue #1692). The headline drops the redundant "not found or" wording and the technical "(GitHub returns 404 for private repos without permissions)" parenthetical, leads with the most-actionable hypothesis ("Repository may be private — ensure the bot has been granted access"), and only suggests `--auto-accept-invite` when that flag is _not_ already active. The Telegram bot surface picks up the same suppression so users do not see the hint echoed back when they already passed the flag.
8
+
9
+ ## 1.56.16
10
+
11
+ ### Patch Changes
12
+
13
+ - 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/`.
14
+ - 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.
15
+
3
16
  ## 1.56.15
4
17
 
5
18
  ### 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.17",
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",
@@ -21,13 +21,16 @@ import { ghPrView, ghIssueView } from './github.lib.mjs';
21
21
  * @param {number|string} [options.number] - Issue or PR number (if applicable)
22
22
  * @param {string} [options.type] - URL type: 'issue' or 'pull'
23
23
  * @param {boolean} [options.verbose=false] - Whether verbose logging is enabled
24
+ * @param {boolean} [options.autoAcceptInvite=false] - Whether the caller already passed
25
+ * `--auto-accept-invite`. When true, the repo-404 message omits the suggestion to
26
+ * use that flag, since it would not be actionable (issue #1692).
24
27
  * @returns {Promise<{valid: boolean, error?: string, level?: string, details?: string}>}
25
28
  * - valid: true if all entities exist and are accessible
26
29
  * - error: user-facing error message (when valid=false)
27
30
  * - level: which entity level failed ('user', 'repo', 'issue', 'pull')
28
31
  * - details: additional context for verbose logging
29
32
  */
30
- export async function validateGitHubEntityExistence({ owner, repo, number, type, verbose = false }) {
33
+ export async function validateGitHubEntityExistence({ owner, repo, number, type, verbose = false, autoAcceptInvite = false }) {
31
34
  // Step 1: Check user/organization existence
32
35
  try {
33
36
  const userResult = await ghCmdRetry(() => $`gh api users/${owner} --jq .login`, { label: `check user ${owner}` });
@@ -53,9 +56,13 @@ export async function validateGitHubEntityExistence({ owner, repo, number, type,
53
56
  if (repoResult.code !== 0) {
54
57
  const errorOutput = (repoResult.stderr ? repoResult.stderr.toString() : '') + (repoResult.stdout ? repoResult.stdout.toString() : '');
55
58
  if (errorOutput.includes('404') || errorOutput.includes('Not Found')) {
59
+ const bullets = ['• Repository may be private — ensure the bot has been granted access', '• The repository name is spelled correctly', '• The repository has not been deleted, transferred, or never existed'];
60
+ if (!autoAcceptInvite) {
61
+ bullets.push('• If Hive Mind bot was recently invited, try using --auto-accept-invite to accept pending invitations');
62
+ }
56
63
  return {
57
64
  valid: false,
58
- error: `Repository '${owner}/${repo}' not found or not accessible.\n\n💡 Please check:\n• The repository name is spelled correctly\n• If it's a private repository, ensure the bot has been granted access (GitHub returns 404 for private repos without permissions)\n• The repository has not been deleted or transferred\n• If you were recently invited, try using --auto-accept-invite to accept pending invitations`,
65
+ error: `Repository '${owner}/${repo}' is not accessible.\n\n💡 Please check:\n${bullets.join('\n')}`,
59
66
  level: 'repo',
60
67
  };
61
68
  }
@@ -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',
package/src/solve.mjs CHANGED
@@ -290,7 +290,7 @@ if (!hasWriteAccess) {
290
290
  }
291
291
 
292
292
  // Issue #1552: Validate entity existence AFTER permissions (cascade: user/org → repo → issue/PR)
293
- const entityCheck = await (await import('./github-entity-validation.lib.mjs')).validateGitHubEntityExistence({ owner, repo, number: urlNumber, type: isIssueUrl ? 'issue' : isPrUrl ? 'pull' : undefined, verbose: argv.verbose });
293
+ const entityCheck = await (await import('./github-entity-validation.lib.mjs')).validateGitHubEntityExistence({ owner, repo, number: urlNumber, type: isIssueUrl ? 'issue' : isPrUrl ? 'pull' : undefined, verbose: argv.verbose, autoAcceptInvite: !!argv.autoAcceptInvite });
294
294
  if (!entityCheck.valid) {
295
295
  await log(`\n❌ ${entityCheck.error}\n`, { level: 'error' });
296
296
  await safeExit(1, `GitHub entity not found (${entityCheck.level})`);
@@ -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,23 +955,26 @@ 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) {
971
974
  VERBOSE && console.log(`[VERBOSE] Auto-accept invite pre-check failed: ${e.message}`);
972
975
  }
973
976
  }
974
- const entityCheck = await validateGitHubEntityExistence({ owner: validation.parsed.owner, repo: validation.parsed.repo, number: validation.parsed.number, type: validation.parsed.type, verbose: VERBOSE });
977
+ const entityCheck = await validateGitHubEntityExistence({ owner: validation.parsed.owner, repo: validation.parsed.repo, number: validation.parsed.number, type: validation.parsed.type, verbose: VERBOSE, autoAcceptInvite: args.some(a => a === '--auto-accept-invite') });
975
978
  if (!entityCheck.valid) {
976
979
  await safeReply(ctx, `❌ ${escapeMarkdown(entityCheck.error)}`, { reply_to_message_id: ctx.message.message_id });
977
980
  return;