@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.
@@ -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 linking utilities
23
- import { extractLinkedIssueNumber } from './github-linking.lib.mjs';
24
-
25
- // Default label configuration
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: Import and re-export post-merge CI functions from separate module
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 and branch health checking
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
  };
@@ -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).
@@ -215,6 +215,9 @@ const KNOWN_OPTION_NAMES = [
215
215
  'prompt-examples-folder',
216
216
  'session-type',
217
217
  'working-directory',
218
+ 'prompt-ensure-all-requirements-are-met',
219
+ 'finalize',
220
+ 'finalize-model',
218
221
  ];
219
222
 
220
223
  /**
@@ -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
- if (cancelledOnly && cancelledBlocker) {
816
- await log(formatAligned('🔄', 'Waiting for re-triggered CI:', cancelledBlocker.details.join(', '), 2));
817
- } else if (pendingBlocker) {
818
- await log(formatAligned('⏳', 'Waiting for CI:', pendingBlocker.details.length > 0 ? pendingBlocker.details.join(', ') : pendingBlocker.message, 2));
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
- await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
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));
@@ -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
+ };