@link-assistant/hive-mind 1.23.10 → 1.23.12
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 +32 -0
- package/README.md +3 -4
- package/package.json +1 -1
- package/src/agent.lib.mjs +20 -5
- package/src/github-merge.lib.mjs +415 -0
- package/src/model-mapping.lib.mjs +20 -7
- package/src/model-validation.lib.mjs +27 -10
- package/src/solve.auto-merge.lib.mjs +196 -13
- package/src/solve.config.lib.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.23.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 50a69ae: Update free models: replace minimax-m2.1-free with minimax-m2.5-free
|
|
8
|
+
|
|
9
|
+
OpenCode Zen:
|
|
10
|
+
- Replace `minimax-m2.1-free` with `minimax-m2.5-free` (M2.1 no longer free)
|
|
11
|
+
- Remove `glm-4.7-free` from recommended free models (no longer free)
|
|
12
|
+
|
|
13
|
+
Kilo Gateway:
|
|
14
|
+
- Add `glm-4.5-air-free` (agent-centric model)
|
|
15
|
+
- Add `minimax-m2.5-free` (upgraded from M2.1)
|
|
16
|
+
- Add `deepseek-r1-free` (advanced reasoning model)
|
|
17
|
+
|
|
18
|
+
Breaking change: Users relying on `minimax-m2.1-free` or `glm-4.7-free` should switch to the updated models. Deprecated models are kept for backward compatibility but may not work.
|
|
19
|
+
|
|
20
|
+
## 1.23.11
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- f1ba29d: Comprehensive CI/CD status handling for --auto-restart-until-mergeable mode
|
|
25
|
+
- Detect when CI failures are caused by billing/spending limits via check run annotations
|
|
26
|
+
- For private repositories: Post an explanatory comment and stop (requires human intervention)
|
|
27
|
+
- For public repositories: Apply exponential backoff and wait (unusual case)
|
|
28
|
+
- Distinguish between CI failure, cancelled, pending, queued, and billing limit states
|
|
29
|
+
- Automatically re-trigger cancelled CI/CD workflow runs instead of restarting AI
|
|
30
|
+
- Only restart AI when genuine code failures occur (not for cancelled/pending/billing)
|
|
31
|
+
- Wait for all CI/CD checks to complete before deciding on AI restart
|
|
32
|
+
- New functions: getDetailedCIStatus(), rerunWorkflowRun(), rerunFailedJobs(), getWorkflowRunsForSha()
|
|
33
|
+
- Expanded test coverage: 45 tests covering all CI/CD status scenarios and decision logic
|
|
34
|
+
|
|
3
35
|
## 1.23.10
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -477,15 +477,14 @@ Examples:
|
|
|
477
477
|
|
|
478
478
|
Free Models (with --tool agent):
|
|
479
479
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kimi-k2.5-free
|
|
480
|
-
/solve https://github.com/owner/repo/issues/123 --tool agent --model minimax-m2.
|
|
480
|
+
/solve https://github.com/owner/repo/issues/123 --tool agent --model minimax-m2.5-free
|
|
481
481
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model gpt-5-nano
|
|
482
|
-
/solve https://github.com/owner/repo/issues/123 --tool agent --model glm-4.7-free
|
|
483
482
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model big-pickle
|
|
484
483
|
|
|
485
484
|
Free Models via Kilo Gateway (with --tool agent):
|
|
486
485
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-5-free
|
|
487
|
-
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-4.
|
|
488
|
-
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/
|
|
486
|
+
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-4.5-air-free
|
|
487
|
+
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/deepseek-r1-free
|
|
489
488
|
```
|
|
490
489
|
|
|
491
490
|
> **📖 Free Models Guide**: See [docs/FREE_MODELS.md](./docs/FREE_MODELS.md) for comprehensive information about all free models including OpenCode Zen and Kilo Gateway providers.
|
package/package.json
CHANGED
package/src/agent.lib.mjs
CHANGED
|
@@ -94,6 +94,7 @@ const getOriginalProviderName = providerId => {
|
|
|
94
94
|
moonshot: 'Moonshot AI',
|
|
95
95
|
google: 'Google',
|
|
96
96
|
opencode: 'OpenCode Zen',
|
|
97
|
+
kilo: 'Kilo Gateway',
|
|
97
98
|
grok: 'xAI',
|
|
98
99
|
};
|
|
99
100
|
|
|
@@ -210,9 +211,10 @@ export const calculateAgentPricing = async (modelId, tokenUsage) => {
|
|
|
210
211
|
|
|
211
212
|
const totalCost = inputCost + outputCost + cacheReadCost + cacheWriteCost + reasoningCost;
|
|
212
213
|
|
|
213
|
-
// Determine if this is a free model from OpenCode Zen
|
|
214
|
-
// Models accessed via OpenCode Zen are free, regardless of original provider pricing
|
|
215
|
-
|
|
214
|
+
// Determine if this is a free model from OpenCode Zen or Kilo Gateway
|
|
215
|
+
// Models accessed via OpenCode Zen or Kilo Gateway are free, regardless of original provider pricing
|
|
216
|
+
// Issue #1300: Added kilo provider detection for Kilo Gateway free models
|
|
217
|
+
const isOpencodeFreeModel = providerFromModel === 'opencode' || providerFromModel === 'kilo' || isFreeVariant || modelName.toLowerCase().includes('free') || modelName.toLowerCase().includes('grok') || providerFromModel === 'moonshot' || providerFromModel === 'openai' || providerFromModel === 'anthropic';
|
|
216
218
|
|
|
217
219
|
// Use base model's provider for original provider reference if available
|
|
218
220
|
const effectiveOriginalProvider = baseModelInfo?.provider || originalProvider || effectiveModelInfo?.provider || null;
|
|
@@ -282,23 +284,36 @@ export const calculateAgentPricing = async (modelId, tokenUsage) => {
|
|
|
282
284
|
// Model mapping to translate aliases to full model IDs for Agent
|
|
283
285
|
// Agent uses OpenCode Zen's JSON interface and models
|
|
284
286
|
// Issue #1185: Free models use opencode/ prefix (not openai/)
|
|
287
|
+
// Issue #1300: Updated mappings - use opencode/ and kilo/ prefixes only,
|
|
288
|
+
// short names for Kilo-exclusive models map to kilo/ prefix
|
|
285
289
|
export const mapModelToId = model => {
|
|
286
290
|
const modelMap = {
|
|
291
|
+
// OpenCode Zen free models
|
|
287
292
|
grok: 'opencode/grok-code',
|
|
288
293
|
'grok-code': 'opencode/grok-code',
|
|
289
294
|
'grok-code-fast-1': 'opencode/grok-code',
|
|
290
295
|
'big-pickle': 'opencode/big-pickle',
|
|
291
296
|
'gpt-5-nano': 'opencode/gpt-5-nano',
|
|
297
|
+
'minimax-m2.5-free': 'opencode/minimax-m2.5-free',
|
|
298
|
+
'kimi-k2.5-free': 'opencode/kimi-k2.5-free',
|
|
299
|
+
// Kilo Gateway free models - short names for Kilo-exclusive models (Issue #1300)
|
|
300
|
+
'glm-5-free': 'kilo/glm-5-free',
|
|
301
|
+
'glm-4.5-air-free': 'kilo/glm-4.5-air-free',
|
|
302
|
+
'deepseek-r1-free': 'kilo/deepseek-r1-free',
|
|
303
|
+
'giga-potato-free': 'kilo/giga-potato-free',
|
|
304
|
+
'trinity-large-preview': 'kilo/trinity-large-preview',
|
|
305
|
+
// Premium models
|
|
292
306
|
sonnet: 'anthropic/claude-3-5-sonnet',
|
|
293
307
|
haiku: 'anthropic/claude-3-5-haiku',
|
|
294
308
|
opus: 'anthropic/claude-3-opus',
|
|
295
309
|
'gemini-3-pro': 'google/gemini-3-pro',
|
|
296
|
-
// Free models mapping for issue #1250
|
|
297
|
-
'kimi-k2.5-free': 'moonshot/kimi-k2.5-free',
|
|
298
310
|
'gpt-4o-mini': 'openai/gpt-4o-mini',
|
|
299
311
|
'gpt-4o': 'openai/gpt-4o',
|
|
300
312
|
'claude-3.5-haiku': 'anthropic/claude-3.5-haiku',
|
|
301
313
|
'claude-3.5-sonnet': 'anthropic/claude-3.5-sonnet',
|
|
314
|
+
// Deprecated free models (backward compatibility)
|
|
315
|
+
'glm-4.7-free': 'opencode/glm-4.7-free',
|
|
316
|
+
'minimax-m2.1-free': 'opencode/minimax-m2.1-free',
|
|
302
317
|
};
|
|
303
318
|
|
|
304
319
|
// Return mapped model ID if it's an alias, otherwise return as-is
|
package/src/github-merge.lib.mjs
CHANGED
|
@@ -790,6 +790,411 @@ export async function getDefaultBranch(owner, repo, verbose = false) {
|
|
|
790
790
|
}
|
|
791
791
|
}
|
|
792
792
|
|
|
793
|
+
/**
|
|
794
|
+
* Get annotations for a check run
|
|
795
|
+
* Issue #1314: Used to detect billing limit errors
|
|
796
|
+
* @param {string} owner - Repository owner
|
|
797
|
+
* @param {string} repo - Repository name
|
|
798
|
+
* @param {number} checkRunId - Check run ID
|
|
799
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
800
|
+
* @returns {Promise<Array<Object>>} Array of annotation objects
|
|
801
|
+
*/
|
|
802
|
+
export async function getCheckRunAnnotations(owner, repo, checkRunId, verbose = false) {
|
|
803
|
+
try {
|
|
804
|
+
const { stdout } = await exec(`gh api repos/${owner}/${repo}/check-runs/${checkRunId}/annotations 2>/dev/null || echo "[]"`);
|
|
805
|
+
const annotations = JSON.parse(stdout.trim() || '[]');
|
|
806
|
+
|
|
807
|
+
if (verbose) {
|
|
808
|
+
console.log(`[VERBOSE] /merge: Check run ${checkRunId} has ${annotations.length} annotations`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return annotations;
|
|
812
|
+
} catch (error) {
|
|
813
|
+
if (verbose) {
|
|
814
|
+
console.log(`[VERBOSE] /merge: Error fetching annotations for check run ${checkRunId}: ${error.message}`);
|
|
815
|
+
}
|
|
816
|
+
return [];
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Check if repository is private
|
|
822
|
+
* Issue #1314: Used to determine behavior when billing limits are reached
|
|
823
|
+
* @param {string} owner - Repository owner
|
|
824
|
+
* @param {string} repo - Repository name
|
|
825
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
826
|
+
* @returns {Promise<{isPrivate: boolean, visibility: string|null}>}
|
|
827
|
+
*/
|
|
828
|
+
export async function getRepoVisibility(owner, repo, verbose = false) {
|
|
829
|
+
try {
|
|
830
|
+
const { stdout } = await exec(`gh api repos/${owner}/${repo} --jq '{isPrivate: .private, visibility: .visibility}'`);
|
|
831
|
+
const info = JSON.parse(stdout.trim());
|
|
832
|
+
|
|
833
|
+
if (verbose) {
|
|
834
|
+
console.log(`[VERBOSE] /merge: Repository ${owner}/${repo} visibility: ${info.visibility}, private: ${info.isPrivate}`);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
isPrivate: info.isPrivate === true,
|
|
839
|
+
visibility: info.visibility || null,
|
|
840
|
+
};
|
|
841
|
+
} catch (error) {
|
|
842
|
+
if (verbose) {
|
|
843
|
+
console.log(`[VERBOSE] /merge: Error checking repository visibility: ${error.message}`);
|
|
844
|
+
}
|
|
845
|
+
// Assume private if we can't determine (safer default)
|
|
846
|
+
return { isPrivate: true, visibility: null };
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Known billing limit error message pattern
|
|
852
|
+
* Issue #1314: This is the exact message GitHub uses for billing/spending limit errors
|
|
853
|
+
*/
|
|
854
|
+
export const BILLING_LIMIT_ERROR_PATTERN = 'The job was not started because recent account payments have failed or your spending limit needs to be increased';
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Check if CI failure is due to billing/spending limits
|
|
858
|
+
* Issue #1314: Detects when GitHub Actions jobs fail due to billing issues rather than code problems
|
|
859
|
+
*
|
|
860
|
+
* Detection criteria:
|
|
861
|
+
* 1. Job has conclusion='failure'
|
|
862
|
+
* 2. Job has empty steps array (no steps were executed)
|
|
863
|
+
* 3. Job has runner_id=0 or null (no runner was assigned)
|
|
864
|
+
* 4. Annotation contains the billing limit error message
|
|
865
|
+
*
|
|
866
|
+
* @param {string} owner - Repository owner
|
|
867
|
+
* @param {string} repo - Repository name
|
|
868
|
+
* @param {number} prNumber - Pull request number
|
|
869
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
870
|
+
* @returns {Promise<{isBillingLimitError: boolean, message: string|null, affectedJobs: string[], allJobsAffected: boolean}>}
|
|
871
|
+
*/
|
|
872
|
+
export async function checkForBillingLimitError(owner, repo, prNumber, verbose = false) {
|
|
873
|
+
try {
|
|
874
|
+
// Get the PR's head SHA
|
|
875
|
+
const { stdout: prJson } = await exec(`gh pr view ${prNumber} --repo ${owner}/${repo} --json headRefOid`);
|
|
876
|
+
const prData = JSON.parse(prJson.trim());
|
|
877
|
+
const sha = prData.headRefOid;
|
|
878
|
+
|
|
879
|
+
// Get workflow runs for this SHA
|
|
880
|
+
const { stdout: runsJson } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=10" --jq '.workflow_runs[].id'`);
|
|
881
|
+
const runIds = runsJson.trim().split('\n').filter(Boolean);
|
|
882
|
+
|
|
883
|
+
if (verbose) {
|
|
884
|
+
console.log(`[VERBOSE] /merge: Found ${runIds.length} workflow runs for PR #${prNumber} at SHA ${sha.substring(0, 7)}`);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const affectedJobs = [];
|
|
888
|
+
let totalJobs = 0;
|
|
889
|
+
|
|
890
|
+
// Check each workflow run's jobs
|
|
891
|
+
for (const runId of runIds) {
|
|
892
|
+
try {
|
|
893
|
+
const { stdout: jobsJson } = await exec(`gh api repos/${owner}/${repo}/actions/runs/${runId}/jobs --jq '.jobs'`);
|
|
894
|
+
const jobs = JSON.parse(jobsJson.trim() || '[]');
|
|
895
|
+
|
|
896
|
+
for (const job of jobs) {
|
|
897
|
+
totalJobs++;
|
|
898
|
+
|
|
899
|
+
// Check for billing limit indicators:
|
|
900
|
+
// 1. Conclusion is failure
|
|
901
|
+
// 2. Steps array is empty (no steps were executed)
|
|
902
|
+
// 3. Runner ID is 0 or null (no runner was assigned)
|
|
903
|
+
const hasNoSteps = !job.steps || job.steps.length === 0;
|
|
904
|
+
const hasNoRunner = job.runner_id === 0 || job.runner_id === null;
|
|
905
|
+
|
|
906
|
+
if (job.conclusion === 'failure' && hasNoSteps && hasNoRunner) {
|
|
907
|
+
// Fetch annotations to confirm billing limit error
|
|
908
|
+
const annotations = await getCheckRunAnnotations(owner, repo, job.id, verbose);
|
|
909
|
+
|
|
910
|
+
const billingAnnotation = annotations.find(a => a.message?.includes(BILLING_LIMIT_ERROR_PATTERN));
|
|
911
|
+
|
|
912
|
+
if (billingAnnotation) {
|
|
913
|
+
affectedJobs.push(job.name);
|
|
914
|
+
|
|
915
|
+
if (verbose) {
|
|
916
|
+
console.log(`[VERBOSE] /merge: Job "${job.name}" (ID: ${job.id}) failed due to billing limits`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
} catch (error) {
|
|
922
|
+
if (verbose) {
|
|
923
|
+
console.log(`[VERBOSE] /merge: Error checking jobs for run ${runId}: ${error.message}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const isBillingLimitError = affectedJobs.length > 0;
|
|
929
|
+
const allJobsAffected = totalJobs > 0 && affectedJobs.length === totalJobs;
|
|
930
|
+
|
|
931
|
+
if (verbose && isBillingLimitError) {
|
|
932
|
+
console.log(`[VERBOSE] /merge: Billing limit detected - ${affectedJobs.length}/${totalJobs} jobs affected`);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return {
|
|
936
|
+
isBillingLimitError,
|
|
937
|
+
message: isBillingLimitError ? BILLING_LIMIT_ERROR_PATTERN : null,
|
|
938
|
+
affectedJobs,
|
|
939
|
+
allJobsAffected,
|
|
940
|
+
};
|
|
941
|
+
} catch (error) {
|
|
942
|
+
if (verbose) {
|
|
943
|
+
console.log(`[VERBOSE] /merge: Error checking for billing limit: ${error.message}`);
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
isBillingLimitError: false,
|
|
947
|
+
message: null,
|
|
948
|
+
affectedJobs: [],
|
|
949
|
+
allJobsAffected: false,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Re-run all jobs in a workflow run
|
|
956
|
+
* Issue #1314: Used to re-trigger CI jobs that were cancelled or not started
|
|
957
|
+
* @param {string} owner - Repository owner
|
|
958
|
+
* @param {string} repo - Repository name
|
|
959
|
+
* @param {number} runId - Workflow run ID
|
|
960
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
961
|
+
* @returns {Promise<{success: boolean, error: string|null}>}
|
|
962
|
+
*/
|
|
963
|
+
export async function rerunWorkflowRun(owner, repo, runId, verbose = false) {
|
|
964
|
+
try {
|
|
965
|
+
await exec(`gh api repos/${owner}/${repo}/actions/runs/${runId}/rerun -X POST`);
|
|
966
|
+
// GitHub returns 201 on success
|
|
967
|
+
if (verbose) {
|
|
968
|
+
console.log(`[VERBOSE] /merge: Successfully triggered re-run for workflow ${runId}`);
|
|
969
|
+
}
|
|
970
|
+
return { success: true, error: null };
|
|
971
|
+
} catch (error) {
|
|
972
|
+
// exec throws when command exits non-zero (e.g., 404 Not Found)
|
|
973
|
+
const errorMessage = error.stderr?.trim() || error.stdout?.trim() || error.message;
|
|
974
|
+
if (verbose) {
|
|
975
|
+
console.log(`[VERBOSE] /merge: Failed to re-run workflow ${runId}: ${errorMessage}`);
|
|
976
|
+
}
|
|
977
|
+
return { success: false, error: errorMessage };
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Re-run only failed jobs in a workflow run
|
|
983
|
+
* Issue #1314: More targeted than full re-run, only retries failed jobs
|
|
984
|
+
* @param {string} owner - Repository owner
|
|
985
|
+
* @param {string} repo - Repository name
|
|
986
|
+
* @param {number} runId - Workflow run ID
|
|
987
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
988
|
+
* @returns {Promise<{success: boolean, error: string|null}>}
|
|
989
|
+
*/
|
|
990
|
+
export async function rerunFailedJobs(owner, repo, runId, verbose = false) {
|
|
991
|
+
try {
|
|
992
|
+
await exec(`gh api repos/${owner}/${repo}/actions/runs/${runId}/rerun-failed-jobs -X POST`);
|
|
993
|
+
// GitHub returns 201 on success
|
|
994
|
+
if (verbose) {
|
|
995
|
+
console.log(`[VERBOSE] /merge: Successfully triggered re-run of failed jobs for workflow ${runId}`);
|
|
996
|
+
}
|
|
997
|
+
return { success: true, error: null };
|
|
998
|
+
} catch (error) {
|
|
999
|
+
const errorMessage = error.stderr?.trim() || error.stdout?.trim() || error.message;
|
|
1000
|
+
if (verbose) {
|
|
1001
|
+
console.log(`[VERBOSE] /merge: Failed to re-run failed jobs for workflow ${runId}: ${errorMessage}`);
|
|
1002
|
+
}
|
|
1003
|
+
return { success: false, error: errorMessage };
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Get detailed CI status for a PR, distinguishing between different non-success states
|
|
1009
|
+
* Issue #1314: Enhanced version that separates cancelled, queued, and billing-limited states
|
|
1010
|
+
*
|
|
1011
|
+
* Possible returned statuses:
|
|
1012
|
+
* - 'success': All checks passed
|
|
1013
|
+
* - 'failure': Some checks failed (genuine code failures, timed_out, or action_required)
|
|
1014
|
+
* - 'cancelled': Some checks were cancelled or stale (need re-triggering)
|
|
1015
|
+
* - 'pending': Some checks are still running, queued, waiting, or requested
|
|
1016
|
+
* - 'billing_limit': Failures are due to billing/spending limits (determined by caller)
|
|
1017
|
+
* - 'no_checks': No CI checks found yet (race condition after push)
|
|
1018
|
+
* - 'unknown': Unable to determine status
|
|
1019
|
+
*
|
|
1020
|
+
* @param {string} owner - Repository owner
|
|
1021
|
+
* @param {string} repo - Repository name
|
|
1022
|
+
* @param {number} prNumber - Pull request number
|
|
1023
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
1024
|
+
* @returns {Promise<Object>} Detailed CI status object
|
|
1025
|
+
*/
|
|
1026
|
+
export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false) {
|
|
1027
|
+
try {
|
|
1028
|
+
// Get the PR's head SHA
|
|
1029
|
+
const { stdout: prJson } = await exec(`gh pr view ${prNumber} --repo ${owner}/${repo} --json headRefOid`);
|
|
1030
|
+
const prData = JSON.parse(prJson.trim());
|
|
1031
|
+
const sha = prData.headRefOid;
|
|
1032
|
+
|
|
1033
|
+
// Get check runs for this SHA
|
|
1034
|
+
const { stdout: checksJson } = await exec(`gh api repos/${owner}/${repo}/commits/${sha}/check-runs --paginate --jq '.check_runs'`);
|
|
1035
|
+
const checkRuns = JSON.parse(checksJson.trim() || '[]');
|
|
1036
|
+
|
|
1037
|
+
// Get commit statuses
|
|
1038
|
+
const { stdout: statusJson } = await exec(`gh api repos/${owner}/${repo}/commits/${sha}/status --jq '.statuses'`);
|
|
1039
|
+
const statuses = JSON.parse(statusJson.trim() || '[]');
|
|
1040
|
+
|
|
1041
|
+
// Build detailed checks list
|
|
1042
|
+
const allChecks = [
|
|
1043
|
+
...checkRuns.map(check => ({
|
|
1044
|
+
name: check.name,
|
|
1045
|
+
status: check.status, // queued, in_progress, completed
|
|
1046
|
+
conclusion: check.conclusion, // success, failure, cancelled, timed_out, skipped, neutral, action_required, stale, null
|
|
1047
|
+
type: 'check_run',
|
|
1048
|
+
id: check.id,
|
|
1049
|
+
})),
|
|
1050
|
+
...statuses.map(status => ({
|
|
1051
|
+
name: status.context,
|
|
1052
|
+
status: status.state === 'pending' ? 'in_progress' : 'completed',
|
|
1053
|
+
conclusion: status.state === 'pending' ? null : status.state === 'success' ? 'success' : status.state === 'failure' ? 'failure' : status.state,
|
|
1054
|
+
type: 'status',
|
|
1055
|
+
id: null,
|
|
1056
|
+
})),
|
|
1057
|
+
];
|
|
1058
|
+
|
|
1059
|
+
// No checks yet
|
|
1060
|
+
if (allChecks.length === 0) {
|
|
1061
|
+
if (verbose) {
|
|
1062
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks yet - treating as no_checks`);
|
|
1063
|
+
}
|
|
1064
|
+
return {
|
|
1065
|
+
status: 'no_checks',
|
|
1066
|
+
checks: [],
|
|
1067
|
+
sha,
|
|
1068
|
+
hasFailures: false,
|
|
1069
|
+
hasCancelled: false,
|
|
1070
|
+
hasStale: false,
|
|
1071
|
+
hasPending: false,
|
|
1072
|
+
hasQueued: false,
|
|
1073
|
+
allPassed: false,
|
|
1074
|
+
failedChecks: [],
|
|
1075
|
+
cancelledChecks: [],
|
|
1076
|
+
staleChecks: [],
|
|
1077
|
+
pendingChecks: [],
|
|
1078
|
+
queuedChecks: [],
|
|
1079
|
+
passedChecks: [],
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Categorize checks
|
|
1084
|
+
// Note: GitHub check run conclusions include: success, failure, cancelled, timed_out, skipped,
|
|
1085
|
+
// neutral, action_required, stale, null (not yet completed)
|
|
1086
|
+
// GitHub check run statuses include: queued, in_progress, completed, waiting, requested, pending
|
|
1087
|
+
const passedChecks = allChecks.filter(c => c.conclusion === 'success' || c.conclusion === 'skipped' || c.conclusion === 'neutral');
|
|
1088
|
+
const failedChecks = allChecks.filter(c => c.conclusion === 'failure' || c.conclusion === 'timed_out' || c.conclusion === 'action_required');
|
|
1089
|
+
const cancelledChecks = allChecks.filter(c => c.conclusion === 'cancelled');
|
|
1090
|
+
const staleChecks = allChecks.filter(c => c.conclusion === 'stale');
|
|
1091
|
+
const pendingChecks = allChecks.filter(c => (c.status === 'in_progress' || c.status === 'waiting' || c.status === 'requested' || c.status === 'pending') && c.conclusion === null);
|
|
1092
|
+
const queuedChecks = allChecks.filter(c => c.status === 'queued' && c.conclusion === null);
|
|
1093
|
+
|
|
1094
|
+
const hasFailures = failedChecks.length > 0;
|
|
1095
|
+
const hasCancelled = cancelledChecks.length > 0;
|
|
1096
|
+
const hasStale = staleChecks.length > 0;
|
|
1097
|
+
const hasPending = pendingChecks.length > 0;
|
|
1098
|
+
const hasQueued = queuedChecks.length > 0;
|
|
1099
|
+
const allPassed = !hasFailures && !hasCancelled && !hasStale && !hasPending && !hasQueued && passedChecks.length === allChecks.length;
|
|
1100
|
+
|
|
1101
|
+
// Determine overall status
|
|
1102
|
+
let status;
|
|
1103
|
+
if (allPassed) {
|
|
1104
|
+
status = 'success';
|
|
1105
|
+
} else if (hasPending || hasQueued) {
|
|
1106
|
+
// Some checks are still running, queued, or waiting for a runner - wait for completion
|
|
1107
|
+
status = 'pending';
|
|
1108
|
+
} else if (hasStale && !hasFailures && !hasCancelled) {
|
|
1109
|
+
// Stale checks need to be re-triggered (similar to cancelled)
|
|
1110
|
+
status = 'cancelled';
|
|
1111
|
+
} else if (hasFailures && !hasCancelled && !hasStale) {
|
|
1112
|
+
status = 'failure';
|
|
1113
|
+
} else if ((hasCancelled || hasStale) && !hasFailures) {
|
|
1114
|
+
status = 'cancelled';
|
|
1115
|
+
} else if (hasFailures && (hasCancelled || hasStale)) {
|
|
1116
|
+
// Mixed: some failed, some cancelled/stale - report as failure (the failures need attention)
|
|
1117
|
+
status = 'failure';
|
|
1118
|
+
} else {
|
|
1119
|
+
status = 'unknown';
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (verbose) {
|
|
1123
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} detailed CI status: ${status}`);
|
|
1124
|
+
console.log(`[VERBOSE] /merge: Total: ${allChecks.length}, Passed: ${passedChecks.length}, Failed: ${failedChecks.length}, Cancelled: ${cancelledChecks.length}, Stale: ${staleChecks.length}, Pending: ${pendingChecks.length}, Queued: ${queuedChecks.length}`);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return {
|
|
1128
|
+
status,
|
|
1129
|
+
checks: allChecks,
|
|
1130
|
+
sha,
|
|
1131
|
+
hasFailures,
|
|
1132
|
+
hasCancelled,
|
|
1133
|
+
hasStale,
|
|
1134
|
+
hasPending,
|
|
1135
|
+
hasQueued,
|
|
1136
|
+
allPassed,
|
|
1137
|
+
failedChecks,
|
|
1138
|
+
cancelledChecks,
|
|
1139
|
+
staleChecks,
|
|
1140
|
+
pendingChecks,
|
|
1141
|
+
queuedChecks,
|
|
1142
|
+
passedChecks,
|
|
1143
|
+
};
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
if (verbose) {
|
|
1146
|
+
console.log(`[VERBOSE] /merge: Error getting detailed CI status: ${error.message}`);
|
|
1147
|
+
}
|
|
1148
|
+
return {
|
|
1149
|
+
status: 'unknown',
|
|
1150
|
+
checks: [],
|
|
1151
|
+
sha: null,
|
|
1152
|
+
hasFailures: false,
|
|
1153
|
+
hasCancelled: false,
|
|
1154
|
+
hasStale: false,
|
|
1155
|
+
hasPending: false,
|
|
1156
|
+
hasQueued: false,
|
|
1157
|
+
allPassed: false,
|
|
1158
|
+
failedChecks: [],
|
|
1159
|
+
cancelledChecks: [],
|
|
1160
|
+
staleChecks: [],
|
|
1161
|
+
pendingChecks: [],
|
|
1162
|
+
queuedChecks: [],
|
|
1163
|
+
passedChecks: [],
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Get workflow run IDs for a specific commit SHA
|
|
1170
|
+
* Issue #1314: Helper to find workflow runs to re-trigger
|
|
1171
|
+
* @param {string} owner - Repository owner
|
|
1172
|
+
* @param {string} repo - Repository name
|
|
1173
|
+
* @param {string} sha - Commit SHA
|
|
1174
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
1175
|
+
* @returns {Promise<Array<{id: number, status: string, conclusion: string|null, name: string}>>}
|
|
1176
|
+
*/
|
|
1177
|
+
export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
|
|
1178
|
+
try {
|
|
1179
|
+
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}]'`);
|
|
1180
|
+
const runs = JSON.parse(stdout.trim() || '[]');
|
|
1181
|
+
|
|
1182
|
+
if (verbose) {
|
|
1183
|
+
console.log(`[VERBOSE] /merge: Found ${runs.length} workflow runs for SHA ${sha.substring(0, 7)}`);
|
|
1184
|
+
for (const run of runs) {
|
|
1185
|
+
console.log(`[VERBOSE] /merge: - ${run.name} (${run.id}): status=${run.status}, conclusion=${run.conclusion}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
return runs;
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
if (verbose) {
|
|
1192
|
+
console.log(`[VERBOSE] /merge: Error fetching workflow runs for SHA ${sha}: ${error.message}`);
|
|
1193
|
+
}
|
|
1194
|
+
return [];
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
793
1198
|
export default {
|
|
794
1199
|
READY_LABEL,
|
|
795
1200
|
checkReadyLabelExists,
|
|
@@ -809,4 +1214,14 @@ export default {
|
|
|
809
1214
|
getActiveBranchRuns,
|
|
810
1215
|
waitForBranchCI,
|
|
811
1216
|
getDefaultBranch,
|
|
1217
|
+
// Issue #1314: Billing limit detection
|
|
1218
|
+
getCheckRunAnnotations,
|
|
1219
|
+
getRepoVisibility,
|
|
1220
|
+
checkForBillingLimitError,
|
|
1221
|
+
BILLING_LIMIT_ERROR_PATTERN,
|
|
1222
|
+
// Issue #1314: Enhanced CI status and re-run capabilities
|
|
1223
|
+
getDetailedCIStatus,
|
|
1224
|
+
rerunWorkflowRun,
|
|
1225
|
+
rerunFailedJobs,
|
|
1226
|
+
getWorkflowRunsForSha,
|
|
812
1227
|
};
|
|
@@ -26,23 +26,36 @@ export const claudeModels = {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
// Agent models (OpenCode API and Kilo Gateway via agent CLI)
|
|
29
|
+
// Issue #1300: Updated free models to match agent PR #191
|
|
29
30
|
export const agentModels = {
|
|
30
|
-
// OpenCode Zen free models
|
|
31
|
+
// OpenCode Zen free models (current)
|
|
31
32
|
grok: 'opencode/grok-code',
|
|
32
33
|
'grok-code': 'opencode/grok-code',
|
|
33
34
|
'grok-code-fast-1': 'opencode/grok-code',
|
|
34
35
|
'big-pickle': 'opencode/big-pickle',
|
|
35
36
|
'gpt-5-nano': 'opencode/gpt-5-nano',
|
|
36
|
-
'
|
|
37
|
-
'minimax-m2.1-free': 'opencode/minimax-m2.1-free',
|
|
37
|
+
'minimax-m2.5-free': 'opencode/minimax-m2.5-free', // New: upgraded from M2.1
|
|
38
38
|
'kimi-k2.5-free': 'opencode/kimi-k2.5-free',
|
|
39
|
-
// Kilo Gateway free models (Issue #1282)
|
|
39
|
+
// Kilo Gateway free models (Issue #1282, updated in #1300)
|
|
40
|
+
// Short names for Kilo-exclusive models (Issue #1300)
|
|
41
|
+
'glm-5-free': 'kilo/glm-5-free', // Kilo-exclusive
|
|
42
|
+
'glm-4.5-air-free': 'kilo/glm-4.5-air-free', // Kilo-exclusive: agent-centric model
|
|
43
|
+
'deepseek-r1-free': 'kilo/deepseek-r1-free', // Kilo-exclusive: reasoning model
|
|
44
|
+
'giga-potato-free': 'kilo/giga-potato-free', // Kilo-exclusive
|
|
45
|
+
'trinity-large-preview': 'kilo/trinity-large-preview', // Kilo-exclusive
|
|
46
|
+
// Full names with kilo/ prefix
|
|
40
47
|
'kilo/glm-5-free': 'kilo/glm-5-free',
|
|
41
|
-
'kilo/glm-4.
|
|
42
|
-
'kilo/
|
|
43
|
-
'kilo/
|
|
48
|
+
'kilo/glm-4.5-air-free': 'kilo/glm-4.5-air-free',
|
|
49
|
+
'kilo/minimax-m2.5-free': 'kilo/minimax-m2.5-free', // Also on OpenCode Zen
|
|
50
|
+
'kilo/deepseek-r1-free': 'kilo/deepseek-r1-free',
|
|
44
51
|
'kilo/giga-potato-free': 'kilo/giga-potato-free',
|
|
45
52
|
'kilo/trinity-large-preview': 'kilo/trinity-large-preview',
|
|
53
|
+
// Deprecated free models (kept for backward compatibility)
|
|
54
|
+
'glm-4.7-free': 'opencode/glm-4.7-free', // Deprecated: no longer free
|
|
55
|
+
'minimax-m2.1-free': 'opencode/minimax-m2.1-free', // Deprecated: replaced by m2.5
|
|
56
|
+
'kilo/glm-4.7-free': 'kilo/glm-4.7-free', // Deprecated: replaced by glm-4.5-air-free
|
|
57
|
+
'kilo/kimi-k2.5-free': 'kilo/kimi-k2.5-free', // Deprecated: not recommended
|
|
58
|
+
'kilo/minimax-m2.1-free': 'kilo/minimax-m2.1-free', // Deprecated: replaced by m2.5
|
|
46
59
|
// Premium models
|
|
47
60
|
sonnet: 'anthropic/claude-3-5-sonnet',
|
|
48
61
|
haiku: 'anthropic/claude-3-5-haiku',
|
|
@@ -94,23 +94,38 @@ export const CODEX_MODELS = {
|
|
|
94
94
|
export const AGENT_MODELS = {
|
|
95
95
|
// Free models (via OpenCode Zen)
|
|
96
96
|
// Issue #1185: Model IDs must use opencode/ prefix for OpenCode Zen models
|
|
97
|
+
// Issue #1300: Updated free models - minimax-m2.5-free replaces m2.1, glm-4.7-free removed
|
|
97
98
|
grok: 'opencode/grok-code',
|
|
98
99
|
'grok-code': 'opencode/grok-code',
|
|
99
100
|
'grok-code-fast-1': 'opencode/grok-code',
|
|
100
101
|
'big-pickle': 'opencode/big-pickle',
|
|
101
102
|
'gpt-5-nano': 'opencode/gpt-5-nano',
|
|
102
|
-
'
|
|
103
|
-
'minimax-m2.1-free': 'opencode/minimax-m2.1-free',
|
|
103
|
+
'minimax-m2.5-free': 'opencode/minimax-m2.5-free', // Upgraded from M2.1 (Issue #1300)
|
|
104
104
|
'kimi-k2.5-free': 'opencode/kimi-k2.5-free',
|
|
105
105
|
// Free models (via Kilo Gateway)
|
|
106
106
|
// Issue #1282: Kilo provider adds access to 500+ models including free tier
|
|
107
|
+
// Issue #1300: Updated Kilo free models with new offerings
|
|
107
108
|
// See: https://kilo.ai/docs/advanced-usage/free-and-budget-models
|
|
108
|
-
|
|
109
|
-
'
|
|
110
|
-
'
|
|
111
|
-
'
|
|
112
|
-
'
|
|
113
|
-
'
|
|
109
|
+
// Short names for Kilo-exclusive models (Issue #1300)
|
|
110
|
+
'glm-5-free': 'kilo/glm-5-free', // Kilo-exclusive: Z.AI flagship model
|
|
111
|
+
'glm-4.5-air-free': 'kilo/glm-4.5-air-free', // Kilo-exclusive: Z.AI agent-centric model
|
|
112
|
+
'deepseek-r1-free': 'kilo/deepseek-r1-free', // Kilo-exclusive: DeepSeek reasoning model
|
|
113
|
+
'giga-potato-free': 'kilo/giga-potato-free', // Kilo-exclusive: Evaluation model
|
|
114
|
+
'trinity-large-preview': 'kilo/trinity-large-preview', // Kilo-exclusive: Arcee AI preview
|
|
115
|
+
// Full names with kilo/ prefix
|
|
116
|
+
'kilo/glm-5-free': 'kilo/glm-5-free',
|
|
117
|
+
'kilo/glm-4.5-air-free': 'kilo/glm-4.5-air-free',
|
|
118
|
+
'kilo/minimax-m2.5-free': 'kilo/minimax-m2.5-free', // Also on OpenCode Zen
|
|
119
|
+
'kilo/deepseek-r1-free': 'kilo/deepseek-r1-free',
|
|
120
|
+
'kilo/giga-potato-free': 'kilo/giga-potato-free',
|
|
121
|
+
'kilo/trinity-large-preview': 'kilo/trinity-large-preview',
|
|
122
|
+
// Deprecated free models (kept for backward compatibility)
|
|
123
|
+
// These models are no longer the recommended options but may still work
|
|
124
|
+
'glm-4.7-free': 'opencode/glm-4.7-free', // Deprecated: no longer free on OpenCode Zen
|
|
125
|
+
'minimax-m2.1-free': 'opencode/minimax-m2.1-free', // Deprecated: replaced by m2.5
|
|
126
|
+
'kilo/glm-4.7-free': 'kilo/glm-4.7-free', // Deprecated: replaced by glm-4.5-air-free
|
|
127
|
+
'kilo/kimi-k2.5-free': 'kilo/kimi-k2.5-free', // Deprecated: not recommended
|
|
128
|
+
'kilo/minimax-m2.1-free': 'kilo/minimax-m2.1-free', // Deprecated: replaced by m2.5
|
|
114
129
|
// Premium models (requires OpenCode Zen subscription)
|
|
115
130
|
sonnet: 'anthropic/claude-3-5-sonnet',
|
|
116
131
|
haiku: 'anthropic/claude-3-5-haiku',
|
|
@@ -120,9 +135,11 @@ export const AGENT_MODELS = {
|
|
|
120
135
|
'opencode/grok-code': 'opencode/grok-code',
|
|
121
136
|
'opencode/big-pickle': 'opencode/big-pickle',
|
|
122
137
|
'opencode/gpt-5-nano': 'opencode/gpt-5-nano',
|
|
123
|
-
'opencode/
|
|
124
|
-
'opencode/minimax-m2.1-free': 'opencode/minimax-m2.1-free',
|
|
138
|
+
'opencode/minimax-m2.5-free': 'opencode/minimax-m2.5-free', // New (Issue #1300)
|
|
125
139
|
'opencode/kimi-k2.5-free': 'opencode/kimi-k2.5-free',
|
|
140
|
+
// Deprecated OpenCode Zen models (kept for backward compatibility)
|
|
141
|
+
'opencode/glm-4.7-free': 'opencode/glm-4.7-free', // Deprecated
|
|
142
|
+
'opencode/minimax-m2.1-free': 'opencode/minimax-m2.1-free', // Deprecated
|
|
126
143
|
// Premium models with provider prefix
|
|
127
144
|
'anthropic/claude-3-5-sonnet': 'anthropic/claude-3-5-sonnet',
|
|
128
145
|
'anthropic/claude-3-5-haiku': 'anthropic/claude-3-5-haiku',
|
|
@@ -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 {
|
|
36
|
+
const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, checkForBillingLimitError, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha } = githubMergeLib;
|
|
37
37
|
|
|
38
38
|
// Import GitHub functions for log attachment
|
|
39
39
|
const githubLib = await import('./github.lib.mjs');
|
|
@@ -141,23 +141,93 @@ const checkForNonBotComments = async (owner, repo, prNumber, issueNumber, lastCh
|
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
143
|
* Get the reasons why PR is not mergeable
|
|
144
|
+
* Issue #1314: Comprehensive CI/CD status handling covering all possible states:
|
|
145
|
+
* - success: All CI passed → no blocker
|
|
146
|
+
* - failure: Genuine code failures → restart AI
|
|
147
|
+
* - cancelled: Manually cancelled or workflow cancelled → re-trigger, don't restart AI
|
|
148
|
+
* - pending/queued: Still running or waiting for runner → wait, don't restart AI
|
|
149
|
+
* - billing_limit: Billing/spending limit reached → stop (private) or wait (public)
|
|
150
|
+
* - no_checks: No CI checks yet (race condition) → wait
|
|
144
151
|
*/
|
|
145
152
|
const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
|
|
146
153
|
const blockers = [];
|
|
147
154
|
|
|
148
|
-
//
|
|
149
|
-
const ciStatus = await
|
|
150
|
-
|
|
155
|
+
// Use detailed CI status to distinguish between all possible states
|
|
156
|
+
const ciStatus = await getDetailedCIStatus(owner, repo, prNumber, verbose);
|
|
157
|
+
|
|
158
|
+
if (ciStatus.status === 'no_checks') {
|
|
159
|
+
// No CI checks exist yet - race condition after push, treat as pending
|
|
151
160
|
blockers.push({
|
|
152
|
-
type: '
|
|
153
|
-
message: 'CI/CD checks
|
|
154
|
-
details:
|
|
161
|
+
type: 'ci_pending',
|
|
162
|
+
message: 'CI/CD checks have not started yet (waiting for checks to appear)',
|
|
163
|
+
details: [],
|
|
155
164
|
});
|
|
156
165
|
} else if (ciStatus.status === 'pending') {
|
|
166
|
+
// CI is still running or queued - wait for completion
|
|
167
|
+
const pendingNames = [...ciStatus.pendingChecks, ...ciStatus.queuedChecks].map(c => c.name);
|
|
168
|
+
blockers.push({
|
|
169
|
+
type: 'ci_pending',
|
|
170
|
+
message: 'CI/CD checks are still running or queued',
|
|
171
|
+
details: pendingNames,
|
|
172
|
+
});
|
|
173
|
+
} else if (ciStatus.status === 'cancelled') {
|
|
174
|
+
// All non-passed checks are cancelled or stale (no genuine failures)
|
|
175
|
+
// First check if this is actually a billing limit issue (billing-limited jobs may appear as cancelled)
|
|
176
|
+
const billingCheck = await checkForBillingLimitError(owner, repo, prNumber, verbose);
|
|
177
|
+
if (billingCheck.isBillingLimitError) {
|
|
178
|
+
blockers.push({
|
|
179
|
+
type: 'billing_limit',
|
|
180
|
+
message: 'GitHub Actions billing/spending limit reached',
|
|
181
|
+
details: billingCheck.affectedJobs,
|
|
182
|
+
allJobsAffected: billingCheck.allJobsAffected,
|
|
183
|
+
billingMessage: billingCheck.message,
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
// These need to be re-triggered, NOT treated as AI-fixable failures
|
|
187
|
+
const cancelledOrStaleChecks = [...ciStatus.cancelledChecks, ...(ciStatus.staleChecks || [])];
|
|
188
|
+
blockers.push({
|
|
189
|
+
type: 'ci_cancelled',
|
|
190
|
+
message: 'CI/CD checks were cancelled or became stale',
|
|
191
|
+
details: cancelledOrStaleChecks.map(c => c.name),
|
|
192
|
+
sha: ciStatus.sha,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
} else if (ciStatus.status === 'failure') {
|
|
196
|
+
// Some checks genuinely failed - check if it's billing limits first
|
|
197
|
+
const billingCheck = await checkForBillingLimitError(owner, repo, prNumber, verbose);
|
|
198
|
+
|
|
199
|
+
if (billingCheck.isBillingLimitError) {
|
|
200
|
+
blockers.push({
|
|
201
|
+
type: 'billing_limit',
|
|
202
|
+
message: 'GitHub Actions billing/spending limit reached',
|
|
203
|
+
details: billingCheck.affectedJobs,
|
|
204
|
+
allJobsAffected: billingCheck.allJobsAffected,
|
|
205
|
+
billingMessage: billingCheck.message,
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
// Check if there are also cancelled/stale checks alongside failures
|
|
209
|
+
const cancelledOrStaleChecks = [...(ciStatus.hasCancelled ? ciStatus.cancelledChecks : []), ...((ciStatus.hasStale && ciStatus.staleChecks) || [])];
|
|
210
|
+
if (cancelledOrStaleChecks.length > 0) {
|
|
211
|
+
blockers.push({
|
|
212
|
+
type: 'ci_cancelled',
|
|
213
|
+
message: 'Some CI/CD checks were cancelled or became stale (will be re-triggered)',
|
|
214
|
+
details: cancelledOrStaleChecks.map(c => c.name),
|
|
215
|
+
sha: ciStatus.sha,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
blockers.push({
|
|
219
|
+
type: 'ci_failure',
|
|
220
|
+
message: 'CI/CD checks are failing',
|
|
221
|
+
details: ciStatus.failedChecks.map(c => c.name),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
} else if (ciStatus.status === 'unknown') {
|
|
225
|
+
// Unable to determine CI status - treat as pending to be safe
|
|
226
|
+
// Do NOT treat as mergeable (which would be incorrect)
|
|
157
227
|
blockers.push({
|
|
158
228
|
type: 'ci_pending',
|
|
159
|
-
message: 'CI/CD
|
|
160
|
-
details:
|
|
229
|
+
message: 'CI/CD status could not be determined (will retry)',
|
|
230
|
+
details: [],
|
|
161
231
|
});
|
|
162
232
|
}
|
|
163
233
|
|
|
@@ -303,9 +373,112 @@ export const watchUntilMergeable = async params => {
|
|
|
303
373
|
feedbackLines.push('Please review and address the feedback from these comments.');
|
|
304
374
|
}
|
|
305
375
|
|
|
306
|
-
//
|
|
376
|
+
// Issue #1314: Check for billing limit errors BEFORE regular CI failures
|
|
377
|
+
// Billing limits require human intervention and should NOT trigger AI restarts
|
|
378
|
+
const billingBlocker = blockers.find(b => b.type === 'billing_limit');
|
|
379
|
+
if (billingBlocker) {
|
|
380
|
+
await log('');
|
|
381
|
+
await log(formatAligned('💳', 'GITHUB ACTIONS BILLING LIMIT DETECTED', ''));
|
|
382
|
+
await log(formatAligned('', 'Affected jobs:', billingBlocker.details.join(', '), 2));
|
|
383
|
+
await log(formatAligned('', 'All jobs affected:', billingBlocker.allJobsAffected ? 'Yes' : 'No', 2));
|
|
384
|
+
await log('');
|
|
385
|
+
|
|
386
|
+
// Check if this is a private repository
|
|
387
|
+
const repoInfo = await getRepoVisibility(owner, repo, argv.verbose);
|
|
388
|
+
|
|
389
|
+
if (repoInfo.isPrivate) {
|
|
390
|
+
// For private repos, human intervention is required - stop and post comment
|
|
391
|
+
await log(formatAligned('🛑', 'STOPPING', 'Private repository - billing limit requires human intervention'));
|
|
392
|
+
await log(formatAligned('', 'Action required:', "Check the 'Billing & plans' section in your GitHub settings", 2));
|
|
393
|
+
|
|
394
|
+
// Post comment explaining the billing limit issue
|
|
395
|
+
try {
|
|
396
|
+
const commentBody = `## 💳 GitHub Actions Billing Limit Reached
|
|
397
|
+
|
|
398
|
+
The CI/CD jobs could not start due to billing/spending limits.
|
|
399
|
+
|
|
400
|
+
**Affected jobs:**
|
|
401
|
+
${billingBlocker.details.map(j => `- ${j}`).join('\n')}
|
|
402
|
+
|
|
403
|
+
**Error message:**
|
|
404
|
+
> ${billingBlocker.billingMessage || BILLING_LIMIT_ERROR_PATTERN}
|
|
405
|
+
|
|
406
|
+
**Action Required:**
|
|
407
|
+
Please check the 'Billing & plans' section in your GitHub settings and either:
|
|
408
|
+
1. Add or update your payment method
|
|
409
|
+
2. Increase your spending limit
|
|
410
|
+
3. Wait for the free tier limits to reset (if applicable)
|
|
411
|
+
|
|
412
|
+
Once the billing issue is resolved, you can re-run the CI checks or push a new commit to trigger a new run.
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
*Detected by hive-mind with --auto-restart-until-mergeable flag. This is NOT a code issue - human intervention is required.*`;
|
|
416
|
+
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
417
|
+
await log(formatAligned('', '💬 Posted billing limit notification to PR', '', 2));
|
|
418
|
+
} catch (commentError) {
|
|
419
|
+
reportError(commentError, {
|
|
420
|
+
context: 'post_billing_limit_comment',
|
|
421
|
+
owner,
|
|
422
|
+
repo,
|
|
423
|
+
prNumber,
|
|
424
|
+
operation: 'comment_on_pr',
|
|
425
|
+
});
|
|
426
|
+
await log(formatAligned('', '⚠️ Could not post comment to PR', '', 2));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return { success: false, reason: 'billing_limit', latestSessionId, latestAnthropicCost };
|
|
430
|
+
} else {
|
|
431
|
+
// For public repos (unusual case), apply exponential backoff and wait
|
|
432
|
+
// Public repos typically have unlimited free CI, so this is unexpected
|
|
433
|
+
await log(formatAligned('⏳', 'Public repository with billing limit (unusual)', 'Applying exponential backoff'));
|
|
434
|
+
await log(formatAligned('', 'Next check in:', `${currentBackoffSeconds} seconds`, 2));
|
|
435
|
+
|
|
436
|
+
// Don't trigger AI restart - just wait and check again
|
|
437
|
+
// The backoff will be applied at the end of the loop
|
|
438
|
+
currentBackoffSeconds = Math.min(currentBackoffSeconds * 2, 3600); // Max 1 hour
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Issue #1314: Handle cancelled CI/CD checks - re-trigger them instead of restarting AI
|
|
443
|
+
// Cancelled checks (e.g., manually cancelled, cancelled by another workflow) should be
|
|
444
|
+
// re-triggered automatically. We should NOT restart the AI for these.
|
|
445
|
+
const cancelledBlocker = blockers.find(b => b.type === 'ci_cancelled');
|
|
446
|
+
if (cancelledBlocker && !billingBlocker) {
|
|
447
|
+
await log('');
|
|
448
|
+
await log(formatAligned('🔄', 'CANCELLED CI/CD CHECKS DETECTED', ''));
|
|
449
|
+
await log(formatAligned('', 'Cancelled checks:', cancelledBlocker.details.join(', '), 2));
|
|
450
|
+
|
|
451
|
+
// Attempt to re-trigger the cancelled/stale workflow runs
|
|
452
|
+
const sha = cancelledBlocker.sha;
|
|
453
|
+
if (sha) {
|
|
454
|
+
const runs = await getWorkflowRunsForSha(owner, repo, sha, argv.verbose);
|
|
455
|
+
const retriggerable = runs.filter(r => r.conclusion === 'cancelled' || r.conclusion === 'stale');
|
|
456
|
+
let rerunTriggered = false;
|
|
457
|
+
|
|
458
|
+
for (const run of retriggerable) {
|
|
459
|
+
await log(formatAligned('', `Re-triggering workflow "${run.name}" (${run.id})...`, '', 2));
|
|
460
|
+
const rerunResult = await rerunWorkflowRun(owner, repo, run.id, argv.verbose);
|
|
461
|
+
if (rerunResult.success) {
|
|
462
|
+
await log(formatAligned('', `✅ Re-triggered: ${run.name}`, '', 2));
|
|
463
|
+
rerunTriggered = true;
|
|
464
|
+
} else {
|
|
465
|
+
await log(formatAligned('', `⚠️ Could not re-trigger ${run.name}: ${rerunResult.error}`, '', 2));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (rerunTriggered) {
|
|
470
|
+
await log(formatAligned('⏳', 'Waiting for re-triggered CI to complete...', '', 2));
|
|
471
|
+
// Don't restart AI - just wait for re-triggered jobs to complete
|
|
472
|
+
// The next iteration of the loop will check the new status
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Don't set shouldRestart for cancelled checks - wait for re-triggered jobs instead
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Reason 2: CI failures (only if NOT a billing limit issue and NOT just cancelled)
|
|
479
|
+
// Only restart AI when we have genuine code failures (real feedback to act on)
|
|
307
480
|
const ciBlocker = blockers.find(b => b.type === 'ci_failure');
|
|
308
|
-
if (ciBlocker) {
|
|
481
|
+
if (ciBlocker && !billingBlocker) {
|
|
309
482
|
shouldRestart = true;
|
|
310
483
|
restartReason = restartReason ? `${restartReason}; CI failures` : 'CI failures detected';
|
|
311
484
|
feedbackLines.push('❌ CI/CD checks are failing:');
|
|
@@ -468,8 +641,18 @@ export const watchUntilMergeable = async params => {
|
|
|
468
641
|
// Update last check time after restart
|
|
469
642
|
lastCheckTime = new Date();
|
|
470
643
|
} else if (blockers.length > 0) {
|
|
471
|
-
// There are blockers but none that warrant
|
|
472
|
-
|
|
644
|
+
// There are blockers but none that warrant an AI restart
|
|
645
|
+
// Issue #1314: Distinguish between different waiting reasons
|
|
646
|
+
const pendingBlocker = blockers.find(b => b.type === 'ci_pending');
|
|
647
|
+
const cancelledOnly = blockers.every(b => b.type === 'ci_cancelled' || b.type === 'ci_pending');
|
|
648
|
+
|
|
649
|
+
if (cancelledOnly && cancelledBlocker) {
|
|
650
|
+
await log(formatAligned('🔄', 'Waiting for re-triggered CI:', cancelledBlocker.details.join(', '), 2));
|
|
651
|
+
} else if (pendingBlocker) {
|
|
652
|
+
await log(formatAligned('⏳', 'Waiting for CI:', pendingBlocker.details.length > 0 ? pendingBlocker.details.join(', ') : pendingBlocker.message, 2));
|
|
653
|
+
} else {
|
|
654
|
+
await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
|
|
655
|
+
}
|
|
473
656
|
} else {
|
|
474
657
|
await log(formatAligned('', 'No action needed', 'Continuing to monitor...', 2));
|
|
475
658
|
}
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -400,7 +400,7 @@ export const createYargsConfig = yargsInstance => {
|
|
|
400
400
|
config = config
|
|
401
401
|
.option('model', {
|
|
402
402
|
type: 'string',
|
|
403
|
-
description: 'Model to use (for claude: opus, sonnet, haiku, haiku-3-5, haiku-3; for opencode: grok, gpt4o; for codex: gpt5, gpt5-codex, o3; for agent:
|
|
403
|
+
description: 'Model to use (for claude: opus, sonnet, haiku, haiku-3-5, haiku-3; for opencode: grok, gpt4o; for codex: gpt5, gpt5-codex, o3; for agent: kimi-k2.5-free, big-pickle, gpt-5-nano, minimax-m2.5-free, glm-5-free, deepseek-r1-free)',
|
|
404
404
|
alias: 'm',
|
|
405
405
|
default: currentParsedArgs => {
|
|
406
406
|
// Dynamic default based on tool selection
|