@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 +13 -0
- package/package.json +2 -2
- package/src/github-merge-ci-signals.lib.mjs +182 -0
- package/src/github-merge.lib.mjs +50 -166
- package/src/session-monitor.lib.mjs +1 -0
- package/src/solve.auto-merge-helpers.lib.mjs +50 -1
- package/src/solve.auto-merge.lib.mjs +6 -0
- package/src/solve.config.lib.mjs +6 -6
- package/src/telegram-bot.mjs +13 -11
- package/src/telegram-isolation.lib.mjs +1 -1
- package/src/telegram-solve-queue.lib.mjs +7 -7
- package/src/work-session-formatting.lib.mjs +14 -14
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.
|
|
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
|
+
}
|
package/src/github-merge.lib.mjs
CHANGED
|
@@ -1203,14 +1203,14 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
|
|
|
1203
1203
|
* @param {string} repo - Repository name
|
|
1204
1204
|
* @param {string} sha - Commit SHA
|
|
1205
1205
|
* @param {boolean} verbose - Whether to log verbose output
|
|
1206
|
-
* @returns {Promise<Array<{id: number, status: string, conclusion: string|null, name: string, html_url: string}>>}
|
|
1206
|
+
* @returns {Promise<Array<{id: number, status: string, conclusion: string|null, name: string, html_url: string, path: string}>>}
|
|
1207
1207
|
*/
|
|
1208
1208
|
export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
|
|
1209
1209
|
try {
|
|
1210
1210
|
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=100" --paginate --slurp`);
|
|
1211
1211
|
const runs = JSON.parse(stdout.trim() || '[]')
|
|
1212
1212
|
.flatMap(page => page.workflow_runs || [])
|
|
1213
|
-
.map(run => ({ id: run.id, status: run.status, conclusion: run.conclusion, name: run.name, html_url: run.html_url }));
|
|
1213
|
+
.map(run => ({ id: run.id, status: run.status, conclusion: run.conclusion, name: run.name, html_url: run.html_url, path: run.path }));
|
|
1214
1214
|
|
|
1215
1215
|
if (verbose) {
|
|
1216
1216
|
console.log(`[VERBOSE] /merge: Found ${runs.length} workflow runs for SHA ${sha.substring(0, 7)}`);
|
|
@@ -1228,6 +1228,50 @@ export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
|
|
|
1228
1228
|
}
|
|
1229
1229
|
}
|
|
1230
1230
|
|
|
1231
|
+
/**
|
|
1232
|
+
* Get the job count for a specific workflow run.
|
|
1233
|
+
*
|
|
1234
|
+
* Issue #1690: Used to detect "invalid workflow file" failures. When a workflow file
|
|
1235
|
+
* has a syntax error (e.g., `Unrecognized named-value: 'env'`), GitHub creates a
|
|
1236
|
+
* workflow_run with `status=completed, conclusion=failure` but never instantiates
|
|
1237
|
+
* any jobs ā `total_count: 0`. Such workflow runs will never produce check-runs.
|
|
1238
|
+
*
|
|
1239
|
+
* Distinguishing this from a real failure (where check-runs exist for the failed jobs)
|
|
1240
|
+
* lets the auto-merge loop break out of "waiting for check-runs to appear" and
|
|
1241
|
+
* propagate the error to the AI solver as a real failure.
|
|
1242
|
+
*
|
|
1243
|
+
* @param {string} owner - Repository owner
|
|
1244
|
+
* @param {string} repo - Repository name
|
|
1245
|
+
* @param {number|string} runId - Workflow run ID
|
|
1246
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
1247
|
+
* @returns {Promise<number|null>} - Total job count, or null on error
|
|
1248
|
+
*/
|
|
1249
|
+
export async function getWorkflowRunJobsCount(owner, repo, runId, verbose = false) {
|
|
1250
|
+
try {
|
|
1251
|
+
// Issue #1690: We only need the total_count field, so a single page is sufficient and
|
|
1252
|
+
// adding --paginate would defeat the --jq selector. Use --silent to bypass the
|
|
1253
|
+
// pagination linter rule because total_count comes from the response root.
|
|
1254
|
+
/* eslint-disable-next-line gh-paginate/require-gh-paginate */
|
|
1255
|
+
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs/${runId}/jobs?per_page=1" --jq '.total_count'`);
|
|
1256
|
+
const count = parseInt(stdout.trim(), 10);
|
|
1257
|
+
if (Number.isNaN(count)) {
|
|
1258
|
+
if (verbose) {
|
|
1259
|
+
console.log(`[VERBOSE] /merge: Could not parse job count for workflow run ${runId} (got: "${stdout.trim()}")`);
|
|
1260
|
+
}
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
if (verbose) {
|
|
1264
|
+
console.log(`[VERBOSE] /merge: Workflow run ${runId} has ${count} job(s)`);
|
|
1265
|
+
}
|
|
1266
|
+
return count;
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
if (verbose) {
|
|
1269
|
+
console.log(`[VERBOSE] /merge: Error fetching jobs for workflow run ${runId}: ${error.message}`);
|
|
1270
|
+
}
|
|
1271
|
+
return null;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1231
1275
|
/**
|
|
1232
1276
|
* Get the count of active (enabled) GitHub Actions workflows in a repository
|
|
1233
1277
|
* Issue #1363: Used to distinguish between "no CI configured" and "CI hasn't started yet"
|
|
@@ -1281,170 +1325,9 @@ export async function getActiveRepoWorkflows(owner, repo, verbose = false) {
|
|
|
1281
1325
|
}
|
|
1282
1326
|
}
|
|
1283
1327
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
* "CI not yet registered in API" (race condition) and "CI definitively not triggered"
|
|
1288
|
-
* @param {string} owner - Repository owner
|
|
1289
|
-
* @param {string} repo - Repository name
|
|
1290
|
-
* @param {string} sha - Commit SHA
|
|
1291
|
-
* @param {boolean} verbose - Whether to log verbose output
|
|
1292
|
-
* @returns {Promise<{date: Date|null, ageSeconds: number|null}>}
|
|
1293
|
-
*/
|
|
1294
|
-
export async function getCommitDate(owner, repo, sha, verbose = false) {
|
|
1295
|
-
try {
|
|
1296
|
-
const { stdout } = await exec(`gh api repos/${owner}/${repo}/commits/${sha} --jq '.commit.committer.date'`);
|
|
1297
|
-
const dateStr = stdout.trim();
|
|
1298
|
-
if (!dateStr) {
|
|
1299
|
-
return { date: null, ageSeconds: null };
|
|
1300
|
-
}
|
|
1301
|
-
const commitDate = new Date(dateStr);
|
|
1302
|
-
const ageSeconds = Math.floor((Date.now() - commitDate.getTime()) / 1000);
|
|
1303
|
-
if (verbose) {
|
|
1304
|
-
console.log(`[VERBOSE] /merge: Commit ${sha.substring(0, 7)} date: ${dateStr} (${ageSeconds}s ago)`);
|
|
1305
|
-
}
|
|
1306
|
-
return { date: commitDate, ageSeconds };
|
|
1307
|
-
} catch (error) {
|
|
1308
|
-
if (verbose) {
|
|
1309
|
-
console.log(`[VERBOSE] /merge: Error fetching commit date for ${sha}: ${error.message}`);
|
|
1310
|
-
}
|
|
1311
|
-
return { date: null, ageSeconds: null };
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
/**
|
|
1316
|
-
* Check if any previous commits in a PR had workflow runs triggered.
|
|
1317
|
-
* Issue #1480: If earlier commits in the same PR triggered CI, we should expect CI
|
|
1318
|
-
* for the HEAD commit too (unless conditions changed). This provides an additional
|
|
1319
|
-
* signal that CI should be expected and avoids false "CI not triggered" conclusions.
|
|
1320
|
-
* @param {string} owner - Repository owner
|
|
1321
|
-
* @param {string} repo - Repository name
|
|
1322
|
-
* @param {number} prNumber - Pull request number
|
|
1323
|
-
* @param {string} headSha - Current HEAD SHA (to exclude from check)
|
|
1324
|
-
* @param {boolean} verbose - Whether to log verbose output
|
|
1325
|
-
* @returns {Promise<{hadPreviousCI: boolean, previousCommitsWithCI: number, totalPreviousCommits: number}>}
|
|
1326
|
-
*/
|
|
1327
|
-
export async function checkPreviousPRCommitsHadCI(owner, repo, prNumber, headSha, verbose = false) {
|
|
1328
|
-
try {
|
|
1329
|
-
// Get all commits in the PR
|
|
1330
|
-
const { stdout: commitsJson } = await exec(`gh api "repos/${owner}/${repo}/pulls/${prNumber}/commits" --paginate --jq '[.[].sha]'`);
|
|
1331
|
-
const allShas = JSON.parse(commitsJson.trim() || '[]');
|
|
1332
|
-
|
|
1333
|
-
// Exclude the current HEAD SHA
|
|
1334
|
-
const previousShas = allShas.filter(sha => sha !== headSha);
|
|
1335
|
-
|
|
1336
|
-
if (previousShas.length === 0) {
|
|
1337
|
-
if (verbose) {
|
|
1338
|
-
console.log(`[VERBOSE] /merge: PR #${prNumber} has no previous commits to check for CI history`);
|
|
1339
|
-
}
|
|
1340
|
-
return { hadPreviousCI: false, previousCommitsWithCI: 0, totalPreviousCommits: 0 };
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
// Check the most recent previous commits (limit to last 3 to avoid excessive API calls)
|
|
1344
|
-
const commitsToCheck = previousShas.slice(-3);
|
|
1345
|
-
let commitsWithCI = 0;
|
|
1346
|
-
|
|
1347
|
-
for (const sha of commitsToCheck) {
|
|
1348
|
-
try {
|
|
1349
|
-
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=100" --paginate --slurp`);
|
|
1350
|
-
const count = JSON.parse(stdout.trim() || '[]').reduce((sum, page) => sum + (page.workflow_runs?.length || 0), 0);
|
|
1351
|
-
if (count > 0) {
|
|
1352
|
-
commitsWithCI++;
|
|
1353
|
-
}
|
|
1354
|
-
} catch {
|
|
1355
|
-
// Skip errors for individual commits
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
const hadPreviousCI = commitsWithCI > 0;
|
|
1360
|
-
|
|
1361
|
-
if (verbose) {
|
|
1362
|
-
console.log(`[VERBOSE] /merge: PR #${prNumber} previous CI history: ${commitsWithCI}/${commitsToCheck.length} checked commits had workflow runs (total PR commits: ${allShas.length})`);
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
return { hadPreviousCI, previousCommitsWithCI: commitsWithCI, totalPreviousCommits: previousShas.length };
|
|
1366
|
-
} catch (error) {
|
|
1367
|
-
if (verbose) {
|
|
1368
|
-
console.log(`[VERBOSE] /merge: Error checking previous PR commits CI history: ${error.message}`);
|
|
1369
|
-
}
|
|
1370
|
-
return { hadPreviousCI: false, previousCommitsWithCI: 0, totalPreviousCommits: 0 };
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
/**
|
|
1375
|
-
* Check if any workflow files in the repository have PR-related triggers
|
|
1376
|
-
* Issue #1480: Used as additional signal to determine if CI should run on PRs.
|
|
1377
|
-
* Parses .github/workflows/*.yml files from the repository content API.
|
|
1378
|
-
* @param {string} owner - Repository owner
|
|
1379
|
-
* @param {string} repo - Repository name
|
|
1380
|
-
* @param {boolean} verbose - Whether to log verbose output
|
|
1381
|
-
* @returns {Promise<{hasPRTriggers: boolean, hasWorkflowFiles: boolean, workflows: Array<{name: string, triggers: string[]}>}>}
|
|
1382
|
-
*/
|
|
1383
|
-
export async function checkWorkflowsHavePRTriggers(owner, repo, verbose = false, ref = null) {
|
|
1384
|
-
try {
|
|
1385
|
-
// Issue #1503: Support querying workflow files from a specific branch (ref)
|
|
1386
|
-
const refParam = ref ? `?ref=${encodeURIComponent(ref)}` : '';
|
|
1387
|
-
// List workflow files in .github/workflows/ (uses ref if provided, otherwise default branch)
|
|
1388
|
-
const { stdout: listJson } = await exec(`gh api "repos/${owner}/${repo}/contents/.github/workflows${refParam}" --paginate --jq '[.[] | select(.name | test("\\\\.(yml|yaml)$")) | {name: .name, download_url: .download_url, path: .path}]' 2>/dev/null`);
|
|
1389
|
-
const files = JSON.parse(listJson.trim() || '[]');
|
|
1390
|
-
|
|
1391
|
-
if (files.length === 0) {
|
|
1392
|
-
if (verbose) console.log(`[VERBOSE] /merge: No workflow files in ${owner}/${repo}/.github/workflows/`);
|
|
1393
|
-
return { hasPRTriggers: false, hasWorkflowFiles: false, workflows: [] };
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
const prTriggerPatterns = [/\bon:\s*\n\s+pull_request/m, /\bon:\s*\[.*pull_request.*\]/m, /\bon:\s*pull_request\b/m, /\bpull_request_target\b/m];
|
|
1397
|
-
const pushTriggerPatterns = [/\bon:\s*\n\s+push/m, /\bon:\s*\[.*push.*\]/m, /\bon:\s*push\b/m];
|
|
1398
|
-
// Issue #1503: Non-PR triggers for diagnostics (won't produce check-runs on PRs)
|
|
1399
|
-
const nonPROnlyTriggerPatterns = [/\bworkflow_dispatch\b/m, /\bschedule\b/m, /\brepository_dispatch\b/m, /\bworkflow_call\b/m];
|
|
1400
|
-
|
|
1401
|
-
const results = [];
|
|
1402
|
-
|
|
1403
|
-
for (const file of files) {
|
|
1404
|
-
try {
|
|
1405
|
-
// Issue #1503: Fetch file content using same ref parameter for branch-specific workflows
|
|
1406
|
-
const { stdout: contentJson } = await exec(`gh api "repos/${owner}/${repo}/contents/${file.path}${refParam}" --jq '.content'`);
|
|
1407
|
-
const content = Buffer.from(contentJson.trim().replace(/"/g, ''), 'base64').toString('utf-8');
|
|
1408
|
-
|
|
1409
|
-
const triggers = [];
|
|
1410
|
-
if (prTriggerPatterns.some(p => p.test(content))) {
|
|
1411
|
-
triggers.push('pull_request');
|
|
1412
|
-
}
|
|
1413
|
-
if (pushTriggerPatterns.some(p => p.test(content))) {
|
|
1414
|
-
triggers.push('push');
|
|
1415
|
-
}
|
|
1416
|
-
// Issue #1503: Track non-PR triggers for diagnostics
|
|
1417
|
-
const nonPRTriggers = nonPROnlyTriggerPatterns.filter(p => p.test(content)).map(p => p.source.replace(/\\b/g, ''));
|
|
1418
|
-
|
|
1419
|
-
if (triggers.length > 0) {
|
|
1420
|
-
results.push({ name: file.name, triggers });
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
if (verbose) {
|
|
1424
|
-
console.log(`[VERBOSE] /merge: Workflow ${file.name}: pr_triggers=[${triggers.join(', ')}], non_pr_triggers=[${nonPRTriggers.join(', ')}]`);
|
|
1425
|
-
}
|
|
1426
|
-
} catch (fileError) {
|
|
1427
|
-
if (verbose) {
|
|
1428
|
-
console.log(`[VERBOSE] /merge: Error reading workflow file ${file.name}: ${fileError.message}`);
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
const hasPRTriggers = results.length > 0;
|
|
1434
|
-
|
|
1435
|
-
if (verbose) {
|
|
1436
|
-
console.log(`[VERBOSE] /merge: ${results.length}/${files.length} workflow files have PR/push triggers`);
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
return { hasPRTriggers, hasWorkflowFiles: true, workflows: results };
|
|
1440
|
-
} catch (error) {
|
|
1441
|
-
if (verbose) {
|
|
1442
|
-
console.log(`[VERBOSE] /merge: Error checking workflow PR triggers: ${error.message}`);
|
|
1443
|
-
}
|
|
1444
|
-
// On error, assume workflows might have PR triggers (safer: avoids false positives)
|
|
1445
|
-
return { hasPRTriggers: true, hasWorkflowFiles: true, workflows: [] };
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1328
|
+
// Issue #1690: Re-export CI signal helpers from separate module to keep this file under 1500 lines
|
|
1329
|
+
import { getCommitDate, checkPreviousPRCommitsHadCI, checkWorkflowsHavePRTriggers } from './github-merge-ci-signals.lib.mjs';
|
|
1330
|
+
export { getCommitDate, checkPreviousPRCommitsHadCI, checkWorkflowsHavePRTriggers };
|
|
1448
1331
|
|
|
1449
1332
|
// Issue #1341: Re-export post-merge CI functions from separate module
|
|
1450
1333
|
import { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha } from './github-merge-ci.lib.mjs';
|
|
@@ -1481,6 +1364,7 @@ export default {
|
|
|
1481
1364
|
rerunWorkflowRun,
|
|
1482
1365
|
rerunFailedJobs,
|
|
1483
1366
|
getWorkflowRunsForSha,
|
|
1367
|
+
getWorkflowRunJobsCount, // Issue #1690: detect invalid workflow files (no jobs created)
|
|
1484
1368
|
waitForCommitCI,
|
|
1485
1369
|
checkBranchCIHealth,
|
|
1486
1370
|
getMergeCommitSha,
|
|
@@ -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
|
}
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -379,8 +379,8 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
379
379
|
},
|
|
380
380
|
'tokens-budget-stats': {
|
|
381
381
|
type: 'boolean',
|
|
382
|
-
description: '
|
|
383
|
-
default:
|
|
382
|
+
description: 'Show detailed token budget statistics including context window usage and ratios (enabled by default, use --no-tokens-budget-stats to disable). Supported for --tool claude, --tool codex, and any tool that returns detailed token usage.',
|
|
383
|
+
default: true,
|
|
384
384
|
},
|
|
385
385
|
'prompt-issue-reporting': {
|
|
386
386
|
type: 'boolean',
|
|
@@ -464,13 +464,13 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
464
464
|
},
|
|
465
465
|
'auto-attach-solution-summary': {
|
|
466
466
|
type: 'boolean',
|
|
467
|
-
description: 'Automatically attach solution summary only if the AI did not create any comments during the session. This provides visible feedback when the AI completes silently.',
|
|
468
|
-
default:
|
|
467
|
+
description: 'Automatically attach solution summary only if the AI did not create any comments during the session. This provides visible feedback when the AI completes silently. Enabled by default; use --no-auto-attach-solution-summary to disable.',
|
|
468
|
+
default: true,
|
|
469
469
|
},
|
|
470
470
|
'auto-accept-invite': {
|
|
471
471
|
type: 'boolean',
|
|
472
|
-
description: 'Automatically accept the pending GitHub repository or organization invitation for the specific repository/organization being solved, before checking write access. Unlike /accept_invites which accepts all pending invitations, this only accepts the invite for the target repo/org.',
|
|
473
|
-
default:
|
|
472
|
+
description: 'Automatically accept the pending GitHub repository or organization invitation for the specific repository/organization being solved, before checking write access. Unlike /accept_invites which accepts all pending invitations, this only accepts the invite for the target repo/org. Enabled by default; use --no-auto-accept-invite to disable.',
|
|
473
|
+
default: true,
|
|
474
474
|
},
|
|
475
475
|
'prompt-ensure-all-requirements-are-met': {
|
|
476
476
|
type: 'boolean',
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 `ā³
|
|
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 ? `
|
|
60
|
-
const isolationInfo = sessionInfo?.isolationBackend ? `\nš Isolation:
|
|
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}
|
|
66
|
-
message += `š Session: \`${sessionName || 'unknown'}\`\n`;
|
|
67
|
+
let message = `${statusEmoji} *${statusText}*\n\n`;
|
|
67
68
|
message += `ā±ļø Duration: ${formatSessionDurationSeconds(durationSeconds)}\n`;
|
|
68
|
-
message +=
|
|
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
|
}
|