@link-assistant/hive-mind 1.54.6 → 1.54.8

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,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.54.8
4
+
5
+ ### Patch Changes
6
+
7
+ - 12f5761: Fix `--auto-restart-until-mergeable` readiness comment deduplication for pull requests with more than one page of comments, and enforce pagination on list-returning `gh api` calls.
8
+
9
+ ## 1.54.7
10
+
11
+ ### Patch Changes
12
+
13
+ - 06b1a41: Fix `--auto-attach-solution-summary` so the AI-comment scan starts at the current work session instead of the older feedback reference time.
14
+
3
15
  ## 1.54.6
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.54.6",
3
+ "version": "1.54.8",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -14,12 +14,12 @@
14
14
  "hive-telegram-bot": "./src/telegram-bot.mjs"
15
15
  },
16
16
  "scripts": {
17
- "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-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
17
+ "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-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-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
18
18
  "test:queue": "node tests/solve-queue.test.mjs",
19
19
  "test:limits-display": "node tests/limits-display.test.mjs",
20
20
  "test:usage-limit": "node tests/test-usage-limit.mjs",
21
- "lint": "eslint 'src/**/*.{js,mjs,cjs}'",
22
- "lint:fix": "eslint 'src/**/*.{js,mjs,cjs}' --fix",
21
+ "lint": "eslint 'src/**/*.{js,mjs,cjs}' 'scripts/**/*.{js,mjs,cjs}' 'eslint-rules/**/*.{js,mjs,cjs}'",
22
+ "lint:fix": "eslint 'src/**/*.{js,mjs,cjs}' 'scripts/**/*.{js,mjs,cjs}' 'eslint-rules/**/*.{js,mjs,cjs}' --fix",
23
23
  "check:duplication": "jscpd .",
24
24
  "format": "prettier --write \"**/*.{js,mjs,json,md}\" --ignore-path .prettierignore",
25
25
  "format:check": "prettier --check \"**/*.{js,mjs,json,md}\" --ignore-path .prettierignore",
@@ -185,8 +185,10 @@ export async function checkBranchCIHealth(owner, repo, branch = 'main', options,
185
185
 
186
186
  // Issue #1425: Query CI runs specifically for the HEAD SHA (no status filter).
187
187
  // This ensures we see in-progress runs for the latest commit, not just completed ones.
188
- const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${headSha}&per_page=20" --jq '[.workflow_runs[] | {id: .id, name: .name, status: .status, conclusion: .conclusion, html_url: .html_url, head_sha: .head_sha, created_at: .created_at}]'`);
189
- const runs = JSON.parse(stdout.trim() || '[]');
188
+ const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${headSha}&per_page=100" --paginate --slurp`);
189
+ const runs = JSON.parse(stdout.trim() || '[]')
190
+ .flatMap(page => page.workflow_runs || [])
191
+ .map(run => ({ id: run.id, name: run.name, status: run.status, conclusion: run.conclusion, html_url: run.html_url, head_sha: run.head_sha, created_at: run.created_at }));
190
192
 
191
193
  if (verbose) {
192
194
  console.log(`[VERBOSE] /merge: Found ${runs.length} CI run(s) for HEAD commit ${headSha.substring(0, 7)} on ${owner}/${repo} branch ${branch}`);
@@ -22,10 +22,11 @@ const exec = promisify(execCallback);
22
22
  */
23
23
  export async function getAllActiveRepoRuns(owner, repo, verbose = false) {
24
24
  try {
25
- const activeFilter = '.workflow_runs[] | select(.status=="in_progress" or .status=="queued" or .status=="waiting" or .status=="requested" or .status=="pending")';
26
- const fields = '{id: .id, name: .name, status: .status, head_branch: .head_branch, head_sha: (.head_sha[:7])}';
27
- const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?per_page=100" --jq '[${activeFilter}] | map(${fields})'`);
28
- const runs = JSON.parse(stdout.trim() || '[]');
25
+ const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?per_page=100" --paginate --slurp`);
26
+ const runs = JSON.parse(stdout.trim() || '[]')
27
+ .flatMap(page => page.workflow_runs || [])
28
+ .filter(run => ['in_progress', 'queued', 'waiting', 'requested', 'pending'].includes(run.status))
29
+ .map(run => ({ id: run.id, name: run.name, status: run.status, head_branch: run.head_branch, head_sha: run.head_sha?.slice(0, 7) }));
29
30
  if (verbose && runs.length > 0) {
30
31
  console.log(`[VERBOSE] repo-actions: ${runs.length} active run(s) in ${owner}/${repo}`);
31
32
  for (const r of runs) console.log(`[VERBOSE] repo-actions: ${r.name} (${r.status}) on ${r.head_branch}`);
@@ -313,9 +313,8 @@ export async function checkPRCIStatus(owner, repo, prNumber, verbose = false) {
313
313
  const prData = JSON.parse(prJson.trim());
314
314
  const sha = prData.headRefOid;
315
315
 
316
- // Get check runs for this SHA
317
- const { stdout: checksJson } = await exec(`gh api repos/${owner}/${repo}/commits/${sha}/check-runs --paginate --jq '.check_runs'`);
318
- const checkRuns = JSON.parse(checksJson.trim() || '[]');
316
+ const { stdout: checksJson } = await exec(`gh api repos/${owner}/${repo}/commits/${sha}/check-runs --paginate --slurp`);
317
+ const checkRuns = JSON.parse(checksJson.trim() || '[]').flatMap(page => page.check_runs || []);
319
318
 
320
319
  // Get commit statuses (some CI systems use status API instead of checks API)
321
320
  const { stdout: statusJson } = await exec(`gh api repos/${owner}/${repo}/commits/${sha}/status --jq '.statuses'`);
@@ -685,9 +684,11 @@ export function parseRepositoryUrl(url) {
685
684
  export async function getActiveBranchRuns(owner, repo, branch = 'main', verbose = false) {
686
685
  try {
687
686
  // Query for in_progress and queued runs on the specified branch
688
- const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?branch=${branch}&per_page=10" --jq '[.workflow_runs[] | select(.status=="in_progress" or .status=="queued")] | map({id: .id, name: .name, status: .status, created_at: .created_at, html_url: .html_url})'`);
689
-
690
- const runs = JSON.parse(stdout.trim() || '[]');
687
+ const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?branch=${branch}&per_page=100" --paginate --slurp`);
688
+ const runs = JSON.parse(stdout.trim() || '[]')
689
+ .flatMap(page => page.workflow_runs || [])
690
+ .filter(run => run.status === 'in_progress' || run.status === 'queued')
691
+ .map(run => ({ id: run.id, name: run.name, status: run.status, created_at: run.created_at, html_url: run.html_url }));
691
692
 
692
693
  if (verbose) {
693
694
  console.log(`[VERBOSE] /merge: Found ${runs.length} active runs on ${owner}/${repo} branch ${branch}`);
@@ -839,7 +840,7 @@ export async function getDefaultBranch(owner, repo, verbose = false) {
839
840
  */
840
841
  export async function getCheckRunAnnotations(owner, repo, checkRunId, verbose = false) {
841
842
  try {
842
- const { stdout } = await exec(`gh api repos/${owner}/${repo}/check-runs/${checkRunId}/annotations 2>/dev/null || echo "[]"`);
843
+ const { stdout } = await exec(`gh api repos/${owner}/${repo}/check-runs/${checkRunId}/annotations --paginate 2>/dev/null || echo "[]"`);
843
844
  const annotations = JSON.parse(stdout.trim() || '[]');
844
845
 
845
846
  if (verbose) {
@@ -895,12 +896,6 @@ export const BILLING_LIMIT_ERROR_PATTERN = 'The job was not started because rece
895
896
  * Check if CI failure is due to billing/spending limits
896
897
  * Issue #1314: Detects when GitHub Actions jobs fail due to billing issues rather than code problems
897
898
  *
898
- * Detection criteria:
899
- * 1. Job has conclusion='failure'
900
- * 2. Job has empty steps array (no steps were executed)
901
- * 3. Job has runner_id=0 or null (no runner was assigned)
902
- * 4. Annotation contains the billing limit error message
903
- *
904
899
  * @param {string} owner - Repository owner
905
900
  * @param {string} repo - Repository name
906
901
  * @param {number} prNumber - Pull request number
@@ -909,14 +904,14 @@ export const BILLING_LIMIT_ERROR_PATTERN = 'The job was not started because rece
909
904
  */
910
905
  export async function checkForBillingLimitError(owner, repo, prNumber, verbose = false) {
911
906
  try {
912
- // Get the PR's head SHA
913
907
  const { stdout: prJson } = await exec(`gh pr view ${prNumber} --repo ${owner}/${repo} --json headRefOid`);
914
908
  const prData = JSON.parse(prJson.trim());
915
909
  const sha = prData.headRefOid;
916
910
 
917
- // Get workflow runs for this SHA
918
- const { stdout: runsJson } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=10" --jq '.workflow_runs[].id'`);
919
- const runIds = runsJson.trim().split('\n').filter(Boolean);
911
+ const { stdout: runsJson } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=100" --paginate --slurp`);
912
+ const runIds = JSON.parse(runsJson.trim() || '[]')
913
+ .flatMap(page => page.workflow_runs || [])
914
+ .map(run => run.id);
920
915
 
921
916
  if (verbose) {
922
917
  console.log(`[VERBOSE] /merge: Found ${runIds.length} workflow runs for PR #${prNumber} at SHA ${sha.substring(0, 7)}`);
@@ -925,11 +920,10 @@ export async function checkForBillingLimitError(owner, repo, prNumber, verbose =
925
920
  const affectedJobs = [];
926
921
  let totalJobs = 0;
927
922
 
928
- // Check each workflow run's jobs
929
923
  for (const runId of runIds) {
930
924
  try {
931
- const { stdout: jobsJson } = await exec(`gh api repos/${owner}/${repo}/actions/runs/${runId}/jobs --jq '.jobs'`);
932
- const jobs = JSON.parse(jobsJson.trim() || '[]');
925
+ const { stdout: jobsJson } = await exec(`gh api repos/${owner}/${repo}/actions/runs/${runId}/jobs --paginate --slurp`);
926
+ const jobs = JSON.parse(jobsJson.trim() || '[]').flatMap(page => page.jobs || []);
933
927
 
934
928
  for (const job of jobs) {
935
929
  totalJobs++;
@@ -1068,9 +1062,8 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
1068
1062
  const prData = JSON.parse(prJson.trim());
1069
1063
  const sha = prData.headRefOid;
1070
1064
 
1071
- // Get check runs for this SHA
1072
- const { stdout: checksJson } = await exec(`gh api repos/${owner}/${repo}/commits/${sha}/check-runs --paginate --jq '.check_runs'`);
1073
- const checkRuns = JSON.parse(checksJson.trim() || '[]');
1065
+ const { stdout: checksJson } = await exec(`gh api repos/${owner}/${repo}/commits/${sha}/check-runs --paginate --slurp`);
1066
+ const checkRuns = JSON.parse(checksJson.trim() || '[]').flatMap(page => page.check_runs || []);
1074
1067
 
1075
1068
  // Get commit statuses
1076
1069
  const { stdout: statusJson } = await exec(`gh api repos/${owner}/${repo}/commits/${sha}/status --jq '.statuses'`);
@@ -1214,8 +1207,10 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
1214
1207
  */
1215
1208
  export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
1216
1209
  try {
1217
- const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=20" --jq '[.workflow_runs[] | {id: .id, status: .status, conclusion: .conclusion, name: .name, html_url: .html_url}]'`);
1218
- const runs = JSON.parse(stdout.trim() || '[]');
1210
+ const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=100" --paginate --slurp`);
1211
+ const runs = JSON.parse(stdout.trim() || '[]')
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 }));
1219
1214
 
1220
1215
  if (verbose) {
1221
1216
  console.log(`[VERBOSE] /merge: Found ${runs.length} workflow runs for SHA ${sha.substring(0, 7)}`);
@@ -1251,13 +1246,13 @@ export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
1251
1246
  */
1252
1247
  export async function getActiveRepoWorkflows(owner, repo, verbose = false) {
1253
1248
  try {
1254
- const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/workflows" --jq '[.workflows[] | select(.state == "active")] | map({id: .id, name: .name, state: .state, path: .path})'`);
1255
- const allWorkflows = JSON.parse(stdout.trim() || '[]');
1249
+ const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/workflows" --paginate --slurp`);
1250
+ const allWorkflows = JSON.parse(stdout.trim() || '[]')
1251
+ .flatMap(page => page.workflows || [])
1252
+ .filter(workflow => workflow.state === 'active')
1253
+ .map(workflow => ({ id: workflow.id, name: workflow.name, state: workflow.state, path: workflow.path }));
1256
1254
 
1257
- // Issue #1399: Filter out GitHub Pages deployment workflows.
1258
- // These have path "dynamic/pages/pages-build-deployment" and only run on the
1259
- // default branch after merge — they never produce check-runs on PR branches.
1260
- // Including them causes an infinite loop when waiting for PR CI checks.
1255
+ // GitHub Pages workflows only run after merge and never produce PR check-runs.
1261
1256
  const workflows = allWorkflows.filter(wf => !wf.path.startsWith('dynamic/pages/'));
1262
1257
 
1263
1258
  if (verbose) {
@@ -1351,8 +1346,8 @@ export async function checkPreviousPRCommitsHadCI(owner, repo, prNumber, headSha
1351
1346
 
1352
1347
  for (const sha of commitsToCheck) {
1353
1348
  try {
1354
- const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=1" --jq '.total_count'`);
1355
- const count = parseInt(stdout.trim(), 10);
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);
1356
1351
  if (count > 0) {
1357
1352
  commitsWithCI++;
1358
1353
  }
@@ -1390,7 +1385,7 @@ export async function checkWorkflowsHavePRTriggers(owner, repo, verbose = false,
1390
1385
  // Issue #1503: Support querying workflow files from a specific branch (ref)
1391
1386
  const refParam = ref ? `?ref=${encodeURIComponent(ref)}` : '';
1392
1387
  // List workflow files in .github/workflows/ (uses ref if provided, otherwise default branch)
1393
- const { stdout: listJson } = await exec(`gh api "repos/${owner}/${repo}/contents/.github/workflows${refParam}" --jq '[.[] | select(.name | test("\\\\.(yml|yaml)$")) | {name: .name, download_url: .download_url, path: .path}]' 2>/dev/null`);
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`);
1394
1389
  const files = JSON.parse(listJson.trim() || '[]');
1395
1390
 
1396
1391
  if (files.length === 0) {
@@ -149,7 +149,7 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
149
149
  if (verbose) {
150
150
  await log(` 🔍 Fetching repository contents for raw URL resolution (repoPath=${repoPath})`, { verbose: true });
151
151
  }
152
- const contentsResult = await $silent`gh api repos/${repoPath}/contents --jq '.[].name'`;
152
+ const contentsResult = await $silent`gh api repos/${repoPath}/contents --paginate --jq '.[].name'`;
153
153
  if (verbose) {
154
154
  await log(` đŸ“Ĩ Repository contents fetch completed (code=${contentsResult.code ?? 'unknown'})`, { verbose: true });
155
155
  }
@@ -43,7 +43,7 @@ export async function autoAcceptInviteForRepo(owner, repo, log, verbose) {
43
43
 
44
44
  // Check for pending repository invitation
45
45
  try {
46
- const { stdout: repoInvJson } = await ghRetry(() => exec('gh api /user/repository_invitations 2>/dev/null || echo "[]"'), { label: 'fetch repo invitations' });
46
+ const { stdout: repoInvJson } = await ghRetry(() => exec('gh api /user/repository_invitations --paginate 2>/dev/null || echo "[]"'), { label: 'fetch repo invitations' });
47
47
  const repoInvitations = JSON.parse(repoInvJson.trim() || '[]');
48
48
  verbose && (await log(` Found ${repoInvitations.length} total pending repo invitation(s)`, { verbose: true }));
49
49
 
@@ -66,7 +66,7 @@ export async function autoAcceptInviteForRepo(owner, repo, log, verbose) {
66
66
 
67
67
  // Check for pending organization membership
68
68
  try {
69
- const { stdout: orgMemJson } = await ghRetry(() => exec('gh api /user/memberships/orgs 2>/dev/null || echo "[]"'), { label: 'fetch org memberships' });
69
+ const { stdout: orgMemJson } = await ghRetry(() => exec('gh api /user/memberships/orgs --paginate 2>/dev/null || echo "[]"'), { label: 'fetch org memberships' });
70
70
  const orgMemberships = JSON.parse(orgMemJson.trim() || '[]');
71
71
  const pendingOrgs = orgMemberships.filter(m => m.state === 'pending');
72
72
  verbose && (await log(` Found ${pendingOrgs.length} total pending org invitation(s)`, { verbose: true }));
@@ -63,12 +63,14 @@ const { SESSION_ENDING_MARKERS } = toolComments;
63
63
  * @param {number} prNumber - Pull request number
64
64
  * @param {string} commentSignature - Unique signature to search for in comment body (e.g., "✅ Ready to merge")
65
65
  * @param {boolean} verbose - Enable verbose logging
66
+ * @param {Function} commandRunner - Tagged-template command runner, injectable for tests
66
67
  * @returns {Promise<boolean>} - True if a matching comment already exists
67
68
  */
68
- export const checkForExistingComment = async (owner, repo, prNumber, commentSignature, verbose = false) => {
69
+ export const checkForExistingComment = async (owner, repo, prNumber, commentSignature, verbose = false, commandRunner = $) => {
69
70
  try {
70
- // Fetch all PR comments as JSON to get individual comment bodies in order
71
- const result = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --jq '[.[].body]' 2>/dev/null`;
71
+ // Fetch every PR comment page so long threads don't scope deduplication to
72
+ // a stale first-page session-ending marker.
73
+ const result = await commandRunner`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate --jq '[.[].body]' 2>/dev/null`;
72
74
  if (result.code === 0 && result.stdout) {
73
75
  const rawOutput = result.stdout.toString().trim();
74
76
  if (!rawOutput) return false;
@@ -604,7 +604,7 @@ Proceed.
604
604
  }
605
605
  compareResult = await $({
606
606
  silent: true,
607
- })`gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${headRef} --jq '.ahead_by' 2>&1`;
607
+ })`gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${headRef} --paginate --jq '.ahead_by' 2>&1`;
608
608
 
609
609
  if (compareResult.code === 0) {
610
610
  const aheadBy = parseInt(compareResult.stdout.toString().trim(), 10);
@@ -798,9 +798,9 @@ Proceed.
798
798
  // Use the correct head reference for the compare API check
799
799
  if (argv.fork && forkedRepo) {
800
800
  const forkUser = forkedRepo.split('/')[0];
801
- await log(` gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${forkUser}:${branchName}`);
801
+ await log(` gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${forkUser}:${branchName} --paginate`);
802
802
  } else {
803
- await log(` gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${branchName}`);
803
+ await log(` gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${branchName} --paginate`);
804
804
  }
805
805
  await log('');
806
806
 
@@ -329,10 +329,12 @@ export const detectAndCountFeedback = async params => {
329
329
 
330
330
  // 6. Check for failed PR checks
331
331
  try {
332
- const checksResult = await $`gh api repos/${owner}/${repo}/commits/$(gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.head.sha')/check-runs --paginate`;
333
- if (checksResult.code === 0) {
334
- const checksData = JSON.parse(checksResult.stdout.toString());
335
- const failedChecks = checksData.check_runs?.filter(check => check.conclusion === 'failure' && new Date(check.completed_at) > lastCommitTime) || [];
332
+ const prHeadResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.head.sha'`;
333
+ if (prHeadResult.code === 0) {
334
+ const prHeadSha = prHeadResult.stdout.toString().trim();
335
+ const checksResult = await $`gh api repos/${owner}/${repo}/commits/${prHeadSha}/check-runs --paginate --slurp`;
336
+ const checkRuns = checksResult.code === 0 ? JSON.parse(checksResult.stdout.toString() || '[]').flatMap(page => page.check_runs || []) : [];
337
+ const failedChecks = checkRuns.filter(check => check.conclusion === 'failure' && new Date(check.completed_at) > lastCommitTime);
336
338
 
337
339
  if (failedChecks.length > 0) {
338
340
  feedbackLines.push(`Failed pull request checks: ${failedChecks.length}`);
package/src/solve.mjs CHANGED
@@ -642,7 +642,7 @@ try {
642
642
  // Continue mode is a manual resume via PR URL
643
643
  sessionType = SESSION_TYPES.RESUME;
644
644
  }
645
- await startWorkSession({
645
+ const workStartTime = await startWorkSession({
646
646
  isContinueMode,
647
647
  prNumber,
648
648
  argv,
@@ -1190,7 +1190,7 @@ try {
1190
1190
  } else if (argv.autoAttachSolutionSummary) {
1191
1191
  // Auto mode - only attach if AI didn't create comments
1192
1192
  await log('🔍 Checking if AI created any comments during session (--auto-attach-solution-summary)...');
1193
- const aiCreatedComments = await checkForAiCreatedComments(referenceTime, owner, repo, prNumber, issueNumber);
1193
+ const aiCreatedComments = await checkForAiCreatedComments(workStartTime, owner, repo, prNumber, issueNumber);
1194
1194
  if (aiCreatedComments) {
1195
1195
  await log('â„šī¸ AI created comments during session, skipping solution summary attachment');
1196
1196
  } else {
@@ -242,7 +242,7 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
242
242
  state.commentId = posted.commentId;
243
243
  } else {
244
244
  // Fallback: find the comment we just created by looking for our marker
245
- const commentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --jq ${`[.[] | select(.body | contains("${CONFIG.PROGRESS_SECTION_START}")) | .id] | last`}`;
245
+ const commentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate --jq ${`[.[] | select(.body | contains("${CONFIG.PROGRESS_SECTION_START}")) | .id] | last`}`;
246
246
  const commentId = commentsResult.stdout?.toString?.().trim();
247
247
  if (commentId && commentId !== 'null') {
248
248
  state.commentId = commentId;
@@ -525,7 +525,7 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
525
525
  await log(`${formatAligned('🔍', 'Safety check:', 'Comparing commits against upstream...')}`);
526
526
  let safeToDelete = false;
527
527
  try {
528
- const cmp = await $`gh api repos/${owner}/${repo}/compare/${owner}:HEAD...${existingForkName.split('/')[0]}:HEAD --jq '.ahead_by' 2>&1`;
528
+ const cmp = await $`gh api repos/${owner}/${repo}/compare/${owner}:HEAD...${existingForkName.split('/')[0]}:HEAD --paginate --jq '.ahead_by' 2>&1`;
529
529
  if (cmp.code === 0 && parseInt(cmp.stdout.toString().trim(), 10) === 0) {
530
530
  await log(`${formatAligned('✅', 'Safe to delete:', 'No additional commits in non-fork repository')}`);
531
531
  safeToDelete = true;
@@ -1058,14 +1058,14 @@ export const { TOOL_GENERATED_COMMENT_MARKERS, isToolGeneratedComment, trackTool
1058
1058
  * Issue #1625: Filter out comments produced by solve.mjs itself (session start,
1059
1059
  * log upload, auto-restart, etc.) so they do not falsely count as AI-authored.
1060
1060
  *
1061
- * @param {Date} referenceTime - The timestamp before tool execution
1061
+ * @param {Date} sessionStartTime - The timestamp when this solve work session started
1062
1062
  * @param {string} owner - Repository owner
1063
1063
  * @param {string} repo - Repository name
1064
1064
  * @param {number} prNumber - Pull request number (null if working on issue only)
1065
1065
  * @param {number} issueNumber - Issue number
1066
1066
  * @returns {Promise<boolean>} - True if AI created comments during the session
1067
1067
  */
1068
- export const checkForAiCreatedComments = async (referenceTime, owner, repo, prNumber, issueNumber) => {
1068
+ export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, prNumber, issueNumber) => {
1069
1069
  try {
1070
1070
  // Get the current user's GitHub username
1071
1071
  const userResult = await $`gh api user --jq .login`;
@@ -1077,10 +1077,10 @@ export const checkForAiCreatedComments = async (referenceTime, owner, repo, prNu
1077
1077
  return false;
1078
1078
  }
1079
1079
 
1080
- await log(`🔎 Checking comments by '${currentUser}' after ${referenceTime.toISOString()} (PR #${prNumber ?? 'none'}, issue #${issueNumber ?? 'none'})`, { verbose: true });
1080
+ await log(`🔎 Checking comments by '${currentUser}' after session start ${sessionStartTime.toISOString()} (PR #${prNumber ?? 'none'}, issue #${issueNumber ?? 'none'})`, { verbose: true });
1081
1081
 
1082
1082
  // Issue #1625: A comment counts as an "AI comment" only if it was posted
1083
- // by the current user AFTER referenceTime AND solve.mjs did NOT post it
1083
+ // by the current user AFTER sessionStartTime AND solve.mjs did NOT post it
1084
1084
  // itself. We identify tool-posted comments in two ways, in order:
1085
1085
  // 1. Primary: comment ID is in the in-memory tracked set populated by
1086
1086
  // every solve.mjs posting site (postTrackedComment / trackToolCommentId).
@@ -1097,7 +1097,7 @@ export const checkForAiCreatedComments = async (referenceTime, owner, repo, prNu
1097
1097
  const skippedByIdCount = { n: 0 };
1098
1098
  for (const comment of comments) {
1099
1099
  if (!comment || !comment.user || comment.user.login !== currentUser) continue;
1100
- if (!(new Date(comment.created_at) > referenceTime)) continue;
1100
+ if (!(new Date(comment.created_at) > sessionStartTime)) continue;
1101
1101
 
1102
1102
  const isReview = kind === 'review';
1103
1103
  if (!isReview) {
@@ -1132,7 +1132,7 @@ export const checkForAiCreatedComments = async (referenceTime, owner, repo, prNu
1132
1132
  if (prCommentsResult.code === 0) {
1133
1133
  const prComments = JSON.parse(prCommentsResult.stdout.toString().trim() || '[]');
1134
1134
  const newPrComments = filterNewAiComments(prComments, 'pr');
1135
- await log(` 📨 PR conversation comments after referenceTime by '${currentUser}' (excluding tool-generated): ${newPrComments.length}`, { verbose: true });
1135
+ await log(` 📨 PR conversation comments after session start by '${currentUser}' (excluding tool-generated): ${newPrComments.length}`, { verbose: true });
1136
1136
  if (newPrComments.length > 0) {
1137
1137
  return true;
1138
1138
  }
@@ -1143,7 +1143,7 @@ export const checkForAiCreatedComments = async (referenceTime, owner, repo, prNu
1143
1143
  if (reviewCommentsResult.code === 0) {
1144
1144
  const reviewComments = JSON.parse(reviewCommentsResult.stdout.toString().trim() || '[]');
1145
1145
  const newReviewComments = filterNewAiComments(reviewComments, 'review');
1146
- await log(` 📝 PR review (inline) comments after referenceTime by '${currentUser}': ${newReviewComments.length}`, { verbose: true });
1146
+ await log(` 📝 PR review (inline) comments after session start by '${currentUser}': ${newReviewComments.length}`, { verbose: true });
1147
1147
  if (newReviewComments.length > 0) {
1148
1148
  return true;
1149
1149
  }
@@ -1156,7 +1156,7 @@ export const checkForAiCreatedComments = async (referenceTime, owner, repo, prNu
1156
1156
  if (issueCommentsResult.code === 0) {
1157
1157
  const issueComments = JSON.parse(issueCommentsResult.stdout.toString().trim() || '[]');
1158
1158
  const newIssueComments = filterNewAiComments(issueComments, 'issue');
1159
- await log(` 📨 Issue comments after referenceTime by '${currentUser}' (excluding tool-generated): ${newIssueComments.length}`, { verbose: true });
1159
+ await log(` 📨 Issue comments after session start by '${currentUser}' (excluding tool-generated): ${newIssueComments.length}`, { verbose: true });
1160
1160
  if (newIssueComments.length > 0) {
1161
1161
  return true;
1162
1162
  }
@@ -174,13 +174,13 @@ export function registerAcceptInvitesCommand(bot, options) {
174
174
 
175
175
  try {
176
176
  // Fetch repository invitations
177
- const { stdout: repoInvJson } = await exec('gh api /user/repository_invitations 2>/dev/null || echo "[]"');
177
+ const { stdout: repoInvJson } = await exec('gh api /user/repository_invitations --paginate 2>/dev/null || echo "[]"');
178
178
  const repoInvitations = JSON.parse(repoInvJson.trim() || '[]');
179
179
  state.totalRepos = repoInvitations.length;
180
180
  VERBOSE && console.log(`[VERBOSE] Found ${repoInvitations.length} pending repo invitations`);
181
181
 
182
182
  // Fetch organization invitations
183
- const { stdout: orgMemJson } = await exec('gh api /user/memberships/orgs 2>/dev/null || echo "[]"');
183
+ const { stdout: orgMemJson } = await exec('gh api /user/memberships/orgs --paginate 2>/dev/null || echo "[]"');
184
184
  const orgMemberships = JSON.parse(orgMemJson.trim() || '[]');
185
185
  const pendingOrgs = orgMemberships.filter(m => m.state === 'pending');
186
186
  state.totalOrgs = pendingOrgs.length;