@link-assistant/hive-mind 1.30.5 → 1.31.1
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 +73 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +3 -0
- package/src/agent.prompts.lib.mjs +6 -1
- package/src/claude.lib.mjs +4 -1
- package/src/claude.prompts.lib.mjs +6 -1
- package/src/codex.lib.mjs +2 -0
- package/src/codex.prompts.lib.mjs +6 -1
- package/src/exit-handler.lib.mjs +16 -1
- package/src/github-merge-ready-sync.lib.mjs +251 -0
- package/src/github-merge.lib.mjs +15 -185
- package/src/opencode.lib.mjs +2 -0
- package/src/opencode.prompts.lib.mjs +6 -1
- package/src/option-suggestions.lib.mjs +3 -0
- package/src/solve.auto-ensure.lib.mjs +120 -0
- package/src/solve.auto-merge.lib.mjs +61 -5
- package/src/solve.config.lib.mjs +26 -0
- package/src/solve.error-handlers.lib.mjs +39 -0
- package/src/solve.interrupt.lib.mjs +70 -0
- package/src/solve.mjs +39 -61
- package/src/telegram-merge-command.lib.mjs +23 -1
- package/src/telegram-merge-queue.lib.mjs +16 -0
package/src/github-merge.lib.mjs
CHANGED
|
@@ -19,15 +19,10 @@ const exec = promisify(execCallback);
|
|
|
19
19
|
// Import GitHub URL parser
|
|
20
20
|
import { parseGitHubUrl } from './github.lib.mjs';
|
|
21
21
|
|
|
22
|
-
// Import
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export const READY_LABEL = {
|
|
27
|
-
name: 'ready',
|
|
28
|
-
description: 'Is ready to be merged',
|
|
29
|
-
color: '0E8A16', // Green color
|
|
30
|
-
};
|
|
22
|
+
// Issue #1413: Import ready tag sync, timeline, and label constant from separate module
|
|
23
|
+
// to keep this file under the 1500 line limit
|
|
24
|
+
import { syncReadyTags, getLinkedPRsFromTimeline, READY_LABEL } from './github-merge-ready-sync.lib.mjs';
|
|
25
|
+
export { syncReadyTags, getLinkedPRsFromTimeline, READY_LABEL };
|
|
31
26
|
|
|
32
27
|
/**
|
|
33
28
|
* Check if 'ready' label exists in repository
|
|
@@ -254,172 +249,6 @@ export async function fetchReadyIssuesWithPRs(owner, repo, verbose = false) {
|
|
|
254
249
|
}
|
|
255
250
|
}
|
|
256
251
|
|
|
257
|
-
/**
|
|
258
|
-
* Add a label to a GitHub issue or pull request
|
|
259
|
-
* @param {'issue'|'pr'} type - Whether to add to issue or PR
|
|
260
|
-
* @param {string} owner - Repository owner
|
|
261
|
-
* @param {string} repo - Repository name
|
|
262
|
-
* @param {number} number - Issue or PR number
|
|
263
|
-
* @param {string} labelName - Label name to add
|
|
264
|
-
* @param {boolean} verbose - Whether to log verbose output
|
|
265
|
-
* @returns {Promise<{success: boolean, error: string|null}>}
|
|
266
|
-
*/
|
|
267
|
-
async function addLabel(type, owner, repo, number, labelName, verbose = false) {
|
|
268
|
-
const cmd = type === 'issue' ? 'issue' : 'pr';
|
|
269
|
-
try {
|
|
270
|
-
await exec(`gh ${cmd} edit ${number} --repo ${owner}/${repo} --add-label "${labelName}"`);
|
|
271
|
-
if (verbose) console.log(`[VERBOSE] /merge: Added '${labelName}' label to ${type} #${number}`);
|
|
272
|
-
return { success: true, error: null };
|
|
273
|
-
} catch (error) {
|
|
274
|
-
if (verbose) console.log(`[VERBOSE] /merge: Failed to add label to ${type} #${number}: ${error.message}`);
|
|
275
|
-
return { success: false, error: error.message };
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Sync 'ready' tags between linked pull requests and issues
|
|
281
|
-
*
|
|
282
|
-
* Issue #1367: Before building the merge queue, ensure that:
|
|
283
|
-
* 1. If a PR has 'ready' label and is clearly linked to an issue (via standard GitHub
|
|
284
|
-
* keywords in the PR body/title), the issue also gets 'ready' label.
|
|
285
|
-
* 2. If an issue has 'ready' label and has a clearly linked open PR, the PR also gets
|
|
286
|
-
* 'ready' label.
|
|
287
|
-
*
|
|
288
|
-
* This ensures the final list of ready PRs reflects all ready work, regardless of
|
|
289
|
-
* where the 'ready' label was originally applied.
|
|
290
|
-
*
|
|
291
|
-
* @param {string} owner - Repository owner
|
|
292
|
-
* @param {string} repo - Repository name
|
|
293
|
-
* @param {boolean} verbose - Whether to log verbose output
|
|
294
|
-
* @returns {Promise<{synced: number, errors: number, details: Array<Object>}>}
|
|
295
|
-
*/
|
|
296
|
-
export async function syncReadyTags(owner, repo, verbose = false) {
|
|
297
|
-
const synced = [];
|
|
298
|
-
const errors = [];
|
|
299
|
-
|
|
300
|
-
if (verbose) {
|
|
301
|
-
console.log(`[VERBOSE] /merge: Syncing 'ready' tags for ${owner}/${repo}...`);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
// Fetch open PRs with 'ready' label (including body for link detection)
|
|
306
|
-
const { stdout: prsJson } = await exec(`gh pr list --repo ${owner}/${repo} --label "${READY_LABEL.name}" --state open --json number,title,body,labels --limit 100`);
|
|
307
|
-
const readyPRs = JSON.parse(prsJson.trim() || '[]');
|
|
308
|
-
|
|
309
|
-
if (verbose) {
|
|
310
|
-
console.log(`[VERBOSE] /merge: Found ${readyPRs.length} open PRs with 'ready' label for tag sync`);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Fetch open issues with 'ready' label
|
|
314
|
-
const { stdout: issuesJson } = await exec(`gh issue list --repo ${owner}/${repo} --label "${READY_LABEL.name}" --state open --json number,title --limit 100`);
|
|
315
|
-
const readyIssues = JSON.parse(issuesJson.trim() || '[]');
|
|
316
|
-
|
|
317
|
-
if (verbose) {
|
|
318
|
-
console.log(`[VERBOSE] /merge: Found ${readyIssues.length} open issues with 'ready' label for tag sync`);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Build a set of issue numbers that already have 'ready'
|
|
322
|
-
const readyIssueNumbers = new Set(readyIssues.map(i => String(i.number)));
|
|
323
|
-
|
|
324
|
-
// Step 1: For each PR with 'ready', find linked issue and sync label to it
|
|
325
|
-
for (const pr of readyPRs) {
|
|
326
|
-
try {
|
|
327
|
-
const prBody = pr.body || '';
|
|
328
|
-
const linkedIssueNumber = extractLinkedIssueNumber(prBody);
|
|
329
|
-
|
|
330
|
-
if (!linkedIssueNumber) {
|
|
331
|
-
if (verbose) {
|
|
332
|
-
console.log(`[VERBOSE] /merge: PR #${pr.number} has no linked issue (no closing keyword in body)`);
|
|
333
|
-
}
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (readyIssueNumbers.has(String(linkedIssueNumber))) {
|
|
338
|
-
if (verbose) {
|
|
339
|
-
console.log(`[VERBOSE] /merge: Issue #${linkedIssueNumber} already has 'ready' label (linked from PR #${pr.number})`);
|
|
340
|
-
}
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Issue doesn't have 'ready' label yet - add it
|
|
345
|
-
if (verbose) {
|
|
346
|
-
console.log(`[VERBOSE] /merge: PR #${pr.number} has 'ready', adding to linked issue #${linkedIssueNumber}`);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const result = await addLabel('issue', owner, repo, linkedIssueNumber, READY_LABEL.name, verbose);
|
|
350
|
-
if (result.success) {
|
|
351
|
-
synced.push({ type: 'pr-to-issue', prNumber: pr.number, issueNumber: Number(linkedIssueNumber) });
|
|
352
|
-
// Mark this issue as now having 'ready' so we don't process it again
|
|
353
|
-
readyIssueNumbers.add(String(linkedIssueNumber));
|
|
354
|
-
} else {
|
|
355
|
-
errors.push({ type: 'pr-to-issue', prNumber: pr.number, issueNumber: Number(linkedIssueNumber), error: result.error });
|
|
356
|
-
}
|
|
357
|
-
} catch (err) {
|
|
358
|
-
if (verbose) {
|
|
359
|
-
console.log(`[VERBOSE] /merge: Error syncing label from PR #${pr.number}: ${err.message}`);
|
|
360
|
-
}
|
|
361
|
-
errors.push({ type: 'pr-to-issue', prNumber: pr.number, error: err.message });
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Build a set of PR numbers that already have 'ready'
|
|
366
|
-
const readyPRNumbers = new Set(readyPRs.map(p => String(p.number)));
|
|
367
|
-
|
|
368
|
-
// Step 2: For each issue with 'ready', find linked PRs and sync label to them
|
|
369
|
-
for (const issue of readyIssues) {
|
|
370
|
-
try {
|
|
371
|
-
// Search for open PRs linked to this issue via closing keywords
|
|
372
|
-
const { stdout: linkedPRsJson } = await exec(`gh pr list --repo ${owner}/${repo} --search "in:body closes #${issue.number} OR fixes #${issue.number} OR resolves #${issue.number}" --state open --json number,title,labels --limit 10`);
|
|
373
|
-
const linkedPRs = JSON.parse(linkedPRsJson.trim() || '[]');
|
|
374
|
-
|
|
375
|
-
for (const linkedPR of linkedPRs) {
|
|
376
|
-
if (readyPRNumbers.has(String(linkedPR.number))) {
|
|
377
|
-
if (verbose) {
|
|
378
|
-
console.log(`[VERBOSE] /merge: PR #${linkedPR.number} already has 'ready' label (linked from issue #${issue.number})`);
|
|
379
|
-
}
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// PR doesn't have 'ready' label yet - add it
|
|
384
|
-
if (verbose) {
|
|
385
|
-
console.log(`[VERBOSE] /merge: Issue #${issue.number} has 'ready', adding to linked PR #${linkedPR.number}`);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const result = await addLabel('pr', owner, repo, linkedPR.number, READY_LABEL.name, verbose);
|
|
389
|
-
if (result.success) {
|
|
390
|
-
synced.push({ type: 'issue-to-pr', issueNumber: issue.number, prNumber: linkedPR.number });
|
|
391
|
-
// Mark this PR as now having 'ready'
|
|
392
|
-
readyPRNumbers.add(String(linkedPR.number));
|
|
393
|
-
} else {
|
|
394
|
-
errors.push({ type: 'issue-to-pr', issueNumber: issue.number, prNumber: linkedPR.number, error: result.error });
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
} catch (err) {
|
|
398
|
-
if (verbose) {
|
|
399
|
-
console.log(`[VERBOSE] /merge: Error syncing label from issue #${issue.number}: ${err.message}`);
|
|
400
|
-
}
|
|
401
|
-
errors.push({ type: 'issue-to-pr', issueNumber: issue.number, error: err.message });
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
} catch (error) {
|
|
405
|
-
if (verbose) {
|
|
406
|
-
console.log(`[VERBOSE] /merge: Error during tag sync: ${error.message}`);
|
|
407
|
-
}
|
|
408
|
-
errors.push({ type: 'fetch', error: error.message });
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (verbose) {
|
|
412
|
-
console.log(`[VERBOSE] /merge: Tag sync complete. Synced: ${synced.length}, Errors: ${errors.length}`);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return {
|
|
416
|
-
synced: synced.length,
|
|
417
|
-
errors: errors.length,
|
|
418
|
-
details: synced,
|
|
419
|
-
errorDetails: errors,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
252
|
/**
|
|
424
253
|
* Get combined list of ready PRs (from both direct PR labels and issue labels)
|
|
425
254
|
* @param {string} owner - Repository owner
|
|
@@ -751,11 +580,15 @@ export async function waitForCI(owner, repo, prNumber, options = {}, verbose = f
|
|
|
751
580
|
onStatusUpdate = null,
|
|
752
581
|
// Issue #1269: Add timeout for callback to prevent infinite blocking
|
|
753
582
|
callbackTimeout = 60 * 1000, // 1 minute max for callback
|
|
583
|
+
isCancelled = null, // Issue #1407: Support early exit when cancellation is requested
|
|
754
584
|
} = options;
|
|
755
585
|
|
|
756
586
|
const startTime = Date.now();
|
|
757
587
|
|
|
758
588
|
while (Date.now() - startTime < timeout) {
|
|
589
|
+
// Issue #1407: Check for cancellation before each poll to allow early exit
|
|
590
|
+
if (isCancelled?.()) return { success: false, status: 'cancelled', error: 'Operation was cancelled' };
|
|
591
|
+
|
|
759
592
|
let ciStatus;
|
|
760
593
|
try {
|
|
761
594
|
ciStatus = await checkPRCIStatus(owner, repo, prNumber, verbose);
|
|
@@ -1455,8 +1288,7 @@ export async function getActiveRepoWorkflows(owner, repo, verbose = false) {
|
|
|
1455
1288
|
}
|
|
1456
1289
|
}
|
|
1457
1290
|
|
|
1458
|
-
// Issue #1341:
|
|
1459
|
-
// to keep this file under the 1500 line limit
|
|
1291
|
+
// Issue #1341: Re-export post-merge CI functions from separate module
|
|
1460
1292
|
import { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha } from './github-merge-ci.lib.mjs';
|
|
1461
1293
|
export { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha };
|
|
1462
1294
|
|
|
@@ -1469,32 +1301,30 @@ export default {
|
|
|
1469
1301
|
fetchReadyPullRequests,
|
|
1470
1302
|
fetchReadyIssuesWithPRs,
|
|
1471
1303
|
getAllReadyPRs,
|
|
1472
|
-
// Issue #1367: Sync 'ready' tags between linked PRs and issues
|
|
1473
|
-
syncReadyTags,
|
|
1304
|
+
syncReadyTags, // Issue #1367: Sync 'ready' tags between linked PRs and issues
|
|
1474
1305
|
checkPRCIStatus,
|
|
1475
1306
|
checkPRMergeable,
|
|
1476
1307
|
checkMergePermissions,
|
|
1477
1308
|
mergePullRequest,
|
|
1478
1309
|
waitForCI,
|
|
1479
1310
|
parseRepositoryUrl,
|
|
1480
|
-
// Issue #1307: New exports for target branch CI waiting
|
|
1481
|
-
getActiveBranchRuns,
|
|
1311
|
+
getActiveBranchRuns, // Issue #1307: New exports for target branch CI waiting
|
|
1482
1312
|
waitForBranchCI,
|
|
1483
1313
|
getDefaultBranch,
|
|
1484
|
-
// Issue #1314: Billing limit detection
|
|
1314
|
+
// Issue #1314: Billing limit detection and enhanced CI status and re-run capabilities
|
|
1485
1315
|
getCheckRunAnnotations,
|
|
1486
1316
|
getRepoVisibility,
|
|
1487
1317
|
checkForBillingLimitError,
|
|
1488
1318
|
BILLING_LIMIT_ERROR_PATTERN,
|
|
1489
|
-
// Issue #1314: Enhanced CI status and re-run capabilities
|
|
1490
1319
|
getDetailedCIStatus,
|
|
1491
1320
|
rerunWorkflowRun,
|
|
1492
1321
|
rerunFailedJobs,
|
|
1493
1322
|
getWorkflowRunsForSha,
|
|
1494
|
-
// Issue #1341: Post-merge CI waiting
|
|
1323
|
+
// Issue #1341: Post-merge CI waiting; Issue #1363: Detect active workflows
|
|
1495
1324
|
waitForCommitCI,
|
|
1496
1325
|
checkBranchCIHealth,
|
|
1497
1326
|
getMergeCommitSha,
|
|
1498
|
-
// Issue #1363: Detect active workflows to distinguish "no CI" from race condition
|
|
1499
1327
|
getActiveRepoWorkflows,
|
|
1328
|
+
// Issue #1413: Use issue timeline to find genuinely linked PRs (avoids false positives from text search)
|
|
1329
|
+
getLinkedPRsFromTimeline,
|
|
1500
1330
|
};
|
package/src/opencode.lib.mjs
CHANGED
|
@@ -465,6 +465,8 @@ export const executeOpenCodeCommand = async params => {
|
|
|
465
465
|
for (const line of messageLines) {
|
|
466
466
|
await log(line, { level: 'warning' });
|
|
467
467
|
}
|
|
468
|
+
} else if (exitCode === 130) {
|
|
469
|
+
await log('\n\n⚠️ OpenCode command interrupted (CTRL+C)');
|
|
468
470
|
} else {
|
|
469
471
|
await log(`\n\n❌ OpenCode command failed with exit code ${exitCode}`, { level: 'error' });
|
|
470
472
|
}
|
|
@@ -213,7 +213,12 @@ Workflow and collaboration.
|
|
|
213
213
|
Self review.
|
|
214
214
|
- When you check your solution draft, run all tests locally.
|
|
215
215
|
- When you compare with repo style, use gh pr diff [number].
|
|
216
|
-
- When you finalize, confirm code, tests, and description are consistent
|
|
216
|
+
- When you finalize, confirm code, tests, and description are consistent.${
|
|
217
|
+
argv && argv.promptEnsureAllRequirementsAreMet
|
|
218
|
+
? `
|
|
219
|
+
- When no explicit feedback or requirements is provided, ensure all changes are correct, consistent, validated, tested, logged and fully meet all discussed requirements (check issue description and all comments in issue and in pull request). Ensure all CI/CD checks pass.`
|
|
220
|
+
: ''
|
|
221
|
+
}
|
|
217
222
|
|
|
218
223
|
GitHub CLI command patterns.
|
|
219
224
|
- IMPORTANT: Always use --paginate flag when fetching lists from GitHub API to ensure all results are returned (GitHub returns max 30 per page by default).
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Finalize module for solve.mjs
|
|
5
|
+
* After the main solve completes, restarts the AI tool N times with a
|
|
6
|
+
* requirements-check prompt to verify all requirements are met.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from solve.mjs to keep files under 1500 lines.
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1383
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Check if use is already defined globally (when imported from solve.mjs)
|
|
14
|
+
// If not, fetch it (when running standalone)
|
|
15
|
+
if (typeof globalThis.use === 'undefined') {
|
|
16
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
17
|
+
}
|
|
18
|
+
const use = globalThis.use;
|
|
19
|
+
|
|
20
|
+
// Use command-stream for consistent $ behavior across runtimes
|
|
21
|
+
const { $ } = await use('command-stream');
|
|
22
|
+
|
|
23
|
+
// Import shared library functions
|
|
24
|
+
const lib = await import('./lib.mjs');
|
|
25
|
+
const { log } = lib;
|
|
26
|
+
|
|
27
|
+
// Import shared restart utilities
|
|
28
|
+
const restartShared = await import('./solve.restart-shared.lib.mjs');
|
|
29
|
+
const { executeToolIteration } = restartShared;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Runs finalize requirements-check iterations after the main solve.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} params
|
|
35
|
+
* @param {string} params.issueUrl
|
|
36
|
+
* @param {string} params.owner
|
|
37
|
+
* @param {string} params.repo
|
|
38
|
+
* @param {string|number} params.issueNumber
|
|
39
|
+
* @param {string|number} params.prNumber
|
|
40
|
+
* @param {string} params.branchName
|
|
41
|
+
* @param {string} params.tempDir
|
|
42
|
+
* @param {object} params.argv - CLI arguments
|
|
43
|
+
* @param {function} params.cleanupClaudeFile - cleanup function
|
|
44
|
+
* @returns {Promise<{sessionId, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo}|null>}
|
|
45
|
+
*/
|
|
46
|
+
export const runAutoEnsureRequirements = async ({ issueUrl, owner, repo, issueNumber, prNumber, branchName, tempDir, argv, cleanupClaudeFile }) => {
|
|
47
|
+
const finalizeCount = argv.finalize;
|
|
48
|
+
if (!finalizeCount || finalizeCount <= 0 || !prNumber) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await log('');
|
|
53
|
+
await log(`🔍 FINALIZE: Starting ${finalizeCount} requirements-check restart(s)`);
|
|
54
|
+
await log(' Will restart the AI tool to verify all requirements are met');
|
|
55
|
+
await log('');
|
|
56
|
+
|
|
57
|
+
// Get PR merge state status for the iterations
|
|
58
|
+
let currentMergeStateStatus = null;
|
|
59
|
+
try {
|
|
60
|
+
const prStateResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.mergeStateStatus'`;
|
|
61
|
+
if (prStateResult.code === 0) {
|
|
62
|
+
currentMergeStateStatus = prStateResult.stdout.toString().trim();
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Ignore errors getting merge state
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let sessionId;
|
|
69
|
+
let anthropicTotalCostUSD;
|
|
70
|
+
let publicPricingEstimate;
|
|
71
|
+
let pricingInfo;
|
|
72
|
+
|
|
73
|
+
// Use --finalize-model if provided, otherwise fall back to --model
|
|
74
|
+
const finalizeModel = argv.finalizeModel || argv.model;
|
|
75
|
+
|
|
76
|
+
for (let ensureIteration = 1; ensureIteration <= finalizeCount; ensureIteration++) {
|
|
77
|
+
await log(`🔄 FINALIZE iteration ${ensureIteration}/${finalizeCount}: Restarting to verify requirements...`);
|
|
78
|
+
|
|
79
|
+
const ensureFeedbackLines = ['', '='.repeat(60), '🔍 FINALIZE REQUIREMENTS CHECK:', '='.repeat(60), '', 'We need to ensure all changes are correct, consistent, validated, tested, logged and fully meet all discussed requirements (check issue description and all comments in issue and in pull request). Ensure all CI/CD checks pass.', ''];
|
|
80
|
+
|
|
81
|
+
const ensureResult = await executeToolIteration({
|
|
82
|
+
issueUrl,
|
|
83
|
+
owner,
|
|
84
|
+
repo,
|
|
85
|
+
issueNumber,
|
|
86
|
+
prNumber,
|
|
87
|
+
branchName,
|
|
88
|
+
tempDir,
|
|
89
|
+
mergeStateStatus: currentMergeStateStatus,
|
|
90
|
+
feedbackLines: ensureFeedbackLines,
|
|
91
|
+
argv: {
|
|
92
|
+
...argv,
|
|
93
|
+
// Override model with finalize-model for this iteration
|
|
94
|
+
model: finalizeModel,
|
|
95
|
+
// Enable prompt-ensure only during finalize cycle (not the first regular run)
|
|
96
|
+
promptEnsureAllRequirementsAreMet: true,
|
|
97
|
+
// Prevent recursive finalize
|
|
98
|
+
finalize: 0,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Update session data from finalize restart
|
|
103
|
+
if (ensureResult) {
|
|
104
|
+
if (ensureResult.sessionId) sessionId = ensureResult.sessionId;
|
|
105
|
+
if (ensureResult.anthropicTotalCostUSD) anthropicTotalCostUSD = ensureResult.anthropicTotalCostUSD;
|
|
106
|
+
if (ensureResult.publicPricingEstimate) publicPricingEstimate = ensureResult.publicPricingEstimate;
|
|
107
|
+
if (ensureResult.pricingInfo) pricingInfo = ensureResult.pricingInfo;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await log(`✅ FINALIZE iteration ${ensureIteration}/${finalizeCount} complete`);
|
|
111
|
+
await log('');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Clean up CLAUDE.md/.gitkeep after ensure restarts
|
|
115
|
+
await cleanupClaudeFile(tempDir, branchName, null, argv);
|
|
116
|
+
|
|
117
|
+
return { sessionId, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo };
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export default { runAutoEnsureRequirements };
|
|
@@ -368,6 +368,12 @@ export const watchUntilMergeable = async params => {
|
|
|
368
368
|
let iteration = 0;
|
|
369
369
|
let lastCheckTime = new Date();
|
|
370
370
|
|
|
371
|
+
// Issue #1335: Cache whether the repo has CI workflows to avoid repeated API calls.
|
|
372
|
+
// When 'no_checks' is seen, we check if the repo actually has workflows configured.
|
|
373
|
+
// - If no workflows exist → 'no_checks' is permanent; treat PR as CI-passing and exit.
|
|
374
|
+
// - If workflows exist → 'no_checks' is a transient race condition; keep waiting.
|
|
375
|
+
let repoHasWorkflows = null; // null = not yet checked; true/false = cached result
|
|
376
|
+
|
|
371
377
|
while (true) {
|
|
372
378
|
iteration++;
|
|
373
379
|
const currentTime = new Date();
|
|
@@ -812,12 +818,62 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
812
818
|
const pendingBlocker = blockers.find(b => b.type === 'ci_pending');
|
|
813
819
|
const cancelledOnly = blockers.every(b => b.type === 'ci_cancelled' || b.type === 'ci_pending');
|
|
814
820
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
821
|
+
// Issue #1335: Detect permanent 'no_checks' state (repo has no CI workflows).
|
|
822
|
+
// The 'ci_pending' blocker with message 'have not started yet' means GitHub returned
|
|
823
|
+
// zero check-runs and zero commit statuses for this PR's HEAD SHA. This is ambiguous:
|
|
824
|
+
// (a) Transient race condition — CI workflows exist but haven't queued yet after push.
|
|
825
|
+
// (b) Permanent state — the repository has no CI/CD workflows configured at all.
|
|
826
|
+
// We resolve the ambiguity by checking if the repo actually has workflow files via the
|
|
827
|
+
// GitHub API. If it has none, the 'no_checks' state is permanent and the PR should be
|
|
828
|
+
// treated as CI-passing (no CI = nothing to wait for).
|
|
829
|
+
const isNoCIChecks = pendingBlocker && pendingBlocker.message.includes('have not started yet');
|
|
830
|
+
if (isNoCIChecks) {
|
|
831
|
+
// Lazy-check whether the repo has workflows (cache result to avoid repeated API calls)
|
|
832
|
+
if (repoHasWorkflows === null) {
|
|
833
|
+
const workflowCheck = await getActiveRepoWorkflows(owner, repo, argv.verbose);
|
|
834
|
+
repoHasWorkflows = workflowCheck.hasWorkflows;
|
|
835
|
+
if (argv.verbose) {
|
|
836
|
+
await log(formatAligned('', 'Repo workflow check:', repoHasWorkflows ? `${workflowCheck.count} workflow(s) found — CI check is a transient race condition` : 'No workflows configured — no CI expected', 2));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (!repoHasWorkflows) {
|
|
841
|
+
// Root cause confirmed: repo has no CI. The 'no_checks' state is permanent.
|
|
842
|
+
// Treat the PR as CI-passing and exit the monitoring loop immediately.
|
|
843
|
+
await log('');
|
|
844
|
+
await log(formatAligned('ℹ️', 'NO CI WORKFLOWS CONFIGURED', 'Repository has no GitHub Actions workflows'));
|
|
845
|
+
await log(formatAligned('', 'Conclusion:', 'No CI expected — treating PR as CI-passing', 2));
|
|
846
|
+
await log(formatAligned('', 'Action:', 'Exiting monitoring loop', 2));
|
|
847
|
+
await log('');
|
|
848
|
+
|
|
849
|
+
// Post a comment explaining the situation
|
|
850
|
+
try {
|
|
851
|
+
const commentBody = `## ℹ️ No CI Workflows Detected
|
|
852
|
+
|
|
853
|
+
No CI/CD checks are configured for this pull request. The repository has no GitHub Actions workflow files in \`.github/workflows/\`.
|
|
854
|
+
|
|
855
|
+
The auto-restart-until-mergeable monitor is stopping since there is no CI to wait for. The PR may be ready to merge if there are no other issues.
|
|
856
|
+
|
|
857
|
+
---
|
|
858
|
+
*Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
|
|
859
|
+
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
860
|
+
} catch {
|
|
861
|
+
// Don't fail if comment posting fails
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return { success: true, reason: 'no_ci_checks', latestSessionId, latestAnthropicCost };
|
|
865
|
+
} else {
|
|
866
|
+
// Repo has workflows but CI hasn't started yet — transient race condition, keep waiting
|
|
867
|
+
await log(formatAligned('⏳', 'Waiting for CI:', 'No checks yet (CI workflows exist, waiting for them to start)', 2));
|
|
868
|
+
}
|
|
819
869
|
} else {
|
|
820
|
-
|
|
870
|
+
if (cancelledOnly && cancelledBlocker) {
|
|
871
|
+
await log(formatAligned('🔄', 'Waiting for re-triggered CI:', cancelledBlocker.details.join(', '), 2));
|
|
872
|
+
} else if (pendingBlocker) {
|
|
873
|
+
await log(formatAligned('⏳', 'Waiting for CI:', pendingBlocker.details.length > 0 ? pendingBlocker.details.join(', ') : pendingBlocker.message, 2));
|
|
874
|
+
} else {
|
|
875
|
+
await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
|
|
876
|
+
}
|
|
821
877
|
}
|
|
822
878
|
} else {
|
|
823
879
|
await log(formatAligned('', 'No action needed', 'Continuing to monitor...', 2));
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -374,6 +374,21 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
374
374
|
description: 'Automatically accept the pending GitHub repository or organization invitation for the specific repository/organization being solved, before checking write access. Unlike /accept_invites which accepts all pending invitations, this only accepts the invite for the target repo/org.',
|
|
375
375
|
default: false,
|
|
376
376
|
},
|
|
377
|
+
'prompt-ensure-all-requirements-are-met': {
|
|
378
|
+
type: 'boolean',
|
|
379
|
+
description: '[EXPERIMENTAL] Add a prompt hint to the system prompt to ensure all changes are correct, consistent, validated, tested, logged and fully meet all discussed requirements. Enabled automatically by --finalize during finalize cycle iterations only.',
|
|
380
|
+
default: false,
|
|
381
|
+
},
|
|
382
|
+
finalize: {
|
|
383
|
+
type: 'number',
|
|
384
|
+
description: '[EXPERIMENTAL] After the main solve completes, automatically restart the AI tool N times (default: 1) with a requirements-check prompt to verify all requirements are met. Use --finalize-model to override the model for finalize iterations.',
|
|
385
|
+
default: 0,
|
|
386
|
+
},
|
|
387
|
+
'finalize-model': {
|
|
388
|
+
type: 'string',
|
|
389
|
+
description: '[EXPERIMENTAL] Model to use for --finalize iterations. Defaults to the same model as --model.',
|
|
390
|
+
default: undefined,
|
|
391
|
+
},
|
|
377
392
|
};
|
|
378
393
|
|
|
379
394
|
// Function to create yargs configuration - avoids duplication
|
|
@@ -535,6 +550,17 @@ export const parseArguments = async (yargs, hideBin) => {
|
|
|
535
550
|
}
|
|
536
551
|
}
|
|
537
552
|
|
|
553
|
+
// --finalize normalization
|
|
554
|
+
// Issue #1383: When finalize is enabled (as boolean or number), normalize to iteration count
|
|
555
|
+
// NOTE: promptEnsureAllRequirementsAreMet is NOT set here — it is only enabled during
|
|
556
|
+
// the finalize cycle iterations themselves (not the first regular worker model run)
|
|
557
|
+
if (argv && argv.finalize) {
|
|
558
|
+
// Normalize: if passed as boolean true (flag without value), treat as 1 iteration
|
|
559
|
+
if (argv.finalize === true) {
|
|
560
|
+
argv.finalize = 1;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
538
564
|
if (argv.tool === 'opencode' && !modelExplicitlyProvided) {
|
|
539
565
|
// User did not explicitly provide --model, so use the correct default for opencode
|
|
540
566
|
argv.model = 'grok-code-fast-1';
|
|
@@ -152,6 +152,45 @@ export const createUnhandledRejectionHandler = options => {
|
|
|
152
152
|
};
|
|
153
153
|
};
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Handles the case where no PR is available when one is required
|
|
157
|
+
*/
|
|
158
|
+
export const handleNoPrAvailableError = async ({ isContinueMode, tempDir, issueNumber, issueUrl, log, formatAligned }) => {
|
|
159
|
+
await log('');
|
|
160
|
+
await log(formatAligned('❌', 'FATAL ERROR:', 'No pull request available'), { level: 'error' });
|
|
161
|
+
await log('');
|
|
162
|
+
await log(' 🔍 What happened:');
|
|
163
|
+
if (isContinueMode) {
|
|
164
|
+
await log(' Continue mode is active but no PR number is available.');
|
|
165
|
+
await log(' This usually means PR creation failed or was skipped incorrectly.');
|
|
166
|
+
} else {
|
|
167
|
+
await log(' Auto-PR creation is enabled but no PR was created.');
|
|
168
|
+
await log(' PR creation may have failed without throwing an error.');
|
|
169
|
+
}
|
|
170
|
+
await log('');
|
|
171
|
+
await log(' 💡 Why this is critical:');
|
|
172
|
+
await log(' The solve command requires a PR for:');
|
|
173
|
+
await log(' • Tracking work progress');
|
|
174
|
+
await log(' • Receiving and processing feedback');
|
|
175
|
+
await log(' • Managing code changes');
|
|
176
|
+
await log(' • Auto-merging when complete');
|
|
177
|
+
await log('');
|
|
178
|
+
await log(' 🔧 How to fix:');
|
|
179
|
+
await log('');
|
|
180
|
+
await log(' Option 1: Create PR manually and use --continue');
|
|
181
|
+
await log(` cd ${tempDir}`);
|
|
182
|
+
await log(` gh pr create --draft --title "Fix issue #${issueNumber}" --body "Fixes #${issueNumber}"`);
|
|
183
|
+
await log(' # Then use the PR URL with solve.mjs');
|
|
184
|
+
await log('');
|
|
185
|
+
await log(' Option 2: Start fresh without continue mode');
|
|
186
|
+
await log(` ./solve.mjs "${issueUrl}" --auto-pull-request-creation`);
|
|
187
|
+
await log('');
|
|
188
|
+
await log(' Option 3: Disable auto-PR creation (Claude will create it)');
|
|
189
|
+
await log(` ./solve.mjs "${issueUrl}" --no-auto-pull-request-creation`);
|
|
190
|
+
await log('');
|
|
191
|
+
await safeExit(1, 'No PR available');
|
|
192
|
+
};
|
|
193
|
+
|
|
155
194
|
/**
|
|
156
195
|
* Handles execution errors in the main catch block
|
|
157
196
|
*/
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interrupt wrapper factory for CTRL+C handling in solve sessions.
|
|
3
|
+
*
|
|
4
|
+
* On SIGINT, auto-commits uncommitted changes and uploads session logs if --attach-logs is enabled.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates an interrupt wrapper function that auto-commits and uploads logs on CTRL+C.
|
|
9
|
+
* @param {object} deps - Dependencies
|
|
10
|
+
* @param {object} deps.cleanupContext - Mutable context object with tempDir, argv, branchName, prNumber, owner, repo
|
|
11
|
+
* @param {Function} deps.checkForUncommittedChanges - Tool-specific function to check and commit changes
|
|
12
|
+
* @param {boolean} deps.shouldAttachLogs - Whether --attach-logs is enabled
|
|
13
|
+
* @param {Function} deps.attachLogToGitHub - Function to upload log to GitHub PR
|
|
14
|
+
* @param {Function} deps.getLogFile - Function that returns the current log file path
|
|
15
|
+
* @param {Function} deps.sanitizeLogContent - Function to sanitize log content before upload
|
|
16
|
+
* @param {object} deps.$ - Shell command runner
|
|
17
|
+
* @param {Function} deps.log - Logging function
|
|
18
|
+
* @returns {Function} Async interrupt wrapper
|
|
19
|
+
*/
|
|
20
|
+
export const createInterruptWrapper = ({ cleanupContext, checkForUncommittedChanges, shouldAttachLogs, attachLogToGitHub, getLogFile, sanitizeLogContent, $, log }) => {
|
|
21
|
+
return async () => {
|
|
22
|
+
const ctx = cleanupContext;
|
|
23
|
+
if (!ctx.tempDir || !ctx.argv) return;
|
|
24
|
+
|
|
25
|
+
await log('\n⚠️ Session interrupted by user (CTRL+C)');
|
|
26
|
+
|
|
27
|
+
// Always auto-commit uncommitted changes on CTRL+C to preserve work
|
|
28
|
+
if (ctx.branchName) {
|
|
29
|
+
try {
|
|
30
|
+
await checkForUncommittedChanges(
|
|
31
|
+
ctx.tempDir,
|
|
32
|
+
ctx.owner,
|
|
33
|
+
ctx.repo,
|
|
34
|
+
ctx.branchName,
|
|
35
|
+
$,
|
|
36
|
+
log,
|
|
37
|
+
true, // always autoCommit on CTRL+C to preserve work
|
|
38
|
+
false // no autoRestart
|
|
39
|
+
);
|
|
40
|
+
} catch (commitError) {
|
|
41
|
+
await log(`⚠️ Could not auto-commit changes on interrupt: ${commitError.message}`, {
|
|
42
|
+
level: 'warning',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Upload logs if --attach-logs is enabled and we have a PR
|
|
48
|
+
if (shouldAttachLogs && ctx.prNumber && ctx.owner && ctx.repo) {
|
|
49
|
+
await log('📎 Uploading interrupted session logs to Pull Request...');
|
|
50
|
+
try {
|
|
51
|
+
await attachLogToGitHub({
|
|
52
|
+
logFile: getLogFile(),
|
|
53
|
+
targetType: 'pr',
|
|
54
|
+
targetNumber: ctx.prNumber,
|
|
55
|
+
owner: ctx.owner,
|
|
56
|
+
repo: ctx.repo,
|
|
57
|
+
$,
|
|
58
|
+
log,
|
|
59
|
+
sanitizeLogContent,
|
|
60
|
+
verbose: ctx.argv.verbose || false,
|
|
61
|
+
errorMessage: 'Session interrupted by user (CTRL+C)',
|
|
62
|
+
});
|
|
63
|
+
} catch (uploadError) {
|
|
64
|
+
await log(`⚠️ Could not upload logs on interrupt: ${uploadError.message}`, {
|
|
65
|
+
level: 'warning',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
};
|