@link-assistant/hive-mind 1.58.0 → 1.59.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 +217 -0
- package/package.json +1 -1
- package/src/anthropic-server-tool-pricing.lib.mjs +34 -0
- package/src/bidirectional-interactive.lib.mjs +392 -21
- package/src/claude.budget-stats.lib.mjs +151 -26
- package/src/claude.cost.lib.mjs +88 -0
- package/src/claude.lib.mjs +46 -55
- package/src/config.lib.mjs +5 -1
- package/src/github-merge-repo-actions.lib.mjs +54 -0
- package/src/github-merge.lib.mjs +24 -6
- package/src/lino.lib.mjs +3 -1
- package/src/queue-config.lib.mjs +7 -2
- package/src/solve.auto-merge-helpers.lib.mjs +89 -7
- package/src/solve.auto-merge.lib.mjs +27 -2
- package/src/solve.config.lib.mjs +29 -0
- package/src/use-with-retry.lib.mjs +107 -0
package/src/github-merge.lib.mjs
CHANGED
|
@@ -342,7 +342,9 @@ export async function checkPRCIStatus(owner, repo, prNumber, verbose = false) {
|
|
|
342
342
|
// ([].every(fn) returns true for any fn).
|
|
343
343
|
if (allChecks.length === 0) {
|
|
344
344
|
if (verbose) {
|
|
345
|
-
|
|
345
|
+
// Issue #1712: Reword for clarity — empty check-runs API response, not "no CI configured".
|
|
346
|
+
const commitUrl = `https://github.com/${owner}/${repo}/commit/${sha}`;
|
|
347
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} HEAD ${commitUrl} has no check-runs/statuses registered yet — treating as pending`);
|
|
346
348
|
}
|
|
347
349
|
return {
|
|
348
350
|
status: 'pending',
|
|
@@ -1070,6 +1072,8 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
|
|
|
1070
1072
|
const statuses = JSON.parse(statusJson.trim() || '[]');
|
|
1071
1073
|
|
|
1072
1074
|
// Build detailed checks list
|
|
1075
|
+
// Issue #1712: Include html_url so the user-facing waiting message can include
|
|
1076
|
+
// a clickable link for each pending/failing check.
|
|
1073
1077
|
const allChecks = [
|
|
1074
1078
|
...checkRuns.map(check => ({
|
|
1075
1079
|
name: check.name,
|
|
@@ -1077,6 +1081,7 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
|
|
|
1077
1081
|
conclusion: check.conclusion, // success, failure, cancelled, timed_out, skipped, neutral, action_required, stale, null
|
|
1078
1082
|
type: 'check_run',
|
|
1079
1083
|
id: check.id,
|
|
1084
|
+
html_url: check.html_url || check.details_url || null,
|
|
1080
1085
|
})),
|
|
1081
1086
|
...statuses.map(status => ({
|
|
1082
1087
|
name: status.context,
|
|
@@ -1084,13 +1089,20 @@ export async function getDetailedCIStatus(owner, repo, prNumber, verbose = false
|
|
|
1084
1089
|
conclusion: status.state === 'pending' ? null : status.state === 'success' ? 'success' : status.state === 'failure' ? 'failure' : status.state,
|
|
1085
1090
|
type: 'status',
|
|
1086
1091
|
id: null,
|
|
1092
|
+
html_url: status.target_url || null,
|
|
1087
1093
|
})),
|
|
1088
1094
|
];
|
|
1089
1095
|
|
|
1090
1096
|
// No checks yet
|
|
1091
1097
|
if (allChecks.length === 0) {
|
|
1092
1098
|
if (verbose) {
|
|
1093
|
-
|
|
1099
|
+
// Issue #1712: Reword to avoid "no CI checks" sounding like "no CI configured".
|
|
1100
|
+
// We are inspecting the GitHub `/commits/{sha}/check-runs` and `/commits/{sha}/status` endpoints;
|
|
1101
|
+
// an empty result means no check-runs/statuses are registered yet for this commit, NOT that the
|
|
1102
|
+
// repository has no CI/CD workflows. The caller (`getMergeBlockers`) decides between race
|
|
1103
|
+
// condition vs. "no CI configured" based on additional API calls.
|
|
1104
|
+
const commitUrl = `https://github.com/${owner}/${repo}/commit/${sha}`;
|
|
1105
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} HEAD ${commitUrl} has no check-runs or commit statuses registered yet (status=no_checks; race vs. no-CI distinction is decided downstream)`);
|
|
1094
1106
|
}
|
|
1095
1107
|
return {
|
|
1096
1108
|
status: 'no_checks',
|
|
@@ -1213,9 +1225,14 @@ export async function getWorkflowRunsForSha(owner, repo, sha, verbose = false) {
|
|
|
1213
1225
|
.map(run => ({ id: run.id, status: run.status, conclusion: run.conclusion, name: run.name, html_url: run.html_url, path: run.path }));
|
|
1214
1226
|
|
|
1215
1227
|
if (verbose) {
|
|
1216
|
-
|
|
1228
|
+
// Issue #1712: Include the commit URL (not just SHA) and the workflow run html_url
|
|
1229
|
+
// so the user can click through to GitHub instead of pasting IDs into a URL by hand.
|
|
1230
|
+
// The run html_url already encodes the run ID, so we don't repeat it as a separate field.
|
|
1231
|
+
const commitUrl = `https://github.com/${owner}/${repo}/commit/${sha}`;
|
|
1232
|
+
console.log(`[VERBOSE] /merge: Found ${runs.length} workflow run(s) for ${commitUrl}`);
|
|
1217
1233
|
for (const run of runs) {
|
|
1218
|
-
|
|
1234
|
+
const concPart = run.conclusion ? `/${run.conclusion}` : '';
|
|
1235
|
+
console.log(`[VERBOSE] /merge: - ${run.name} [${run.status}${concPart}]: ${run.html_url}`);
|
|
1219
1236
|
}
|
|
1220
1237
|
}
|
|
1221
1238
|
|
|
@@ -1333,8 +1350,8 @@ export { getCommitDate, checkPreviousPRCommitsHadCI, checkWorkflowsHavePRTrigger
|
|
|
1333
1350
|
import { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha } from './github-merge-ci.lib.mjs';
|
|
1334
1351
|
export { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha };
|
|
1335
1352
|
|
|
1336
|
-
import { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas } from './github-merge-repo-actions.lib.mjs'; // Issue #1503
|
|
1337
|
-
export { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas };
|
|
1353
|
+
import { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas, getActivePRWorkflowRuns } from './github-merge-repo-actions.lib.mjs'; // Issue #1503, #1712
|
|
1354
|
+
export { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas, getActivePRWorkflowRuns };
|
|
1338
1355
|
|
|
1339
1356
|
export default {
|
|
1340
1357
|
READY_LABEL,
|
|
@@ -1376,4 +1393,5 @@ export default {
|
|
|
1376
1393
|
getAllActiveRepoRuns,
|
|
1377
1394
|
waitForAllRepoActions,
|
|
1378
1395
|
checkCIConsensus, // Issue #1503
|
|
1396
|
+
getActivePRWorkflowRuns, // Issue #1712: list active runs across ALL PR commits
|
|
1379
1397
|
};
|
package/src/lino.lib.mjs
CHANGED
|
@@ -2,7 +2,9 @@ if (typeof use === 'undefined') {
|
|
|
2
2
|
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Issue #1710: hosted CI npm-install flake — retry once on a corrupt install.
|
|
6
|
+
const { useWithRetry } = await import('./use-with-retry.lib.mjs');
|
|
7
|
+
const linoModule = await useWithRetry(globalThis.use, 'links-notation');
|
|
6
8
|
const LinoParser = linoModule.Parser || linoModule.default?.Parser;
|
|
7
9
|
|
|
8
10
|
const fs = await import('fs');
|
package/src/queue-config.lib.mjs
CHANGED
|
@@ -36,10 +36,15 @@ if (typeof globalThis.use === 'undefined') {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// Issue #1710 / #1712: use-m occasionally hands back a truncated/corrupt global
|
|
40
|
+
// package on hosted CI (npm install -g flake — manifests as ERR_INVALID_PACKAGE_CONFIG,
|
|
41
|
+
// "Failed to resolve the path", or SyntaxError mid-import). useWithRetry deletes
|
|
42
|
+
// the broken install dir and re-fetches.
|
|
43
|
+
const { useWithRetry } = await import('./use-with-retry.lib.mjs');
|
|
44
|
+
const getenvModule = await useWithRetry(globalThis.use, 'getenv');
|
|
40
45
|
// Node 24 CJS/ESM interop may return the whole module object instead of the function directly
|
|
41
46
|
const getenv = typeof getenvModule === 'function' ? getenvModule : getenvModule.default || getenvModule;
|
|
42
|
-
const linoModule = await use
|
|
47
|
+
const linoModule = await useWithRetry(globalThis.use, 'links-notation');
|
|
43
48
|
const LinoParser = linoModule.Parser || linoModule.default?.Parser;
|
|
44
49
|
|
|
45
50
|
/**
|
|
@@ -33,7 +33,43 @@ const { reportError } = sentryLib;
|
|
|
33
33
|
|
|
34
34
|
// Import GitHub merge functions
|
|
35
35
|
const githubMergeLib = await import('./github-merge.lib.mjs');
|
|
36
|
-
const { checkPRMergeable, checkForBillingLimitError, getDetailedCIStatus, getWorkflowRunsForSha, getWorkflowRunJobsCount, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers, checkPreviousPRCommitsHadCI } = githubMergeLib;
|
|
36
|
+
const { checkPRMergeable, checkForBillingLimitError, getDetailedCIStatus, getWorkflowRunsForSha, getWorkflowRunJobsCount, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers, checkPreviousPRCommitsHadCI, getActivePRWorkflowRuns } = githubMergeLib;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Issue #1712: Plain-English meaning of GitHub Actions / check-run statuses, so the
|
|
40
|
+
* verbose log explains itself instead of forcing the user to look up GitHub docs.
|
|
41
|
+
* Returns the same status string suffixed with a parenthetical hint, e.g.
|
|
42
|
+
* "in_progress (currently executing)". Unknown statuses are returned unchanged.
|
|
43
|
+
*/
|
|
44
|
+
const STATUS_HINTS = {
|
|
45
|
+
in_progress: 'currently executing',
|
|
46
|
+
queued: 'waiting for a runner',
|
|
47
|
+
pending: 'waiting to start',
|
|
48
|
+
waiting: 'blocked on a deployment / approval gate',
|
|
49
|
+
requested: 'requested but not yet picked up',
|
|
50
|
+
completed: 'finished',
|
|
51
|
+
};
|
|
52
|
+
const CONCLUSION_HINTS = {
|
|
53
|
+
success: 'passed',
|
|
54
|
+
failure: 'failed',
|
|
55
|
+
cancelled: 'cancelled (will be re-triggered if applicable)',
|
|
56
|
+
timed_out: 'timed out',
|
|
57
|
+
skipped: 'skipped (e.g. paths-ignore matched)',
|
|
58
|
+
neutral: 'neutral / informational',
|
|
59
|
+
action_required: 'manual approval required',
|
|
60
|
+
stale: 'stale — superseded by a newer run',
|
|
61
|
+
startup_failure: 'workflow failed to start (likely invalid YAML)',
|
|
62
|
+
};
|
|
63
|
+
const explainStatus = (status, conclusion) => {
|
|
64
|
+
const statusPart = status ? `${status}${STATUS_HINTS[status] ? ` (${STATUS_HINTS[status]})` : ''}` : 'unknown';
|
|
65
|
+
if (!conclusion) return statusPart;
|
|
66
|
+
const concPart = `${conclusion}${CONCLUSION_HINTS[conclusion] ? ` (${CONCLUSION_HINTS[conclusion]})` : ''}`;
|
|
67
|
+
return `${statusPart} → ${concPart}`;
|
|
68
|
+
};
|
|
69
|
+
const formatRunLine = run => {
|
|
70
|
+
const status = run.conclusion ? `${run.status}/${run.conclusion}` : run.status;
|
|
71
|
+
return `${run.name} [${status}] — ${run.html_url}`;
|
|
72
|
+
};
|
|
37
73
|
|
|
38
74
|
// Issue #1625: Import centralized session-ending markers so the duplicate-
|
|
39
75
|
// search scope for checkForExistingComment() stays in lock-step with the
|
|
@@ -343,13 +379,38 @@ export const getMergeBlockers = async (owner, repo, prNumber, verbose = false, c
|
|
|
343
379
|
}
|
|
344
380
|
|
|
345
381
|
// Some workflow runs are still in progress or produced results — genuine race condition
|
|
382
|
+
// Issue #1712: User-facing blocker `details` carry the run URL + status so the
|
|
383
|
+
// top-level "⏳ Waiting for CI:" line is self-explanatory. Verbose listing is
|
|
384
|
+
// produced by `getWorkflowRunsForSha(..., verbose=true)` above — do NOT print the
|
|
385
|
+
// same run list twice. Here we only emit the one-line summary that explains
|
|
386
|
+
// *why* we're still waiting (race vs. real run).
|
|
387
|
+
const commitUrl = `https://github.com/${owner}/${repo}/pull/${prNumber}/commits/${ciStatus.sha}`;
|
|
346
388
|
if (verbose) {
|
|
347
|
-
await log(`[VERBOSE] /merge:
|
|
389
|
+
await log(`[VERBOSE] /merge: ${workflowRuns.length} workflow run(s) registered for PR #${prNumber} HEAD ${commitUrl} — waiting for them to publish check-runs (race condition between workflow_run and check_runs APIs, typically ~30–120 s)`);
|
|
348
390
|
}
|
|
391
|
+
|
|
392
|
+
// Also surface any active workflow runs on OLDER PR commits, so the user's view of
|
|
393
|
+
// the GitHub Actions tab (which shows yellow dots for every commit) reconciles
|
|
394
|
+
// with the log. These are NOT blockers — GitHub's concurrency group cancels them
|
|
395
|
+
// when a new commit is pushed — but listing them stops the user from worrying that
|
|
396
|
+
// the watcher is missing them.
|
|
397
|
+
const activeAcrossCommits = await getActivePRWorkflowRuns(owner, repo, prNumber, ciStatus.sha, verbose, getWorkflowRunsForSha);
|
|
398
|
+
if (verbose && activeAcrossCommits.otherActive > 0) {
|
|
399
|
+
await log(`[VERBOSE] /merge: ${activeAcrossCommits.otherActive} additional active workflow run(s) on older commits of PR #${prNumber} (these are not blockers — GitHub's concurrency group will cancel them):`);
|
|
400
|
+
for (const group of activeAcrossCommits.groups) {
|
|
401
|
+
if (group.isHead) continue;
|
|
402
|
+
const olderCommitUrl = `https://github.com/${owner}/${repo}/pull/${prNumber}/commits/${group.sha}`;
|
|
403
|
+
await log(`[VERBOSE] /merge: on ${olderCommitUrl}:`);
|
|
404
|
+
for (const run of group.runs) {
|
|
405
|
+
await log(`[VERBOSE] /merge: - ${formatRunLine(run)} — ${explainStatus(run.status, run.conclusion)}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
349
410
|
blockers.push({
|
|
350
411
|
type: 'ci_pending',
|
|
351
|
-
message: `
|
|
352
|
-
details: workflowRuns.map(
|
|
412
|
+
message: `Waiting for ${workflowRuns.length} workflow run(s) on HEAD ${commitUrl} to publish check-runs`,
|
|
413
|
+
details: workflowRuns.map(formatRunLine),
|
|
353
414
|
});
|
|
354
415
|
} else {
|
|
355
416
|
// No workflow runs for this SHA — but this could be a race condition!
|
|
@@ -519,11 +580,27 @@ export const getMergeBlockers = async (owner, repo, prNumber, verbose = false, c
|
|
|
519
580
|
}
|
|
520
581
|
} else if (ciStatus.status === 'pending') {
|
|
521
582
|
// CI is still running or queued - wait for completion
|
|
522
|
-
const
|
|
583
|
+
const pendingChecks = [...ciStatus.pendingChecks, ...ciStatus.queuedChecks];
|
|
584
|
+
const pendingDetails = pendingChecks.map(c => {
|
|
585
|
+
const statusPart = c.status ? ` [${c.status}]` : '';
|
|
586
|
+
const urlPart = c.html_url ? ` — ${c.html_url}` : '';
|
|
587
|
+
return `${c.name}${statusPart}${urlPart}`;
|
|
588
|
+
});
|
|
589
|
+
if (verbose) {
|
|
590
|
+
// Issue #1712: One concise line + a per-check entry that includes a plain-English
|
|
591
|
+
// explanation of the status. We do NOT also call getWorkflowRunsForSha here — the
|
|
592
|
+
// detailed CI status already covers the same data via check-runs.
|
|
593
|
+
const commitUrl = `https://github.com/${owner}/${repo}/pull/${prNumber}/commits/${ciStatus.sha}`;
|
|
594
|
+
await log(`[VERBOSE] /merge: ${pendingChecks.length} check-run(s) still running/queued on PR #${prNumber} HEAD ${commitUrl}:`);
|
|
595
|
+
for (const c of pendingChecks) {
|
|
596
|
+
const url = c.html_url ? ` — ${c.html_url}` : '';
|
|
597
|
+
await log(`[VERBOSE] /merge: - ${c.name}: ${explainStatus(c.status, c.conclusion)}${url}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
523
600
|
blockers.push({
|
|
524
601
|
type: 'ci_pending',
|
|
525
602
|
message: 'CI/CD checks are still running or queued',
|
|
526
|
-
details:
|
|
603
|
+
details: pendingDetails,
|
|
527
604
|
});
|
|
528
605
|
} else if (ciStatus.status === 'cancelled') {
|
|
529
606
|
// All non-passed checks are cancelled or stale (no genuine failures)
|
|
@@ -540,10 +617,15 @@ export const getMergeBlockers = async (owner, repo, prNumber, verbose = false, c
|
|
|
540
617
|
} else {
|
|
541
618
|
// These need to be re-triggered, NOT treated as AI-fixable failures
|
|
542
619
|
const cancelledOrStaleChecks = [...ciStatus.cancelledChecks, ...(ciStatus.staleChecks || [])];
|
|
620
|
+
const cancelledDetails = cancelledOrStaleChecks.map(c => {
|
|
621
|
+
const concPart = c.conclusion ? ` [${c.conclusion}]` : '';
|
|
622
|
+
const urlPart = c.html_url ? ` — ${c.html_url}` : '';
|
|
623
|
+
return `${c.name}${concPart}${urlPart}`;
|
|
624
|
+
});
|
|
543
625
|
blockers.push({
|
|
544
626
|
type: 'ci_cancelled',
|
|
545
627
|
message: 'CI/CD checks were cancelled or became stale',
|
|
546
|
-
details:
|
|
628
|
+
details: cancelledDetails,
|
|
547
629
|
sha: ciStatus.sha,
|
|
548
630
|
});
|
|
549
631
|
}
|
|
@@ -111,6 +111,11 @@ export const watchUntilMergeable = async params => {
|
|
|
111
111
|
await log(formatAligned('', 'Wait for all repo actions:', waitForAllRepoActionsFlag ? 'Yes (strict repo-wide safety)' : 'No (PR-scoped CI only)', 2));
|
|
112
112
|
await log(formatAligned('', 'Stop conditions:', 'PR merged, PR closed, or becomes mergeable', 2));
|
|
113
113
|
await log(formatAligned('', 'Restart triggers:', 'New non-bot comments, CI failures, merge conflicts', 2));
|
|
114
|
+
// Issue #1708: Surface that --auto-input-until-mergeable streamed feedback
|
|
115
|
+
// into the prior session, so any restart triggered here is a fallback.
|
|
116
|
+
if (argv.autoInputUntilMergeable) {
|
|
117
|
+
await log(formatAligned('', 'Streaming-first:', '--auto-input-until-mergeable was active; this loop is the fallback', 2));
|
|
118
|
+
}
|
|
114
119
|
await log('');
|
|
115
120
|
await log('Press Ctrl+C to stop watching manually');
|
|
116
121
|
await log('');
|
|
@@ -898,10 +903,30 @@ No further AI sessions will be started automatically for this run. Please review
|
|
|
898
903
|
const cancelledOnly = blockers.every(b => b.type === 'ci_cancelled' || b.type === 'ci_pending');
|
|
899
904
|
const cancelledBlocker = blockers.find(b => b.type === 'ci_cancelled');
|
|
900
905
|
|
|
906
|
+
// Issue #1712: When `details` contain URLs (which they now always do for ci_pending /
|
|
907
|
+
// ci_cancelled blockers), comma-joining them produces an unreadable single-line wall
|
|
908
|
+
// of text. Render the first detail inline (with the message as the header) and any
|
|
909
|
+
// additional details on their own indented lines. Each detail is already
|
|
910
|
+
// self-explanatory: "<name> [<status>] — <url>".
|
|
911
|
+
const renderBlocker = (icon, header, blocker) => {
|
|
912
|
+
if (!blocker.details || blocker.details.length === 0) {
|
|
913
|
+
return log(formatAligned(icon, header, blocker.message, 2));
|
|
914
|
+
}
|
|
915
|
+
if (blocker.details.length === 1) {
|
|
916
|
+
return log(formatAligned(icon, header, blocker.details[0], 2));
|
|
917
|
+
}
|
|
918
|
+
return (async () => {
|
|
919
|
+
await log(formatAligned(icon, header, blocker.message, 2));
|
|
920
|
+
for (const detail of blocker.details) {
|
|
921
|
+
await log(formatAligned('', '', detail, 4));
|
|
922
|
+
}
|
|
923
|
+
})();
|
|
924
|
+
};
|
|
925
|
+
|
|
901
926
|
if (cancelledOnly && cancelledBlocker) {
|
|
902
|
-
await
|
|
927
|
+
await renderBlocker('🔄', 'Waiting for re-triggered CI:', cancelledBlocker);
|
|
903
928
|
} else if (pendingBlocker) {
|
|
904
|
-
await
|
|
929
|
+
await renderBlocker('⏳', 'Waiting for CI:', pendingBlocker);
|
|
905
930
|
} else {
|
|
906
931
|
await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
|
|
907
932
|
}
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -198,6 +198,15 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
198
198
|
description: 'Auto-restart until PR becomes mergeable (no iteration limit). Restarts on new comments from non-bot users, CI failures, merge conflicts, or other issues. Does NOT auto-merge.',
|
|
199
199
|
default: true,
|
|
200
200
|
},
|
|
201
|
+
// Issue #1708: Stage 1 introduces this flag inert — it parses, appears in
|
|
202
|
+
// --help, and is read by validateAutoInputUntilMergeable below, but does not
|
|
203
|
+
// change the runtime loop yet. Stages 2-6 will wire it into watchUntilMergeable
|
|
204
|
+
// and the bidirectional NDJSON pipe (see docs/case-studies/issue-1708/).
|
|
205
|
+
'auto-input-until-mergeable': {
|
|
206
|
+
type: 'boolean',
|
|
207
|
+
description: '[EXPERIMENTAL] Extend a single AI tool session as long as possible by streaming new input (uncommitted changes, CI/CD failures, PR/issue comments, issue title/body updates) directly into the running session, instead of restarting it. Implies --accept-incomming-comments-as-input and --queue-comments-to-input by default (comments are deferred until the AI finishes the current step and is waiting for input). Existing auto-restart/auto-resume loops remain enabled as a fallback, but the goal is to keep them dormant. The full streaming-aware watchUntilMergeable replacement and per-tool wiring is staged in subsequent PRs (see docs/case-studies/issue-1708/). Falls back gracefully on non-Claude tools and on streaming errors. Disabled by default.',
|
|
208
|
+
default: false,
|
|
209
|
+
},
|
|
201
210
|
'wait-for-all-actions-in-repository-before-mergeable': {
|
|
202
211
|
type: 'boolean',
|
|
203
212
|
description: 'Wait for ALL active GitHub Actions workflow runs in the entire repository to complete before declaring PR mergeable. When enabled, blocks merge if ANY CI/CD run in the repository is active, regardless of branch — this is a strict safety mode for repositories with cross-branch CI/CD coupling. Disabled by default.',
|
|
@@ -377,6 +386,26 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
377
386
|
description: '[EXPERIMENTAL] Convenience flag that enables --interactive-mode, --accept-incomming-comments-as-input and --exclude-all-own-incomming-comments-from-input together. Only supported for --tool claude.',
|
|
378
387
|
default: false,
|
|
379
388
|
},
|
|
389
|
+
// Issue #1708: Comment delivery mode for --accept-incomming-comments-as-input.
|
|
390
|
+
// --stream-comments-to-input: forward comments immediately as they arrive
|
|
391
|
+
// (the default for --accept-incomming-comments-as-input on its own; matches
|
|
392
|
+
// the existing #817 behavior of pushing comments to Claude as soon as
|
|
393
|
+
// pollIncomingComments sees them).
|
|
394
|
+
// --queue-comments-to-input: hold comments until the AI signals it is idle
|
|
395
|
+
// (waiting for input), then flush the queue. Used by
|
|
396
|
+
// --auto-input-until-mergeable so the model finishes the current step
|
|
397
|
+
// before getting interrupted with new instructions.
|
|
398
|
+
// The two flags are mutually exclusive; if both are set, queue mode wins.
|
|
399
|
+
'stream-comments-to-input': {
|
|
400
|
+
type: 'boolean',
|
|
401
|
+
description: '[EXPERIMENTAL] When --accept-incomming-comments-as-input is enabled, forward each new PR/issue comment to the AI immediately as it arrives (real-time streaming). This is the default behavior for --accept-incomming-comments-as-input on its own. Mutually exclusive with --queue-comments-to-input; queue mode wins if both are set. Only supported for --tool claude.',
|
|
402
|
+
default: false,
|
|
403
|
+
},
|
|
404
|
+
'queue-comments-to-input': {
|
|
405
|
+
type: 'boolean',
|
|
406
|
+
description: '[EXPERIMENTAL] When --accept-incomming-comments-as-input is enabled, queue new PR/issue comments and only flush them once the AI signals it is idle (waiting for input). This is the default mode implied by --auto-input-until-mergeable so the AI completes the current step before being interrupted with new instructions. Mutually exclusive with --stream-comments-to-input; queue mode wins if both are set. Only supported for --tool claude.',
|
|
407
|
+
default: false,
|
|
408
|
+
},
|
|
380
409
|
'prompt-explore-sub-agent': {
|
|
381
410
|
type: 'boolean',
|
|
382
411
|
description: 'Encourage AI to use Explore-style sub-agent workflow for codebase exploration. Supported for --tool claude and --tool codex.',
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Retry wrapper for `use-m` package loading.
|
|
5
|
+
*
|
|
6
|
+
* Issue #1710: Hosted CI runners occasionally hand back a truncated or
|
|
7
|
+
* partially-installed global package after `npm install -g <pkg>`. Three
|
|
8
|
+
* surface symptoms have been observed:
|
|
9
|
+
*
|
|
10
|
+
* 1. `import` throws a SyntaxError ("Unexpected end of input") wrapped
|
|
11
|
+
* in use-m's `Failed to import module from '<path>'.` — the file on
|
|
12
|
+
* disk is cut off mid-line.
|
|
13
|
+
* 2. use-m throws `Failed to resolve the path to '<pkg>' from '<dir>'`
|
|
14
|
+
* — the install completed without error but the package tree is
|
|
15
|
+
* missing files that the `main`/`exports` entry depends on.
|
|
16
|
+
* 3. Node throws `Invalid package config <dir>/package.json.` with
|
|
17
|
+
* `code: 'ERR_INVALID_PACKAGE_CONFIG'` — the package.json itself
|
|
18
|
+
* is corrupt/truncated and cannot even be parsed (issue #1712).
|
|
19
|
+
*
|
|
20
|
+
* The recovery is identical for all three: delete the broken alias install
|
|
21
|
+
* directory and ask use-m to re-fetch. A clean reinstall almost always
|
|
22
|
+
* succeeds. This helper centralises that retry so every call site picks
|
|
23
|
+
* it up.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {(specifier: string) => Promise<unknown>} use - the use-m loader.
|
|
28
|
+
* @param {string} specifier - the npm specifier to load (e.g. `'getenv'`).
|
|
29
|
+
* @param {object} [options]
|
|
30
|
+
* @param {number} [options.attempts=3] - total attempts including the first try.
|
|
31
|
+
* @param {(path: string) => Promise<void>} [options.cleanup] - injectable cleanup
|
|
32
|
+
* for the corrupted install directory (defaults to recursive `rm`).
|
|
33
|
+
* @returns {Promise<unknown>} the module returned by use-m.
|
|
34
|
+
*/
|
|
35
|
+
export const useWithRetry = async (use, specifier, options = {}) => {
|
|
36
|
+
const attempts = options.attempts ?? 3;
|
|
37
|
+
const cleanup = options.cleanup ?? defaultCleanup;
|
|
38
|
+
let lastError;
|
|
39
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
40
|
+
try {
|
|
41
|
+
return await use(specifier);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
lastError = error;
|
|
44
|
+
if (attempt === attempts || !isCorruptInstallError(error)) {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
const corruptedPath = extractCorruptedFilePath(error);
|
|
48
|
+
if (corruptedPath) {
|
|
49
|
+
try {
|
|
50
|
+
// Two failure modes:
|
|
51
|
+
// * "Failed to import module from '<file>'" — corruptedPath is a file
|
|
52
|
+
// inside the use-m alias dir (e.g. /.../getenv-v-latest/index.js).
|
|
53
|
+
// * "Failed to resolve the path to 'pkg' from '<dir>'" — corruptedPath
|
|
54
|
+
// is the alias dir itself (e.g. /.../links-notation-v-latest).
|
|
55
|
+
// For files, walk up to the alias dir; otherwise remove the dir as-is.
|
|
56
|
+
const { dirname } = await import('node:path');
|
|
57
|
+
const target = corruptedPath.endsWith('-v-latest') || /-v-\d/.test(corruptedPath) ? corruptedPath : dirname(corruptedPath);
|
|
58
|
+
await cleanup(target);
|
|
59
|
+
} catch {
|
|
60
|
+
// Best-effort cleanup; fall through to retry regardless.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Unreachable — the loop either returns or throws.
|
|
66
|
+
throw lastError;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const isCorruptInstallError = error => {
|
|
70
|
+
const cause = error?.cause;
|
|
71
|
+
if (cause instanceof SyntaxError) return true;
|
|
72
|
+
const causeMessage = typeof cause?.message === 'string' ? cause.message : '';
|
|
73
|
+
if (/Unexpected end of input|Unexpected token/.test(causeMessage)) return true;
|
|
74
|
+
// Mode 3 (issue #1712): package.json itself is corrupt — Node refuses to
|
|
75
|
+
// even parse it and throws ERR_INVALID_PACKAGE_CONFIG before use-m's own
|
|
76
|
+
// resolve/import logic gets a chance to run.
|
|
77
|
+
if (error?.code === 'ERR_INVALID_PACKAGE_CONFIG') return true;
|
|
78
|
+
if (cause?.code === 'ERR_INVALID_PACKAGE_CONFIG') return true;
|
|
79
|
+
// Mode 2 (also seen on hosted CI): npm install completes but the package
|
|
80
|
+
// tree is incomplete, so use-m can't resolve the entry point.
|
|
81
|
+
const message = typeof error?.message === 'string' ? error.message : '';
|
|
82
|
+
if (/^Failed to resolve the path to /.test(message)) return true;
|
|
83
|
+
// Fallback string match for ERR_INVALID_PACKAGE_CONFIG (in case the error
|
|
84
|
+
// bubbles through use-m without preserving the `code` property).
|
|
85
|
+
return /^Invalid package config /.test(message);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const extractCorruptedFilePath = error => {
|
|
89
|
+
const message = typeof error?.message === 'string' ? error.message : '';
|
|
90
|
+
const importMatch = message.match(/Failed to import module from '([^']+)'/);
|
|
91
|
+
if (importMatch) return importMatch[1];
|
|
92
|
+
// For "Failed to resolve the path to 'pkg' from '<dir>'" the second path
|
|
93
|
+
// is already the alias install directory — return it directly so callers
|
|
94
|
+
// can clean it up (cleanup() handles both files and directories).
|
|
95
|
+
const resolveMatch = message.match(/Failed to resolve the path to '[^']+' from '([^']+)'/);
|
|
96
|
+
if (resolveMatch) return resolveMatch[1];
|
|
97
|
+
// Mode 3 (issue #1712): "Invalid package config <dir>/package.json." —
|
|
98
|
+
// extract the package.json path so the caller's cleanup() walks up to
|
|
99
|
+
// the alias dir.
|
|
100
|
+
const invalidConfigMatch = message.match(/Invalid package config (\S+?package\.json)/);
|
|
101
|
+
return invalidConfigMatch ? invalidConfigMatch[1] : null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const defaultCleanup = async path => {
|
|
105
|
+
const { rm } = await import('node:fs/promises');
|
|
106
|
+
await rm(path, { recursive: true, force: true });
|
|
107
|
+
};
|