@link-assistant/hive-mind 1.59.5 → 1.59.6

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/package.json +1 -1
  3. package/src/bidirectional-interactive.lib.mjs +1 -0
  4. package/src/contributing-guidelines.lib.mjs +3 -2
  5. package/src/github-error-reporter.lib.mjs +3 -2
  6. package/src/github-merge-ci-signals.lib.mjs +8 -2
  7. package/src/github-merge-ci.lib.mjs +8 -2
  8. package/src/github-merge-ready-sync.lib.mjs +7 -1
  9. package/src/github-merge-repo-actions.lib.mjs +7 -1
  10. package/src/github-merge.lib.mjs +40 -32
  11. package/src/github-rate-limit.lib.mjs +276 -0
  12. package/src/github.batch.lib.mjs +1 -0
  13. package/src/hive.mjs +2 -2
  14. package/src/hive.recheck.lib.mjs +1 -0
  15. package/src/lib.mjs +30 -4
  16. package/src/limits.lib.mjs +1 -0
  17. package/src/protect-branch.mjs +3 -2
  18. package/src/queue-config.lib.mjs +7 -3
  19. package/src/review.mjs +3 -2
  20. package/src/reviewers-hive.mjs +3 -2
  21. package/src/solve.accept-invite.lib.mjs +7 -1
  22. package/src/solve.auto-continue.lib.mjs +3 -2
  23. package/src/solve.auto-ensure.lib.mjs +3 -2
  24. package/src/solve.auto-merge-helpers.lib.mjs +3 -2
  25. package/src/solve.auto-merge.lib.mjs +3 -2
  26. package/src/solve.auto-pr.lib.mjs +1 -0
  27. package/src/solve.branch-errors.lib.mjs +1 -0
  28. package/src/solve.error-handlers.lib.mjs +1 -0
  29. package/src/solve.execution.lib.mjs +3 -2
  30. package/src/solve.feedback.lib.mjs +1 -0
  31. package/src/solve.mjs +3 -1
  32. package/src/solve.preparation.lib.mjs +1 -0
  33. package/src/solve.progress-monitoring.lib.mjs +1 -0
  34. package/src/solve.repository.lib.mjs +3 -3
  35. package/src/solve.restart-shared.lib.mjs +3 -2
  36. package/src/solve.results.lib.mjs +3 -2
  37. package/src/solve.session.lib.mjs +1 -0
  38. package/src/solve.watch.lib.mjs +3 -2
  39. package/src/telegram-accept-invitations.lib.mjs +7 -1
  40. package/src/token-sanitization.lib.mjs +1 -0
  41. package/src/youtrack/youtrack-sync.mjs +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,125 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.59.6
4
+
5
+ ### Patch Changes
6
+
7
+ - d6d05a0: Fully safeguard from GitHub API rate-limit errors — issue #1726.
8
+
9
+ `/merge` merged a draft PR even though every `gh api` call had been failing
10
+ with `HTTP 403: API rate limit exceeded`. The merge subsystem caught those
11
+ errors silently in `getActiveRepoWorkflows()` and reported _"no CI checks
12
+ and repo has no active workflows — no CI/CD configured"_, which `/merge`
13
+ interpreted as _"all clear"_. Verbose log
14
+ ([`docs/case-studies/issue-1726/data/a4dccea2-a941-4a0c-a50e-60b1ed454e1e.log`](./docs/case-studies/issue-1726/data/a4dccea2-a941-4a0c-a50e-60b1ed454e1e.log),
15
+ lines 40251–40269):
16
+
17
+ ```
18
+ [VERBOSE] /merge: Error fetching workflows for link-foundation/relative-meta-logic:
19
+ Command failed: gh api "repos/link-foundation/relative-meta-logic/actions/workflows" --paginate --slurp
20
+ gh: API rate limit exceeded for user ID 1431904 ... (HTTP 403)
21
+
22
+ [VERBOSE] /merge: PR #100 has no CI checks and repo has no active workflows - no CI/CD configured
23
+ ```
24
+
25
+ Two combining root causes:
26
+ 1. **`getActiveRepoWorkflows()` swallowed exceptions** in
27
+ [`src/github-merge.lib.mjs`](./src/github-merge.lib.mjs) and returned
28
+ `[]`. Rate-limit responses became "this repo has no workflows", which the
29
+ merge gate treated as "no CI configured, safe to merge".
30
+ 2. **No gh API call site had rate-limit retry**. The existing
31
+ `ghCmdRetry`/`ghRetry` helpers only recognised transient TCP/TLS faults,
32
+ so a 403 fell straight through. ~135 raw `$gh ...` and
33
+ ``exec(`gh ...`)`` call sites scattered across `src/solve.*`,
34
+ `src/github-merge.*`, scripts, and reviewers.
35
+
36
+ Fix:
37
+ - **New rate-limit module**
38
+ [`src/github-rate-limit.lib.mjs`](./src/github-rate-limit.lib.mjs) with
39
+ `isRateLimitError`, `parseRateLimitReset`, `fetchNextRateLimitReset`,
40
+ `computeRateLimitWait`, `ghWithRateLimitRetry`, `execGhWithRetry`,
41
+ `wrapDollarWithGhRetry`. Applies the issue's policy:
42
+ `wait = (resetTime − now) + bufferMs (10 min) + random(0..jitterMs) (0..5 min)`,
43
+ reusing `limitReset.bufferMs` / `limitReset.jitterMs` from
44
+ [`src/config.lib.mjs`](./src/config.lib.mjs) (introduced in #1236).
45
+ - **Propagate errors instead of swallowing**. `getActiveRepoWorkflows()`
46
+ no longer wraps the gh call in try/catch that returns `[]`. Errors bubble
47
+ up; the merge gate sees the failure and stops.
48
+ - **Layered retry in legacy helpers**. `ghRetry` and `ghCmdRetry` in
49
+ [`src/lib.mjs`](./src/lib.mjs) check `isRateLimitError` first and delegate
50
+ to `ghWithRateLimitRetry` before applying transient-network retry.
51
+ - **Local `exec` shim** in 7 merge files rebound through
52
+ `ghWithRateLimitRetry` — converts every existing ``exec(`gh ...`)`` site
53
+ without per-call edits.
54
+ - **Wrapped `$` at every entry point** (15 files). `wrapDollarWithGhRetry`
55
+ routes every `$gh ...` through the retry helper while passing non-gh
56
+ commands unchanged.
57
+ - **Marker imports** in 17 callee files that receive `$` as a parameter,
58
+ declaring rate-limit awareness for the ESLint rule.
59
+ - **Queue threshold lowered** from 75% to 50% in
60
+ [`src/queue-config.lib.mjs`](./src/queue-config.lib.mjs).
61
+ - **Custom ESLint rule**
62
+ [`eslint-rules/no-direct-gh-exec.mjs`](./eslint-rules/no-direct-gh-exec.mjs)
63
+ flags any unsafe `gh` exec call site; files that import a known-safe
64
+ wrapper are exempted at file scope.
65
+
66
+ Tests:
67
+ - [`tests/github-rate-limit.test.mjs`](./tests/github-rate-limit.test.mjs)
68
+ — 22 unit tests covering `isRateLimitError` (primary, secondary,
69
+ abuse-detection, stderr, cause-chain), `parseRateLimitReset` (header
70
+ variants), `computeRateLimitWait` (future / null / past reset, jitter
71
+ bounds), `ghWithRateLimitRetry` (success, propagation, retry-then-succeed,
72
+ exhausted retries), `wrapDollarWithGhRetry` (passthrough, retry,
73
+ propagation).
74
+ - [`tests/test-no-direct-gh-exec-rule.mjs`](./tests/test-no-direct-gh-exec-rule.mjs)
75
+ — RuleTester valid/invalid cases.
76
+ - Updated `tests/queue-config.test.mjs` and `tests/limits-display.test.mjs`
77
+ for the 50% threshold.
78
+
79
+ Documentation:
80
+ [`docs/case-studies/issue-1726/`](./docs/case-studies/issue-1726/README.md)
81
+ contains the failing run logs, root-cause analysis, fix breakdown, and
82
+ verification commands.
83
+
84
+ - bb0af8c: Fix `check-file-line-limits` CI failure on `main` after issue #1726 merge.
85
+
86
+ After PR #1726 (rate-limit safeguards) merged into `main`, the
87
+ `check-file-line-limits` job failed because three `.mjs` files crossed the
88
+ 1500-line hard limit:
89
+ - `src/hive.mjs` — 1500 → 1504 lines
90
+ - `src/limits.lib.mjs` — 1497 → 1501 lines
91
+ - `src/solve.repository.lib.mjs` — 1500 → 1501 lines
92
+
93
+ Two root causes combined: (1) the per-file marker block PR #1726 added was 4
94
+ lines (2 comment lines + import + `void`), with no headroom check; (2) ESLint's
95
+ `max-lines` rule was configured with `skipBlankLines: true, skipComments: true`
96
+ while the CI script counts raw `wc -l`, so `npm run lint` passed locally even
97
+ though the CI script would fail. Local lint and CI line-limit had silently
98
+ drifted apart. See
99
+ [`docs/case-studies/issue-1730`](./docs/case-studies/issue-1730/README.md)
100
+ for the timeline, log excerpts, and template comparison.
101
+
102
+ Fix:
103
+ - **Synchronize ESLint `max-lines` with the CI script** in
104
+ [`eslint.config.mjs`](./eslint.config.mjs) by setting `skipBlankLines: false,
105
+ skipComments: false`. Now `npm run lint` catches the failure locally before
106
+ push, restoring the invariant the rule's comment claimed.
107
+ - **Compact the rate-limit marker** introduced by #1726 from 4 lines to 1 line
108
+ in all 17 files. ESLint's existing `varsIgnorePattern: '^_'` means the
109
+ `void _wrapDollarWithGhRetry;` line was redundant; the trailing-comment form
110
+ preserves rate-limit awareness for `no-direct-gh-exec` while saving 3 lines
111
+ per file. Files: `src/hive.mjs`, `src/limits.lib.mjs`,
112
+ `src/{solve.session,solve.preparation,solve.progress-monitoring,solve.error-handlers,solve.feedback,solve.auto-pr,solve.branch-errors,hive.recheck,github.batch,bidirectional-interactive,token-sanitization}.lib.mjs`,
113
+ `src/youtrack/youtrack-sync.mjs`,
114
+ `scripts/{create-github-release,format-github-release,format-release-notes}.mjs`.
115
+ - **Compact `solve.repository.lib.mjs`** wrap pattern from 4 lines to 3 while
116
+ keeping the destructure form so `eslint-rules/no-direct-gh-exec.mjs` still
117
+ recognizes `wrapDollarWithGhRetry` in scope.
118
+
119
+ After the fix, all three previously-failing files are at or below 1500 raw
120
+ lines (1500 / 1498 / 1500) and `npm run lint` would now reject any
121
+ re-introduction of the regression.
122
+
3
123
  ## 1.59.5
4
124
 
5
125
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.59.5",
3
+ "version": "1.59.6",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -22,6 +22,7 @@
22
22
  * @experimental
23
23
  */
24
24
 
25
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
25
26
  // Configuration constants
26
27
  const CONFIG = {
27
28
  // Minimum time between comment checks to avoid rate limiting (in ms)
@@ -9,8 +9,9 @@ if (typeof globalThis.use === 'undefined') {
9
9
  globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
10
10
  }
11
11
 
12
- const { $ } = await use('command-stream');
13
-
12
+ const { $: __rawDollar$ } = await use('command-stream');
13
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
14
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
14
15
  /**
15
16
  * Common paths where contributing guidelines might be found
16
17
  */
@@ -13,8 +13,9 @@ if (typeof globalThis.use === 'undefined') {
13
13
  }
14
14
 
15
15
  const fs = (await use('fs')).promises;
16
- const { $ } = await use('command-stream');
17
-
16
+ const { $: __rawDollar$ } = await use('command-stream');
17
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
18
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
18
19
  const GITHUB_ISSUE_BODY_MAX_SIZE = 60000;
19
20
  const GITHUB_FILE_MAX_SIZE = 10 * 1024 * 1024;
20
21
 
@@ -13,8 +13,14 @@
13
13
 
14
14
  import { promisify } from 'util';
15
15
  import { exec as execCallback } from 'child_process';
16
-
17
- const exec = promisify(execCallback);
16
+ import { ghWithRateLimitRetry } from './github-rate-limit.lib.mjs';
17
+
18
+ const execRaw = promisify(execCallback);
19
+ // Issue #1726: rate-limit safe gh wrapper.
20
+ const exec = (cmd, opts) =>
21
+ ghWithRateLimitRetry(() => execRaw(cmd, opts), {
22
+ label: `gh exec (${cmd.split(/\s+/).slice(0, 3).join(' ')})`,
23
+ });
18
24
 
19
25
  /**
20
26
  * Get the committed date of a specific commit from GitHub API
@@ -11,8 +11,14 @@
11
11
  import { getWorkflowRunsForSha } from './github-merge.lib.mjs';
12
12
  import { promisify } from 'util';
13
13
  import { exec as execCallback } from 'child_process';
14
-
15
- const exec = promisify(execCallback);
14
+ import { ghWithRateLimitRetry } from './github-rate-limit.lib.mjs';
15
+
16
+ const execRaw = promisify(execCallback);
17
+ // Issue #1726: every gh call must be rate-limit safe.
18
+ const exec = (cmd, opts) =>
19
+ ghWithRateLimitRetry(() => execRaw(cmd, opts), {
20
+ label: `gh exec (${cmd.split(/\s+/).slice(0, 3).join(' ')})`,
21
+ });
16
22
 
17
23
  /**
18
24
  * Wait for all workflow runs triggered by a specific commit to complete
@@ -11,8 +11,14 @@
11
11
 
12
12
  import { promisify } from 'util';
13
13
  import { exec as execCallback } from 'child_process';
14
+ import { ghWithRateLimitRetry } from './github-rate-limit.lib.mjs';
14
15
 
15
- const exec = promisify(execCallback);
16
+ const execRaw = promisify(execCallback);
17
+ // Issue #1726: rate-limit safe gh wrapper.
18
+ const exec = (cmd, opts) =>
19
+ ghWithRateLimitRetry(() => execRaw(cmd, opts), {
20
+ label: `gh exec (${cmd.split(/\s+/).slice(0, 3).join(' ')})`,
21
+ });
16
22
 
17
23
  import { extractLinkedIssueNumber } from './github-linking.lib.mjs';
18
24
 
@@ -12,10 +12,16 @@
12
12
  import { promisify } from 'util';
13
13
  import { exec as execCallback } from 'child_process';
14
14
  import { githubLimits } from './config.lib.mjs';
15
+ import { ghWithRateLimitRetry } from './github-rate-limit.lib.mjs';
15
16
  const execRaw = promisify(execCallback);
16
17
  // Issue #1722: raise exec maxBuffer above Node's 1 MB default for paginated gh
17
18
  // API responses (workflow runs can easily exceed that on busy repos).
18
- const exec = (cmd, opts = {}) => execRaw(cmd, { maxBuffer: githubLimits.bufferMaxSize, ...opts });
19
+ // Issue #1726: wrap with rate-limit retry so a 5,000/hr quota hit waits for
20
+ // reset instead of bubbling up as a generic fetch failure.
21
+ const exec = (cmd, opts = {}) =>
22
+ ghWithRateLimitRetry(() => execRaw(cmd, { maxBuffer: githubLimits.bufferMaxSize, ...opts }), {
23
+ label: `gh exec (${cmd.split(/\s+/).slice(0, 3).join(' ')})`,
24
+ });
19
25
 
20
26
  // Statuses we treat as "not yet finished".
21
27
  const ACTIVE_RUN_STATUSES = ['in_progress', 'queued', 'waiting', 'requested', 'pending'];
@@ -18,13 +18,24 @@ const execRaw = promisify(execCallback);
18
18
 
19
19
  import { parseGitHubUrl } from './github.lib.mjs';
20
20
  import { githubLimits } from './config.lib.mjs';
21
+ import { ghWithRateLimitRetry } from './github-rate-limit.lib.mjs';
21
22
 
22
23
  // Issue #1722: gh api `--paginate --slurp` responses for repos with many
23
24
  // historical workflow runs can easily exceed Node's default 1 MB exec buffer
24
25
  // (observed 12.7 MB on this repo's main branch). Default to the configured
25
26
  // githubLimits.bufferMaxSize (10 MB; HIVE_MIND_GITHUB_BUFFER_MAX_SIZE) for all
26
27
  // gh calls in this file.
27
- const exec = (cmd, opts = {}) => execRaw(cmd, { maxBuffer: githubLimits.bufferMaxSize, ...opts });
28
+ //
29
+ // Issue #1726: every gh call in the merge subsystem must be rate-limit safe.
30
+ // Wrapping the local `exec` shim ensures all 25+ call sites pick up retry
31
+ // behaviour without per-call changes. Non-rate-limit errors continue to throw
32
+ // so genuine failures (404, auth, malformed JSON downstream) surface to the
33
+ // caller — they MUST NOT be swallowed as in the original /merge bug where a
34
+ // rate-limit error was silently treated as "no workflows".
35
+ const exec = (cmd, opts = {}) =>
36
+ ghWithRateLimitRetry(() => execRaw(cmd, { maxBuffer: githubLimits.bufferMaxSize, ...opts }), {
37
+ label: `gh exec (${cmd.split(/\s+/).slice(0, 3).join(' ')})`,
38
+ });
28
39
 
29
40
  // Issue #1413: Import ready tag sync, timeline, and label constant from separate module
30
41
  // to keep this file under the 1500 line limit
@@ -1340,40 +1351,37 @@ export async function getWorkflowRunJobsCount(owner, repo, runId, verbose = fals
1340
1351
  * @returns {Promise<{count: number, hasWorkflows: boolean, workflows: Array<{id: number, name: string, state: string, path: string}>}>}
1341
1352
  */
1342
1353
  export async function getActiveRepoWorkflows(owner, repo, verbose = false) {
1343
- try {
1344
- const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/workflows" --paginate --slurp`);
1345
- const allWorkflows = JSON.parse(stdout.trim() || '[]')
1346
- .flatMap(page => page.workflows || [])
1347
- .filter(workflow => workflow.state === 'active')
1348
- .map(workflow => ({ id: workflow.id, name: workflow.name, state: workflow.state, path: workflow.path }));
1349
-
1350
- // GitHub Pages workflows only run after merge and never produce PR check-runs.
1351
- const workflows = allWorkflows.filter(wf => !wf.path.startsWith('dynamic/pages/'));
1352
-
1353
- if (verbose) {
1354
- console.log(`[VERBOSE] /merge: Found ${allWorkflows.length} active workflows in ${owner}/${repo} (${workflows.length} PR-relevant after filtering out GitHub Pages deployment workflows)`);
1355
- for (const wf of allWorkflows) {
1356
- const filtered = wf.path.startsWith('dynamic/pages/');
1357
- console.log(`[VERBOSE] /merge: - ${wf.name} (${wf.id}): ${wf.state}, path=${wf.path}${filtered ? ' [excluded: GitHub Pages deployment]' : ''}`);
1358
- }
1359
- }
1354
+ // Issue #1726: this function previously swallowed every error as "no workflows",
1355
+ // including GitHub API rate-limit responses. The /merge command then thought CI
1356
+ // was unconfigured and proceeded as if checks had passed — a hard failure mode
1357
+ // visible in the original case-study log where errors were thrown but the
1358
+ // process exited 0.
1359
+ //
1360
+ // Rate-limit errors are now retried inside the local exec() wrapper. After
1361
+ // retries are exhausted, the error MUST propagate so callers can decide
1362
+ // whether to abort or continue — never default to "no workflows".
1363
+ const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/workflows" --paginate --slurp`);
1364
+ const allWorkflows = JSON.parse(stdout.trim() || '[]')
1365
+ .flatMap(page => page.workflows || [])
1366
+ .filter(workflow => workflow.state === 'active')
1367
+ .map(workflow => ({ id: workflow.id, name: workflow.name, state: workflow.state, path: workflow.path }));
1368
+
1369
+ // GitHub Pages workflows only run after merge and never produce PR check-runs.
1370
+ const workflows = allWorkflows.filter(wf => !wf.path.startsWith('dynamic/pages/'));
1360
1371
 
1361
- return {
1362
- count: workflows.length,
1363
- hasWorkflows: workflows.length > 0,
1364
- workflows,
1365
- };
1366
- } catch (error) {
1367
- if (verbose) {
1368
- console.log(`[VERBOSE] /merge: Error fetching workflows for ${owner}/${repo}: ${error.message}`);
1372
+ if (verbose) {
1373
+ console.log(`[VERBOSE] /merge: Found ${allWorkflows.length} active workflows in ${owner}/${repo} (${workflows.length} PR-relevant after filtering out GitHub Pages deployment workflows)`);
1374
+ for (const wf of allWorkflows) {
1375
+ const filtered = wf.path.startsWith('dynamic/pages/');
1376
+ console.log(`[VERBOSE] /merge: - ${wf.name} (${wf.id}): ${wf.state}, path=${wf.path}${filtered ? ' [excluded: GitHub Pages deployment]' : ''}`);
1369
1377
  }
1370
- // On error, assume no workflows (safer: avoids false positives in the no-CI case)
1371
- return {
1372
- count: 0,
1373
- hasWorkflows: false,
1374
- workflows: [],
1375
- };
1376
1378
  }
1379
+
1380
+ return {
1381
+ count: workflows.length,
1382
+ hasWorkflows: workflows.length > 0,
1383
+ workflows,
1384
+ };
1377
1385
  }
1378
1386
 
1379
1387
  // Issue #1690: Re-export CI signal helpers from separate module to keep this file under 1500 lines
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GitHub API rate-limit detection and retry utilities.
5
+ *
6
+ * Issue #1726: Hosted runners hit GitHub's 5,000/hr core API quota and bubble
7
+ * the failure up as a generic 403/HTTP error. The wrappers in lib.mjs only
8
+ * recognise transient TCP/TLS faults; rate-limit responses fell through and
9
+ * crashed callers (or worse, were silently swallowed in the merge subsystem
10
+ * making it look like "no workflows / no checks" — see
11
+ * src/github-merge.lib.mjs:getActiveRepoWorkflows in the original log).
12
+ *
13
+ * The retry policy required by the issue:
14
+ * wait = (resetTimestamp - now) + bufferMs (10 min) + random(jitterMs) (0-5 min)
15
+ *
16
+ * `bufferMs` and `jitterMs` already exist in src/config.lib.mjs#limitReset
17
+ * (added in #1236 for Claude limit waits) so we re-use them rather than
18
+ * duplicate constants.
19
+ */
20
+ import { promisify } from 'node:util';
21
+ import { exec as execCb } from 'node:child_process';
22
+
23
+ import { limitReset, retryLimits } from './config.lib.mjs';
24
+
25
+ const exec = promisify(execCb);
26
+
27
+ const RATE_LIMIT_PATTERNS = ['api rate limit exceeded', 'rate limit exceeded', 'you have exceeded a secondary rate limit', 'secondary rate limit', 'abuse detection', 'was submitted too quickly'];
28
+
29
+ /**
30
+ * Pull every plausible string out of a thrown error/result so pattern matches
31
+ * survive whatever shape the upstream caller gave us (Error, exec result with
32
+ * stdout/stderr, command-stream result, plain string, etc.).
33
+ */
34
+ const collectErrorText = error => {
35
+ if (!error) return '';
36
+ if (typeof error === 'string') return error;
37
+ const parts = [];
38
+ if (typeof error.message === 'string') parts.push(error.message);
39
+ if (typeof error.stderr === 'string') parts.push(error.stderr);
40
+ else if (error.stderr && typeof error.stderr.toString === 'function') parts.push(error.stderr.toString());
41
+ if (typeof error.stdout === 'string') parts.push(error.stdout);
42
+ else if (error.stdout && typeof error.stdout.toString === 'function') parts.push(error.stdout.toString());
43
+ if (error.cause) parts.push(collectErrorText(error.cause));
44
+ return parts.join('\n');
45
+ };
46
+
47
+ /**
48
+ * Detect whether `error` represents a GitHub rate-limit response.
49
+ * Recognises both primary (5,000/hr) and secondary (abuse-detection) forms.
50
+ *
51
+ * @param {unknown} error
52
+ * @returns {boolean}
53
+ */
54
+ export const isRateLimitError = error => {
55
+ const text = collectErrorText(error).toLowerCase();
56
+ if (!text) return false;
57
+ return RATE_LIMIT_PATTERNS.some(pattern => text.includes(pattern));
58
+ };
59
+
60
+ /**
61
+ * Extract a `Date` for when the rate-limit window resets, in priority order:
62
+ * 1. `X-RateLimit-Reset` header value (Unix epoch seconds) embedded in the
63
+ * error text — `gh` prints headers when --include is used and graphql
64
+ * surfaces them in the error body.
65
+ * 2. `Retry-After` header (seconds from now).
66
+ * 3. None — caller falls back to a polled `gh api rate_limit` lookup.
67
+ *
68
+ * @param {unknown} error
69
+ * @returns {Date|null}
70
+ */
71
+ export const parseRateLimitReset = error => {
72
+ const text = collectErrorText(error);
73
+ if (!text) return null;
74
+
75
+ const resetMatch = text.match(/x-ratelimit-reset:\s*(\d+)/i);
76
+ if (resetMatch) {
77
+ const epochSeconds = Number(resetMatch[1]);
78
+ if (Number.isFinite(epochSeconds) && epochSeconds > 0) {
79
+ return new Date(epochSeconds * 1000);
80
+ }
81
+ }
82
+
83
+ const retryAfterMatch = text.match(/retry-after:\s*(\d+)/i);
84
+ if (retryAfterMatch) {
85
+ const seconds = Number(retryAfterMatch[1]);
86
+ if (Number.isFinite(seconds) && seconds >= 0) {
87
+ return new Date(Date.now() + seconds * 1000);
88
+ }
89
+ }
90
+
91
+ return null;
92
+ };
93
+
94
+ /**
95
+ * Ask `gh api rate_limit` directly when the error didn't carry a reset header.
96
+ * Returns the most-restrictive (soonest) reset time across the resources we
97
+ * touch (core, search, graphql) so we don't resume into a still-throttled
98
+ * bucket.
99
+ *
100
+ * @returns {Promise<Date|null>}
101
+ */
102
+ export const fetchNextRateLimitReset = async () => {
103
+ try {
104
+ // eslint-disable-next-line gh-rate-limit/no-direct-gh-exec -- this IS the rate-limit helper; calling itself recursively would loop.
105
+ const { stdout } = await exec('gh api rate_limit');
106
+ const data = JSON.parse(stdout);
107
+ const resources = data?.resources || {};
108
+ const candidates = [];
109
+ for (const key of ['core', 'graphql', 'search']) {
110
+ const r = resources[key];
111
+ if (r && Number.isFinite(r.reset) && r.remaining === 0) {
112
+ candidates.push(r.reset);
113
+ }
114
+ }
115
+ if (candidates.length === 0) return null;
116
+ const soonestEpoch = Math.min(...candidates);
117
+ return new Date(soonestEpoch * 1000);
118
+ } catch {
119
+ return null;
120
+ }
121
+ };
122
+
123
+ /**
124
+ * Compute the absolute wait deadline that satisfies issue #1726:
125
+ * reset + bufferMs (default 10 min) + random(0..jitterMs) (default 0-5 min)
126
+ *
127
+ * @param {Date|null} reset
128
+ * @returns {{ waitMs: number, deadline: Date, reset: Date|null, bufferMs: number, jitterMs: number }}
129
+ */
130
+ export const computeRateLimitWait = (reset, now = Date.now()) => {
131
+ const bufferMs = limitReset.bufferMs;
132
+ const jitterMs = Math.floor(Math.random() * (limitReset.jitterMs + 1));
133
+ const resetTime = reset instanceof Date ? reset.getTime() : null;
134
+ const baselineWait = resetTime && resetTime > now ? resetTime - now : 0;
135
+ const waitMs = baselineWait + bufferMs + jitterMs;
136
+ return {
137
+ waitMs,
138
+ deadline: new Date(now + waitMs),
139
+ reset: reset || null,
140
+ bufferMs,
141
+ jitterMs,
142
+ };
143
+ };
144
+
145
+ /**
146
+ * Sleep with optional periodic countdown notifications.
147
+ *
148
+ * @param {number} ms
149
+ * @param {(msg: string) => Promise<void>|void} [log]
150
+ */
151
+ const sleepWithCountdown = async (ms, log) => {
152
+ if (ms <= 0) return;
153
+ if (!log || ms <= 60_000) {
154
+ await new Promise(resolve => setTimeout(resolve, ms));
155
+ return;
156
+ }
157
+ let remaining = ms;
158
+ const timer = setInterval(() => {
159
+ remaining -= 60_000;
160
+ if (remaining > 0) {
161
+ const minutes = Math.round(remaining / 60_000);
162
+ Promise.resolve(log(`⏳ Rate-limit wait: ${minutes} min remaining...`)).catch(() => {});
163
+ }
164
+ }, 60_000);
165
+ try {
166
+ await new Promise(resolve => setTimeout(resolve, ms));
167
+ } finally {
168
+ clearInterval(timer);
169
+ }
170
+ };
171
+
172
+ /**
173
+ * Wrap `fn` so that GitHub rate-limit errors are converted into a sleep until
174
+ * (resetTime + bufferMs + jitterMs) followed by a retry. Non-rate-limit errors
175
+ * are rethrown immediately so we don't mask programming bugs or 404s.
176
+ *
177
+ * @template T
178
+ * @param {() => Promise<T>} fn
179
+ * @param {object} [options]
180
+ * @param {number} [options.maxAttempts] - hard cap on rate-limit retries (default `retryLimits.maxApiRetries`).
181
+ * @param {string} [options.label] - prefix for log messages.
182
+ * @param {(msg: string) => Promise<void>|void} [options.log] - logger. Defaults to console.warn.
183
+ * @returns {Promise<T>}
184
+ */
185
+ export const ghWithRateLimitRetry = async (fn, options = {}) => {
186
+ const maxAttempts = options.maxAttempts ?? retryLimits.maxApiRetries;
187
+ const label = options.label || 'gh';
188
+ const log = options.log || (msg => console.warn(msg));
189
+
190
+ let lastError;
191
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
192
+ try {
193
+ return await fn();
194
+ } catch (error) {
195
+ lastError = error;
196
+ if (!isRateLimitError(error)) throw error;
197
+
198
+ if (attempt === maxAttempts) {
199
+ await Promise.resolve(log(`❌ ${label}: rate limit still active after ${attempt} attempts; giving up.`));
200
+ throw error;
201
+ }
202
+
203
+ const reset = parseRateLimitReset(error) || (await fetchNextRateLimitReset());
204
+ const { waitMs, deadline, bufferMs, jitterMs } = computeRateLimitWait(reset);
205
+ const waitMinutes = Math.round(waitMs / 60_000);
206
+ const resetSummary = reset ? `reset at ${reset.toISOString()}` : 'reset time unknown (using buffer + jitter only)';
207
+ await Promise.resolve(log(`⏳ ${label}: GitHub API rate limit hit (attempt ${attempt}/${maxAttempts}). Waiting ${waitMinutes} min (${resetSummary}; buffer ${Math.round(bufferMs / 60_000)} min + jitter ${Math.round(jitterMs / 1000)}s) until ${deadline.toISOString()}.`));
208
+ await sleepWithCountdown(waitMs, log);
209
+ }
210
+ }
211
+ // Unreachable — loop either returns or throws.
212
+ throw lastError;
213
+ };
214
+
215
+ /**
216
+ * Convenience wrapper around child_process.exec that retries on rate-limit
217
+ * errors. Use it for callers that build a `gh` command string and want the
218
+ * existing exec-based ergonomics.
219
+ *
220
+ * @param {string} command
221
+ * @param {object} [options] - forwarded to ghWithRateLimitRetry, plus `execOptions`.
222
+ * @returns {Promise<{stdout: string, stderr: string}>}
223
+ */
224
+ export const execGhWithRetry = async (command, options = {}) => {
225
+ const { execOptions, ...retryOptions } = options;
226
+ return ghWithRateLimitRetry(() => exec(command, execOptions), {
227
+ label: retryOptions.label || `gh exec (${command.split(/\s+/).slice(0, 3).join(' ')})`,
228
+ ...retryOptions,
229
+ });
230
+ };
231
+
232
+ /**
233
+ * Wrap a command-stream `$` tagged-template so every `$gh ...` it issues is
234
+ * retried on rate-limit errors. Returns a callable that delegates to the
235
+ * underlying `$` for non-`gh` commands and through `ghWithRateLimitRetry` for
236
+ * `gh ...` commands.
237
+ *
238
+ * Usage at the top of a file:
239
+ * const { $: rawDollar } = await use('command-stream');
240
+ * const $ = wrapDollarWithGhRetry(rawDollar);
241
+ *
242
+ * @template T
243
+ * @param {(strings: TemplateStringsArray, ...values: unknown[]) => Promise<T>} dollar
244
+ * @param {object} [options] - forwarded to ghWithRateLimitRetry per call.
245
+ * @returns {(strings: TemplateStringsArray, ...values: unknown[]) => Promise<T>}
246
+ */
247
+ export const wrapDollarWithGhRetry = (dollar, options = {}) => {
248
+ const wrapped = (strings, ...values) => {
249
+ // Reconstruct the literal command for inspection (sufficient — leading
250
+ // `gh ` is what we care about).
251
+ let preview = '';
252
+ for (let i = 0; i < strings.length; i++) {
253
+ preview += strings[i];
254
+ if (i < values.length) preview += String(values[i] ?? '');
255
+ }
256
+ const isGh = /^\s*gh(?:\s|$)/.test(preview);
257
+ if (!isGh) return dollar(strings, ...values);
258
+ return ghWithRateLimitRetry(() => dollar(strings, ...values), {
259
+ label: `$gh (${preview.trim().split(/\s+/).slice(0, 3).join(' ')})`,
260
+ ...options,
261
+ });
262
+ };
263
+ // Preserve a reference to the underlying $ for consumers that need it.
264
+ wrapped.raw = dollar;
265
+ return wrapped;
266
+ };
267
+
268
+ export default {
269
+ isRateLimitError,
270
+ parseRateLimitReset,
271
+ fetchNextRateLimitReset,
272
+ computeRateLimitWait,
273
+ ghWithRateLimitRetry,
274
+ execGhWithRetry,
275
+ wrapDollarWithGhRetry,
276
+ };
@@ -11,6 +11,7 @@ if (typeof globalThis.use === 'undefined') {
11
11
  import { log, cleanErrorMessage } from './lib.mjs';
12
12
  import { githubLimits, timeouts } from './config.lib.mjs';
13
13
 
14
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
14
15
  /**
15
16
  * Check if a PR body/title indicates it fixes/closes/resolves a specific issue number
16
17
  * GitHub auto-closes issues when PR body contains keywords like "fixes #123", "closes #123", "resolves #123"
package/src/hive.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // Import Sentry instrumentation first (must be before other imports)
3
3
  import './instrument.mjs';
4
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
4
5
  const earlyArgs = process.argv.slice(2);
5
6
  if (earlyArgs.includes('--version')) {
6
7
  const { getVersion } = await import('./version.lib.mjs');
@@ -28,7 +29,6 @@ if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
28
29
  // Reuse createYargsConfig from shared module to avoid duplication
29
30
  const { createYargsConfig } = await import('./hive.config.lib.mjs');
30
31
  const helpYargs = createYargsConfig(yargs(rawArgs)).version(false);
31
- // Show help and exit
32
32
  helpYargs.showHelp();
33
33
  process.exit(0);
34
34
  } catch (error) {
@@ -1497,4 +1497,4 @@ if (isRunningDirectly) {
1497
1497
  console.error('\nPlease report this issue at: https://github.com/link-assistant/hive-mind/issues');
1498
1498
  process.exit(1);
1499
1499
  }
1500
- } // End of main execution block
1500
+ }
@@ -5,6 +5,7 @@ import { log, cleanErrorMessage } from './lib.mjs';
5
5
  import { batchCheckPullRequestsForIssues, batchCheckArchivedRepositories } from './github.lib.mjs';
6
6
  import { reportError } from './sentry.lib.mjs';
7
7
 
8
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
8
9
  /**
9
10
  * Recheck conditions for an issue right before processing
10
11
  * This ensures the issue should still be processed even if conditions changed since queuing
package/src/lib.mjs CHANGED
@@ -478,12 +478,16 @@ export const isTransientNetworkError = error => {
478
478
  /**
479
479
  * Retry a GitHub CLI / API operation with exponential backoff on transient network errors.
480
480
  * Unlike the generic `retry()`, this function:
481
- * - Only retries on transient network errors (TCP reset, TLS timeout, etc.)
482
- * - Immediately rethrows non-transient errors (404, 403, auth failures)
481
+ * - Retries on transient network errors (TCP reset, TLS timeout, etc.)
482
+ * - Retries on GitHub API rate-limit errors, sleeping until reset + buffer + jitter
483
+ * (issue #1726 — see src/github-rate-limit.lib.mjs)
484
+ * - Immediately rethrows non-transient errors (404, 403 non-rate-limit, auth failures)
483
485
  * - Logs stderr to the log file when a command fails (fixing terminal/log parity)
484
486
  *
485
487
  * Issue #1536: Most gh commands had no retry logic, causing solve to abort on
486
488
  * intermittent network issues.
489
+ * Issue #1726: Rate limit errors silently surfaced as command failure with no retry,
490
+ * causing the merge subsystem to swallow them as "no workflows found".
487
491
  *
488
492
  * @param {Function} fn - Async function to execute (should call gh CLI or GitHub API)
489
493
  * @param {Object} [options] - Options
@@ -496,11 +500,20 @@ export const isTransientNetworkError = error => {
496
500
  */
497
501
  export const ghRetry = async (fn, options = {}) => {
498
502
  const { maxAttempts = 3, delay = 1000, backoff = 2, label = 'gh command' } = options;
503
+ const { isRateLimitError, parseRateLimitReset, fetchNextRateLimitReset, computeRateLimitWait } = await import('./github-rate-limit.lib.mjs');
499
504
 
500
505
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
501
506
  try {
502
507
  return await fn();
503
508
  } catch (error) {
509
+ if (isRateLimitError(error) && attempt < maxAttempts) {
510
+ const reset = parseRateLimitReset(error) || (await fetchNextRateLimitReset());
511
+ const { waitMs, deadline, bufferMs, jitterMs } = computeRateLimitWait(reset);
512
+ const resetSummary = reset ? `reset at ${reset.toISOString()}` : 'reset time unknown';
513
+ await log(`⏳ ${label}: GitHub API rate limit hit (attempt ${attempt}/${maxAttempts}). Waiting ${Math.round(waitMs / 60000)} min (${resetSummary}; buffer ${Math.round(bufferMs / 60000)} min + jitter ${Math.round(jitterMs / 1000)}s) until ${deadline.toISOString()}.`, { level: 'warn' });
514
+ await sleep(waitMs);
515
+ continue;
516
+ }
504
517
  if (isTransientNetworkError(error) && attempt < maxAttempts) {
505
518
  const waitTime = delay * Math.pow(backoff, attempt - 1);
506
519
  await log(`⚠️ ${label}: Network error (attempt ${attempt}/${maxAttempts}), retrying in ${waitTime / 1000}s...`, { level: 'warn' });
@@ -527,6 +540,7 @@ export const ghRetry = async (fn, options = {}) => {
527
540
  */
528
541
  export const ghCmdRetry = async (cmdFn, options = {}) => {
529
542
  const { maxAttempts = 3, delay = 1000, backoff = 2, label = 'gh command' } = options;
543
+ const { isRateLimitError, parseRateLimitReset, fetchNextRateLimitReset, computeRateLimitWait } = await import('./github-rate-limit.lib.mjs');
530
544
 
531
545
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
532
546
  const result = await cmdFn();
@@ -541,9 +555,21 @@ export const ghCmdRetry = async (cmdFn, options = {}) => {
541
555
  return result;
542
556
  }
543
557
 
544
- // Check if this is a transient network error worth retrying
545
558
  const combinedOutput = (result.stdout?.toString() || '') + ' ' + (result.stderr?.toString() || '');
546
- if (isTransientNetworkError({ message: combinedOutput }) && attempt < maxAttempts) {
559
+ const errorLike = { message: combinedOutput, stdout: result.stdout, stderr: result.stderr };
560
+
561
+ // Issue #1726: rate-limit errors deserve a long, deterministic wait.
562
+ if (isRateLimitError(errorLike) && attempt < maxAttempts) {
563
+ const reset = parseRateLimitReset(errorLike) || (await fetchNextRateLimitReset());
564
+ const { waitMs, deadline, bufferMs, jitterMs } = computeRateLimitWait(reset);
565
+ const resetSummary = reset ? `reset at ${reset.toISOString()}` : 'reset time unknown';
566
+ await log(`⏳ ${label}: GitHub API rate limit hit (attempt ${attempt}/${maxAttempts}). Waiting ${Math.round(waitMs / 60000)} min (${resetSummary}; buffer ${Math.round(bufferMs / 60000)} min + jitter ${Math.round(jitterMs / 1000)}s) until ${deadline.toISOString()}.`, { level: 'warn' });
567
+ await sleep(waitMs);
568
+ continue;
569
+ }
570
+
571
+ // Check if this is a transient network error worth retrying
572
+ if (isTransientNetworkError(errorLike) && attempt < maxAttempts) {
547
573
  const waitTime = delay * Math.pow(backoff, attempt - 1);
548
574
  await log(`⚠️ ${label}: Network error (attempt ${attempt}/${maxAttempts}), retrying in ${waitTime / 1000}s...`, { level: 'warn' });
549
575
  await sleep(waitTime);
@@ -12,6 +12,7 @@ import { promisify } from 'node:util';
12
12
  import dayjs from 'dayjs';
13
13
  import utc from 'dayjs/plugin/utc.js';
14
14
 
15
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
15
16
  // Initialize dayjs plugins
16
17
  dayjs.extend(utc);
17
18
 
@@ -18,8 +18,9 @@
18
18
  const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
19
19
 
20
20
  // Use command-stream for consistent $ behavior across runtimes
21
- const { $ } = await use('command-stream');
22
-
21
+ const { $: __rawDollar$ } = await use('command-stream');
22
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
23
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
23
24
  // Parse command line arguments
24
25
  const args = process.argv.slice(2);
25
26
 
@@ -88,7 +88,7 @@ function normalizeMetricName(name) {
88
88
  * (cpu (65% enqueue))
89
89
  * (claude-5-hour (65% dequeue-one-at-a-time))
90
90
  * (claude-weekly (97% dequeue-one-at-a-time))
91
- * (github-api (75% enqueue))
91
+ * (github-api (50% enqueue))
92
92
  * )
93
93
  * ```
94
94
  *
@@ -253,7 +253,11 @@ export const QUEUE_CONFIG = {
253
253
  claudeWeekly: getThresholdConfig('claudeWeekly', 'HIVE_MIND_CLAUDE_WEEKLY_THRESHOLD', 'HIVE_MIND_CLAUDE_WEEKLY_STRATEGY', 0.97, 'dequeue-one-at-a-time'),
254
254
  codex5Hour: getThresholdConfig('codex5Hour', 'HIVE_MIND_CODEX_5_HOUR_SESSION_THRESHOLD', 'HIVE_MIND_CODEX_5_HOUR_SESSION_STRATEGY', 0.65, 'dequeue-one-at-a-time'),
255
255
  codexWeekly: getThresholdConfig('codexWeekly', 'HIVE_MIND_CODEX_WEEKLY_THRESHOLD', 'HIVE_MIND_CODEX_WEEKLY_STRATEGY', 0.97, 'dequeue-one-at-a-time'),
256
- githubApi: getThresholdConfig('githubApi', 'HIVE_MIND_GITHUB_API_THRESHOLD', 'HIVE_MIND_GITHUB_API_STRATEGY', 0.75, 'enqueue'),
256
+ // Issue #1726: lowered default from 0.75 to 0.50 to start backing off earlier
257
+ // and leave a wider safety margin before the hard 5,000/hr ceiling. Hosted
258
+ // runners hit the ceiling repeatedly with the 75% setting (see
259
+ // docs/case-studies/issue-1726).
260
+ githubApi: getThresholdConfig('githubApi', 'HIVE_MIND_GITHUB_API_THRESHOLD', 'HIVE_MIND_GITHUB_API_STRATEGY', 0.5, 'enqueue'),
257
261
  },
258
262
 
259
263
  // Legacy flat threshold values for backward compatibility
@@ -265,7 +269,7 @@ export const QUEUE_CONFIG = {
265
269
  CLAUDE_WEEKLY_THRESHOLD: getThresholdConfig('claudeWeekly', 'HIVE_MIND_CLAUDE_WEEKLY_THRESHOLD', 'HIVE_MIND_CLAUDE_WEEKLY_STRATEGY', 0.97, 'dequeue-one-at-a-time').value,
266
270
  CODEX_5_HOUR_SESSION_THRESHOLD: getThresholdConfig('codex5Hour', 'HIVE_MIND_CODEX_5_HOUR_SESSION_THRESHOLD', 'HIVE_MIND_CODEX_5_HOUR_SESSION_STRATEGY', 0.65, 'dequeue-one-at-a-time').value,
267
271
  CODEX_WEEKLY_THRESHOLD: getThresholdConfig('codexWeekly', 'HIVE_MIND_CODEX_WEEKLY_THRESHOLD', 'HIVE_MIND_CODEX_WEEKLY_STRATEGY', 0.97, 'dequeue-one-at-a-time').value,
268
- GITHUB_API_THRESHOLD: getThresholdConfig('githubApi', 'HIVE_MIND_GITHUB_API_THRESHOLD', 'HIVE_MIND_GITHUB_API_STRATEGY', 0.75, 'enqueue').value,
272
+ GITHUB_API_THRESHOLD: getThresholdConfig('githubApi', 'HIVE_MIND_GITHUB_API_THRESHOLD', 'HIVE_MIND_GITHUB_API_STRATEGY', 0.5, 'enqueue').value,
269
273
 
270
274
  // Timing
271
275
  // MIN_START_INTERVAL_MS: Time to allow solve command to start actual claude process
package/src/review.mjs CHANGED
@@ -35,8 +35,9 @@ if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
35
35
  const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
36
36
 
37
37
  // Use command-stream for consistent $ behavior across runtimes
38
- const { $ } = await use('command-stream');
39
-
38
+ const { $: __rawDollar$ } = await use('command-stream');
39
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
40
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
40
41
  const yargs = (await use('yargs@latest')).default;
41
42
  const os = (await use('os')).default;
42
43
  const path = (await use('path')).default;
@@ -4,8 +4,9 @@
4
4
  const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
5
5
 
6
6
  // Use command-stream for consistent $ behavior across runtimes
7
- const { $ } = await use('command-stream');
8
-
7
+ const { $: __rawDollar$ } = await use('command-stream');
8
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
9
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
9
10
  const yargs = (await use('yargs@latest')).default;
10
11
  const path = (await use('path')).default;
11
12
  const fs = (await use('fs')).promises;
@@ -16,8 +16,14 @@
16
16
 
17
17
  import { promisify } from 'util';
18
18
  import { exec as execCallback } from 'child_process';
19
+ import { ghWithRateLimitRetry } from './github-rate-limit.lib.mjs';
19
20
 
20
- const exec = promisify(execCallback);
21
+ const execRaw = promisify(execCallback);
22
+ // Issue #1726: rate-limit safe gh wrapper.
23
+ const exec = (cmd, opts) =>
24
+ ghWithRateLimitRetry(() => execRaw(cmd, opts), {
25
+ label: `gh exec (${cmd.split(/\s+/).slice(0, 3).join(' ')})`,
26
+ });
21
27
 
22
28
  // Import retry utility (issue #1536)
23
29
  const lib = await import('./lib.mjs');
@@ -13,8 +13,9 @@ if (typeof globalThis.use === 'undefined') {
13
13
  const use = globalThis.use;
14
14
 
15
15
  // Use command-stream for consistent $ behavior across runtimes
16
- const { $ } = await use('command-stream');
17
-
16
+ const { $: __rawDollar$ } = await use('command-stream');
17
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
18
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
18
19
  // Import shared library functions
19
20
  const lib = await import('./lib.mjs');
20
21
  const { log, cleanErrorMessage } = lib;
@@ -18,8 +18,9 @@ if (typeof globalThis.use === 'undefined') {
18
18
  const use = globalThis.use;
19
19
 
20
20
  // Use command-stream for consistent $ behavior across runtimes
21
- const { $ } = await use('command-stream');
22
-
21
+ const { $: __rawDollar$ } = await use('command-stream');
22
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
23
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
23
24
  // Import shared library functions
24
25
  const lib = await import('./lib.mjs');
25
26
  const { log } = lib;
@@ -21,8 +21,9 @@ if (typeof globalThis.use === 'undefined') {
21
21
  const use = globalThis.use;
22
22
 
23
23
  // Use command-stream for consistent $ behavior across runtimes
24
- const { $ } = await use('command-stream');
25
-
24
+ const { $: __rawDollar$ } = await use('command-stream');
25
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
26
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
26
27
  // Import shared library functions
27
28
  const lib = await import('./lib.mjs');
28
29
  const { log, formatAligned } = lib;
@@ -17,8 +17,9 @@ if (typeof globalThis.use === 'undefined') {
17
17
  const use = globalThis.use;
18
18
 
19
19
  // Use command-stream for consistent $ behavior across runtimes
20
- const { $ } = await use('command-stream');
21
-
20
+ const { $: __rawDollar$ } = await use('command-stream');
21
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
22
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
22
23
  // Import shared library functions
23
24
  const lib = await import('./lib.mjs');
24
25
  const { log, cleanErrorMessage, formatAligned, getLogFile } = lib;
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { closingIssueNumbersContain, parseClosingIssueNumbers } from './pr-issue-linking.lib.mjs';
7
7
 
8
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
8
9
  export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNumber, owner, repo, defaultBranch, forkedRepo, isContinueMode, prNumber, log, formatAligned, $, reportError, path, fs }) {
9
10
  // Skip auto-PR creation if:
10
11
  // 1. Auto-PR creation is disabled AND we're not in continue mode with no PR
@@ -9,6 +9,7 @@
9
9
  // Import Sentry integration
10
10
  import { reportError } from './sentry.lib.mjs';
11
11
 
12
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
12
13
  export async function handleBranchCheckoutError({ branchName, prNumber, errorOutput, issueUrl, owner, repo, tempDir, argv, formatAligned, log, $ }) {
13
14
  // Check if this is a PR from a fork
14
15
  let isForkPR = false;
@@ -5,6 +5,7 @@
5
5
  // Import exit handler
6
6
  import { safeExit } from './exit-handler.lib.mjs';
7
7
 
8
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
8
9
  // Import Sentry integration
9
10
  import { reportError } from './sentry.lib.mjs';
10
11
 
@@ -12,8 +12,9 @@ if (typeof globalThis.use === 'undefined') {
12
12
  const use = globalThis.use;
13
13
 
14
14
  // Use command-stream for consistent $ behavior across runtimes
15
- const { $ } = await use('command-stream');
16
-
15
+ const { $: __rawDollar$ } = await use('command-stream');
16
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
17
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
17
18
  const os = (await use('os')).default;
18
19
  const path = (await use('path')).default;
19
20
  const fs = (await use('fs')).promises;
@@ -6,6 +6,7 @@
6
6
  // Import Sentry integration
7
7
  import { reportError } from './sentry.lib.mjs';
8
8
 
9
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
9
10
  export const detectAndCountFeedback = async params => {
10
11
  const { prNumber, branchName, owner, repo, issueNumber, isContinueMode, argv, mergeStateStatus, prState, workStartTime, log, formatAligned, cleanErrorMessage, $ } = params;
11
12
 
package/src/solve.mjs CHANGED
@@ -7,7 +7,9 @@ await handleSolveEarlyExit(earlyArgs);
7
7
 
8
8
  const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
9
9
  globalThis.use = use;
10
- const { $ } = await use('command-stream');
10
+ const { $: __rawDollar$ } = await use('command-stream');
11
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
12
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
11
13
  const config = await import('./solve.config.lib.mjs');
12
14
  const { initializeConfig, parseArguments } = config;
13
15
  // Import Sentry integration
@@ -3,6 +3,7 @@
3
3
  * Handles timestamp collection, feedback detection, and pre-execution checks
4
4
  */
5
5
 
6
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
6
7
  // Import feedback detection functionality
7
8
  const feedback = await import('./solve.feedback.lib.mjs');
8
9
  const { detectAndCountFeedback } = feedback;
@@ -28,6 +28,7 @@
28
28
  // comment is excluded from --auto-attach-solution-summary's AI-comment check.
29
29
  import { LIVE_PROGRESS_SECTION_START_MARKER, LIVE_PROGRESS_SECTION_END_MARKER, postTrackedCommentFromFile, trackToolCommentId } from './tool-comments.lib.mjs';
30
30
 
31
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
31
32
  /**
32
33
  * Configuration constants for progress monitoring
33
34
  */
@@ -11,9 +11,9 @@ if (typeof globalThis.use === 'undefined') {
11
11
  }
12
12
  const use = globalThis.use;
13
13
 
14
- // Use command-stream for consistent $ behavior across runtimes
15
- const { $ } = await use('command-stream');
16
-
14
+ // Use command-stream for consistent $ behavior; wrap with rate-limit retry (#1726)
15
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
16
+ const $ = wrapDollarWithGhRetry((await use('command-stream')).$);
17
17
  const os = (await use('os')).default;
18
18
  const path = (await use('path')).default;
19
19
  const fs = (await use('fs')).promises;
@@ -20,8 +20,9 @@ if (typeof globalThis.use === 'undefined') {
20
20
  const use = globalThis.use;
21
21
 
22
22
  // Use command-stream for consistent $ behavior across runtimes
23
- const { $ } = await use('command-stream');
24
-
23
+ const { $: __rawDollar$ } = await use('command-stream');
24
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
25
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
25
26
  // Import path and fs for cleanup operations
26
27
  const path = (await use('path')).default;
27
28
  const fs = (await use('fs')).promises;
@@ -12,8 +12,9 @@ if (typeof globalThis.use === 'undefined') {
12
12
  const use = globalThis.use;
13
13
 
14
14
  // Use command-stream for consistent $ behavior across runtimes
15
- const { $ } = await use('command-stream');
16
-
15
+ const { $: __rawDollar$ } = await use('command-stream');
16
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
17
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
17
18
  const path = (await use('path')).default;
18
19
 
19
20
  // Import shared library functions
@@ -8,6 +8,7 @@
8
8
  // in checkForAiCreatedComments() always matches what we actually posted.
9
9
  import { AI_WORK_SESSION_STARTED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER, AI_WORK_SESSION_RESUMED_MARKER, AUTO_RESUME_ON_LIMIT_RESET_MARKER, AUTO_RESTART_ON_LIMIT_RESET_MARKER, postTrackedComment } from './tool-comments.lib.mjs';
10
10
 
11
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
11
12
  /**
12
13
  * Session type definitions for different work session contexts
13
14
  * See: https://github.com/link-assistant/hive-mind/issues/1152
@@ -15,8 +15,9 @@ if (typeof globalThis.use === 'undefined') {
15
15
  const use = globalThis.use;
16
16
 
17
17
  // Use command-stream for consistent $ behavior across runtimes
18
- const { $ } = await use('command-stream');
19
-
18
+ const { $: __rawDollar$ } = await use('command-stream');
19
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
20
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
20
21
  // Import shared library functions
21
22
  const lib = await import('./lib.mjs');
22
23
  const { log, cleanErrorMessage, formatAligned, getLogFile } = lib;
@@ -18,8 +18,14 @@
18
18
 
19
19
  import { promisify } from 'util';
20
20
  import { exec as execCallback } from 'child_process';
21
+ import { ghWithRateLimitRetry } from './github-rate-limit.lib.mjs';
21
22
 
22
- const exec = promisify(execCallback);
23
+ const execRaw = promisify(execCallback);
24
+ // Issue #1726: rate-limit safe gh wrapper.
25
+ const exec = (cmd, opts) =>
26
+ ghWithRateLimitRetry(() => execRaw(cmd, opts), {
27
+ label: `gh exec (${cmd.split(/\s+/).slice(0, 3).join(' ')})`,
28
+ });
23
29
 
24
30
  /**
25
31
  * Escapes special characters in text for Telegram Markdown formatting
@@ -18,6 +18,7 @@
18
18
  import { maskToken, log, isENOSPC } from './lib.mjs';
19
19
  import { reportError } from './sentry.lib.mjs';
20
20
 
21
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
21
22
  // Dynamic imports for runtime dependencies
22
23
  const getOsModule = async () => (await import('os')).default;
23
24
  const getPathModule = async () => (await import('path')).default;
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from '../github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
2
3
 
3
4
  /**
4
5
  * YouTrack to GitHub Issue Synchronization Module