@link-assistant/hive-mind 1.56.14 → 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,18 @@
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
+
10
+ ## 1.56.15
11
+
12
+ ### Patch Changes
13
+
14
+ - cdd8010: Refine the Telegram bot work-session messages: introduce `šŸ”„ Starting...` and `ā³ Executing...` to distinguish launch from execution, change the completion headline to `āœ… Work session finished successfully` / `āŒ Work session failed (exit code: N)`, show duration before session, and preserve the audit infoBlock (`Requested by`, `URL`, `šŸ›  Options`, `šŸ”’ Locked options`) on every state — including completion and failure paths — so admins keep a record even when users delete their original `/solve` message.
15
+
3
16
  ## 1.56.14
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.14",
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-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,
@@ -257,6 +257,7 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
257
257
  statusResult,
258
258
  observedEndTime: new Date(),
259
259
  exitCode: finalExitCode,
260
+ infoBlock: sessionInfo?.infoBlock || '',
260
261
  });
261
262
 
262
263
  // Update the original reply message if messageId is available, otherwise send new message
@@ -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',
@@ -51,7 +51,7 @@ const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STO
51
51
  const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
52
52
  const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
53
53
  const { trackSession, startSessionMonitoring, hasActiveSessionForUrlAsync } = await import('./session-monitor.lib.mjs');
54
- const { formatExecutingWorkSessionMessage } = await import('./work-session-formatting.lib.mjs');
54
+ const { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } = await import('./work-session-formatting.lib.mjs');
55
55
 
56
56
  const config = yargs(hideBin(process.argv))
57
57
  .usage('Usage: hive-telegram-bot [options]')
@@ -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({
@@ -566,7 +566,7 @@ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args,
566
566
  session = iso.runner.generateSessionId();
567
567
  VERBOSE && console.log(`[VERBOSE] Using isolation (${iso.backend}), session: ${session}`);
568
568
  result = await iso.runner.executeWithIsolation(commandName, args, { backend: iso.backend, sessionId: session, verbose: VERBOSE });
569
- if (result.success) trackSession(session, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, isolationBackend: iso.backend, sessionId: session, tool }, VERBOSE);
569
+ if (result.success) trackSession(session, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, isolationBackend: iso.backend, sessionId: session, tool, infoBlock }, VERBOSE);
570
570
  } else {
571
571
  result = await executeStartScreen(commandName, args);
572
572
  const match = result.success && (result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -R\s+(\S+)/));
@@ -575,19 +575,18 @@ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args,
575
575
  // These sessions cannot reliably detect completion (screen stays alive via
576
576
  // `exec bash`), so active URL checks auto-expire them after 10 min.
577
577
  // This prevents accidental duplicate commands within the timeout window.
578
- if (result.success && session !== 'unknown') trackSession(session, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, tool }, VERBOSE);
578
+ if (result.success && session !== 'unknown') trackSession(session, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, tool, infoBlock }, VERBOSE);
579
579
  }
580
580
  if (result.warning) return safeEdit(`āš ļø ${result.warning}`);
581
581
  if (result.success) {
582
582
  await safeEdit(
583
583
  formatExecutingWorkSessionMessage({
584
- commandName,
585
584
  sessionName: session,
586
585
  isolationBackend: iso?.backend || null,
587
586
  infoBlock,
588
587
  })
589
588
  );
590
- } else await safeEdit(`āŒ Error executing ${commandName} command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\``);
589
+ } else await safeEdit(`āŒ Error executing ${commandName} command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\`\n\n${infoBlock}`);
591
590
  }
592
591
 
593
592
  bot.command('help', async ctx => {
@@ -956,16 +955,19 @@ async function handleSolveCommand(ctx) {
956
955
  return;
957
956
  }
958
957
  // Validate merged arguments using solve's yargs config
958
+ let parsedSolveArgs;
959
959
  try {
960
- await parseArgsWithYargs(args, yargs, createSolveYargsConfig);
960
+ parsedSolveArgs = await parseArgsWithYargs(args, yargs, createSolveYargsConfig);
961
961
  } catch (error) {
962
962
  await safeReply(ctx, `āŒ Invalid options: ${escapeMarkdown(error.message || String(error))}\n\nUse /help to see available options`, {
963
963
  reply_to_message_id: ctx.message.message_id,
964
964
  });
965
965
  return;
966
966
  }
967
- // Issue #1552: Validate GitHub entity existence before queueing/executing
968
- 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) {
969
971
  try {
970
972
  await (await import('./solve.accept-invite.lib.mjs')).autoAcceptInviteForRepo(validation.parsed.owner, validation.parsed.repo, async () => {}, false);
971
973
  } catch (e) {
@@ -1012,7 +1014,7 @@ async function handleSolveCommand(ctx) {
1012
1014
 
1013
1015
  const toolQueuedCount = queueStats.queuedByTool[solveTool] || 0; // tool-specific queue count (#1551)
1014
1016
  if (check.canStart && toolQueuedCount === 0) {
1015
- const startingMessage = await safeReply(ctx, `šŸš€ Starting solve command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
1017
+ const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock }), { reply_to_message_id: ctx.message.message_id });
1016
1018
  await executeAndUpdateMessage(ctx, startingMessage, 'solve', args, infoBlock, effectiveSolveIsolation, solveTool);
1017
1019
  } else {
1018
1020
  const queueItem = solveQueue.enqueue({ url: normalizedUrl, args, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: effectiveSolveIsolation });
@@ -1177,7 +1179,7 @@ async function handleHiveCommand(ctx) {
1177
1179
  infoBlock += `${userOptionsRaw ? '\n' : '\n\n'}šŸ”’ Locked options: ${escapeMarkdown(hiveOverrides.join(' '))}`;
1178
1180
  }
1179
1181
 
1180
- const startingMessage = await safeReply(ctx, `šŸš€ Starting hive command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
1182
+ const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock }), { reply_to_message_id: ctx.message.message_id });
1181
1183
  await executeAndUpdateMessage(ctx, startingMessage, 'hive', args, infoBlock, effectiveHiveIsolation, hiveTool);
1182
1184
  }
1183
1185
 
@@ -80,7 +80,7 @@ export function createIsolationAwareQueueCallback(botIsolationBackend, botIsolat
80
80
  if (iso) {
81
81
  const sid = iso.runner.generateSessionId();
82
82
  const r = await iso.runner.executeWithIsolation(item.command || 'solve', item.args, { backend: iso.backend, sessionId: sid, verbose });
83
- if (r.success) trackSession(sid, { chatId: item.ctx?.chat?.id, messageId: item.messageInfo?.messageId, startTime: new Date(), url: item.url, command: item.command || 'solve', isolationBackend: iso.backend, sessionId: sid, tool: item.tool || 'claude' }, verbose);
83
+ if (r.success) trackSession(sid, { chatId: item.ctx?.chat?.id, messageId: item.messageInfo?.messageId, startTime: new Date(), url: item.url, command: item.command || 'solve', isolationBackend: iso.backend, sessionId: sid, tool: item.tool || 'claude', infoBlock: item.infoBlock }, verbose);
84
84
  return { ...r, sessionId: sid, isolationBackend: iso.backend, output: r.output || `session: ${sid}` };
85
85
  }
86
86
  return fallbackCallback(item);
@@ -20,7 +20,7 @@ export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, ge
20
20
  import { formatDuration, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningProcesses } from './telegram-solve-queue.helpers.lib.mjs';
21
21
  export { QUEUE_CONFIG, THRESHOLD_STRATEGIES } from './queue-config.lib.mjs';
22
22
  import { QUEUE_CONFIG } from './queue-config.lib.mjs';
23
- import { formatExecutingWorkSessionMessage } from './work-session-formatting.lib.mjs';
23
+ import { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
24
24
 
25
25
  export const QueueItemStatus = {
26
26
  QUEUED: 'queued',
@@ -1058,7 +1058,7 @@ export class SolveQueue {
1058
1058
  this.stats.totalStarted++;
1059
1059
 
1060
1060
  // Update message to show Starting status
1061
- await this.updateItemMessage(item, `šŸš€ Starting solve command...\n\n${item.infoBlock}`);
1061
+ await this.updateItemMessage(item, formatStartingWorkSessionMessage({ infoBlock: item.infoBlock }));
1062
1062
 
1063
1063
  this.log(`Starting: ${item.toString()} from ${tool} queue`);
1064
1064
 
@@ -1144,17 +1144,16 @@ export class SolveQueue {
1144
1144
  if (chatId && messageId) {
1145
1145
  try {
1146
1146
  if (result.warning) {
1147
- await item.ctx.telegram.editMessageText(chatId, messageId, undefined, `āš ļø ${result.warning}`, { parse_mode: 'Markdown' });
1147
+ await item.ctx.telegram.editMessageText(chatId, messageId, undefined, `āš ļø ${result.warning}\n\n${item.infoBlock}`, { parse_mode: 'Markdown' });
1148
1148
  } else if (result.success) {
1149
1149
  const response = formatExecutingWorkSessionMessage({
1150
- commandName: item.command || 'solve',
1151
1150
  sessionName,
1152
1151
  isolationBackend: result.isolationBackend,
1153
1152
  infoBlock: item.infoBlock,
1154
1153
  });
1155
1154
  await item.ctx.telegram.editMessageText(chatId, messageId, undefined, response, { parse_mode: 'Markdown' });
1156
1155
  } else {
1157
- const response = `āŒ Error executing solve command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\``;
1156
+ const response = `āŒ Error executing solve command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\`\n\n${item.infoBlock}`;
1158
1157
  await item.ctx.telegram.editMessageText(chatId, messageId, undefined, response, { parse_mode: 'Markdown' });
1159
1158
  }
1160
1159
  } catch (error) {
@@ -1177,7 +1176,8 @@ export class SolveQueue {
1177
1176
  const { chatId, messageId } = item.messageInfo || {};
1178
1177
  if (chatId && messageId && item.ctx) {
1179
1178
  try {
1180
- await item.ctx.telegram.editMessageText(chatId, messageId, undefined, `āŒ Error: ${error.message}`, { parse_mode: 'Markdown' });
1179
+ const errorText = item.infoBlock ? `āŒ Error: ${error.message}\n\n${item.infoBlock}` : `āŒ Error: ${error.message}`;
1180
+ await item.ctx.telegram.editMessageText(chatId, messageId, undefined, errorText, { parse_mode: 'Markdown' });
1181
1181
  } catch (editError) {
1182
1182
  // Log the edit failure for debugging
1183
1183
  // See: https://github.com/link-assistant/hive-mind/issues/1062
@@ -1361,7 +1361,7 @@ export function createQueueExecuteCallback(executeStartScreen, trackSessionFn) {
1361
1361
  const match = result.output && (result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -R\s+(\S+)/));
1362
1362
  const session = match ? match[1] : null;
1363
1363
  if (session) {
1364
- trackSessionFn(session, { chatId: item.ctx?.chat?.id, messageId: item.messageInfo?.messageId, startTime: new Date(), url: item.url, command: 'solve', tool: item.tool || 'claude' });
1364
+ trackSessionFn(session, { chatId: item.ctx?.chat?.id, messageId: item.messageInfo?.messageId, startTime: new Date(), url: item.url, command: 'solve', tool: item.tool || 'claude', infoBlock: item.infoBlock });
1365
1365
  }
1366
1366
  }
1367
1367
  return result;
@@ -1,10 +1,5 @@
1
1
  const FAILURE_STATUSES = new Set(['failed', 'cancelled', 'canceled', 'error']);
2
2
 
3
- function capitalizeCommandName(commandName) {
4
- const normalized = commandName || 'solve';
5
- return normalized.charAt(0).toUpperCase() + normalized.slice(1);
6
- }
7
-
8
3
  function parseDateValue(value) {
9
4
  if (!value) return null;
10
5
  const date = value instanceof Date ? value : new Date(value);
@@ -46,27 +41,32 @@ export function formatSessionDurationSeconds(seconds) {
46
41
  return parts.join(' ');
47
42
  }
48
43
 
49
- export function formatExecutingWorkSessionMessage({ commandName = 'solve', sessionName = 'unknown', isolationBackend = null, infoBlock = '' } = {}) {
44
+ export function formatStartingWorkSessionMessage({ infoBlock = '' } = {}) {
45
+ const details = infoBlock ? `\n\n${infoBlock}` : '';
46
+ return `šŸ”„ Starting...${details}`;
47
+ }
48
+
49
+ export function formatExecutingWorkSessionMessage({ sessionName = 'unknown', isolationBackend = null, infoBlock = '' } = {}) {
50
50
  const isolationInfo = isolationBackend ? `\nšŸ”’ Isolation: \`${isolationBackend}\`` : '';
51
51
  const details = infoBlock ? `\n\n${infoBlock}` : '';
52
- return `ā³ ${capitalizeCommandName(commandName)} command executing...\n\nšŸ“Š Session: \`${sessionName}\`${isolationInfo}${details}`;
52
+ return `ā³ Executing...\n\nšŸ“Š Session: \`${sessionName}\`${isolationInfo}${details}`;
53
53
  }
54
54
 
55
- export function formatSessionCompletionMessage({ sessionName, sessionInfo, statusResult = null, observedEndTime = new Date(), exitCode = null } = {}) {
55
+ export function formatSessionCompletionMessage({ sessionName, sessionInfo, statusResult = null, observedEndTime = new Date(), exitCode = null, infoBlock = '' } = {}) {
56
56
  const finalExitCode = getSessionCompletionExitCode({ exitCode, statusResult });
57
57
  const failed = finalExitCode !== null && finalExitCode !== 0;
58
58
  const statusEmoji = failed ? 'āŒ' : 'āœ…';
59
- const statusText = failed ? `Failed (exit code: ${finalExitCode})` : 'Completed';
60
- const isolationInfo = sessionInfo?.isolationBackend ? `\nšŸ”’ Isolation: ${sessionInfo.isolationBackend}` : '';
59
+ const statusText = failed ? `Work session failed (exit code: ${finalExitCode})` : 'Work session finished successfully';
60
+ const isolationInfo = sessionInfo?.isolationBackend ? `\nšŸ”’ Isolation: \`${sessionInfo.isolationBackend}\`` : '';
61
61
  const startTime = parseDateValue(statusResult?.startTime) || parseDateValue(sessionInfo?.startTime) || observedEndTime;
62
62
  const endTime = parseDateValue(statusResult?.endTime) || observedEndTime;
63
63
  const durationSeconds = Math.max(0, (endTime.getTime() - startTime.getTime()) / 1000);
64
+ const resolvedInfoBlock = infoBlock || sessionInfo?.infoBlock || '';
65
+ const details = resolvedInfoBlock ? `\n\n${resolvedInfoBlock}` : '';
64
66
 
65
- let message = `${statusEmoji} *Work Session ${statusText}*\n\n`;
66
- message += `šŸ“Š Session: \`${sessionName || 'unknown'}\`\n`;
67
+ let message = `${statusEmoji} *${statusText}*\n\n`;
67
68
  message += `ā±ļø Duration: ${formatSessionDurationSeconds(durationSeconds)}\n`;
68
- message += `šŸ”— URL: ${sessionInfo?.url || 'unknown'}${isolationInfo}\n\n`;
69
- message += 'The work session has finished. You can now review the results.';
69
+ message += `šŸ“Š Session: \`${sessionName || 'unknown'}\`${isolationInfo}${details}`;
70
70
 
71
71
  return message;
72
72
  }