@link-assistant/hive-mind 2.11.13 ā 2.12.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 +18 -0
- package/package.json +4 -1
- package/src/agent-command.lib.mjs +74 -0
- package/src/agent.lib.mjs +59 -34
- package/src/agentic-cli-updater.lib.mjs +241 -0
- package/src/claude.connection.lib.mjs +209 -0
- package/src/claude.lib.mjs +6 -202
- package/src/codex.lib.mjs +0 -128
- package/src/formal-ai-isolation.lib.mjs +62 -0
- package/src/formal-ai-maintenance.lib.mjs +106 -0
- package/src/formal-ai-model.lib.mjs +25 -0
- package/src/formal-ai-runtime.lib.mjs +10 -0
- package/src/formal-ai-sidecar.lib.mjs +565 -0
- package/src/formal-ai-updater.lib.mjs +294 -0
- package/src/formal-ai-version.lib.mjs +100 -0
- package/src/formal-ai.lib.mjs +11 -16
- package/src/github-rate-limit.lib.mjs +3 -0
- package/src/github-url-parser.lib.mjs +255 -0
- package/src/github.lib.mjs +22 -343
- package/src/hive.mjs +0 -152
- package/src/interactive-mode.lib.mjs +0 -43
- package/src/isolation-runner.lib.mjs +44 -173
- package/src/limits.lib.mjs +0 -89
- package/src/model-args.lib.mjs +32 -0
- package/src/models/index.mjs +5 -19
- package/src/session-monitor.lib.mjs +14 -172
- package/src/solve.auto-merge.lib.mjs +70 -164
- package/src/solve.mjs +31 -193
- package/src/solve.repository.lib.mjs +0 -83
- package/src/solve.results.lib.mjs +2 -92
- package/src/solve.session.lib.mjs +52 -19
- package/src/solve.tool-uncommitted.lib.mjs +22 -0
- package/src/state-lock.lib.mjs +82 -0
- package/src/telegram-bot.mjs +17 -65
- package/src/telegram-fix-command.lib.mjs +1 -8
- package/src/telegram-merge-queue.lib.mjs +3 -155
- package/src/telegram-solve-queue.lib.mjs +9 -168
- package/src/telegram-task-command.lib.mjs +1 -8
- package/src/use-m-bootstrap.lib.mjs +6 -5
- package/src/use-with-retry.lib.mjs +128 -2
- package/src/working-session-summary.lib.mjs +47 -1
package/src/github.lib.mjs
CHANGED
|
@@ -20,14 +20,14 @@ export { buildCostInfoString };
|
|
|
20
20
|
// #1756: route gh exec calls through transient + rate-limit retry wrapper
|
|
21
21
|
import { execGhWithRetry } from './github-rate-limit.lib.mjs';
|
|
22
22
|
import { QUIET_PROBE } from './quiet-probe.lib.mjs'; // issues #2130, #2135: keep read-only probe payloads out of the attached log
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
import { isGitHubUrlType, normalizeGitHubUrl, parseGitHubUrl } from './github-url-parser.lib.mjs';
|
|
24
|
+
export { isGitHubUrlType, normalizeGitHubUrl, parseGitHubUrl };
|
|
25
|
+
// Issue #1625: Named marker constants (single source of truth) + in-memory tracking for tool-posted comments. See tool-comments.lib.mjs for design.
|
|
25
26
|
import { SOLUTION_DRAFT_LOG_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, postTrackedComment, postTrackedCommentFromFile } from './tool-comments.lib.mjs';
|
|
26
27
|
export const maskGitHubToken = maskToken; // Alias for backward compatibility
|
|
27
28
|
export const escapeCodeBlocksInLog = logContent => logContent.replace(/```/g, '\\`\\`\\`'); // Escape ``` in logs
|
|
28
29
|
const buildIssueFailureActionSection = targetType => {
|
|
29
30
|
if (targetType !== 'issue') return '';
|
|
30
|
-
|
|
31
31
|
return `
|
|
32
32
|
|
|
33
33
|
### What you can do
|
|
@@ -42,12 +42,8 @@ const normalizeFailureActionSection = section => {
|
|
|
42
42
|
};
|
|
43
43
|
export const checkFileInBranch = async (owner, repo, fileName, branchName) => {
|
|
44
44
|
const { $ } = await use('command-stream');
|
|
45
|
-
|
|
46
45
|
try {
|
|
47
|
-
// Issue #2130: this is an existence probe, and "absent" is the answer the
|
|
48
|
-
// caller is usually looking for. Mirroring the command would print the whole
|
|
49
|
-
// contents payload on a hit and `gh: Not Found (HTTP 404)` on a miss, which
|
|
50
|
-
// reads as a failure in the log even though nothing went wrong.
|
|
46
|
+
// Issue #2130: this is an existence probe, and "absent" is the answer the caller is usually looking for. Mirroring the command would print the whole contents payload on a hit and `gh: Not Found (HTTP 404)` on a miss, which reads as a failure in the log even though nothing went wrong.
|
|
51
47
|
const result = await $(QUIET_PROBE)`gh api repos/${owner}/${repo}/contents/${fileName}?ref=${branchName}`;
|
|
52
48
|
return result.code === 0;
|
|
53
49
|
} catch (error) {
|
|
@@ -70,9 +66,7 @@ export const checkGitHubPermissions = async () => {
|
|
|
70
66
|
const { $ } = await use('command-stream');
|
|
71
67
|
try {
|
|
72
68
|
await log('\nš Checking GitHub authentication and permissions...');
|
|
73
|
-
// Get auth status including token scopes.
|
|
74
|
-
// Issue #2130: capture without mirroring - the parsed summary below is what
|
|
75
|
-
// belongs in the log, not gh's raw account/token/scope block.
|
|
69
|
+
// Get auth status including token scopes. Issue #2130: capture without mirroring - the parsed summary below is what belongs in the log, not gh's raw account/token/scope block.
|
|
76
70
|
const authStatusResult = await $(QUIET_PROBE)`gh auth status 2>&1`;
|
|
77
71
|
const authOutput = authStatusResult.stdout.toString() + authStatusResult.stderr.toString();
|
|
78
72
|
if (authStatusResult.code !== 0 || authOutput.includes('not logged into any GitHub hosts')) {
|
|
@@ -287,10 +281,7 @@ Could you please enable the **"Allow edits by maintainers"** checkbox? This will
|
|
|
287
281
|
3. Check the box ā
|
|
288
282
|
Alternatively, you can enable it when creating/editing the PR. See: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork
|
|
289
283
|
Thank you! š`;
|
|
290
|
-
// Issue #1625: track this comment so it's not counted as AI-authored by
|
|
291
|
-
// --auto-attach-solution-summary. The "Allow edits by maintainers"
|
|
292
|
-
// phrase embedded above matches MAINTAINER_ACCESS_REQUEST_MARKER as a
|
|
293
|
-
// fallback if the ID capture fails.
|
|
284
|
+
// Issue #1625: track this comment so it's not counted as AI-authored by --auto-attach-solution-summary. The "Allow edits by maintainers" phrase embedded above matches MAINTAINER_ACCESS_REQUEST_MARKER as a fallback if the ID capture fails.
|
|
294
285
|
const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
295
286
|
if (posted.ok) {
|
|
296
287
|
await log(`ā
Comment posted successfully${posted.commentId ? ` (id=${posted.commentId})` : ''}`, { verbose: true });
|
|
@@ -313,24 +304,19 @@ Thank you! š`;
|
|
|
313
304
|
};
|
|
314
305
|
export const selectLogUploadUrl = ({ uploadResult, isPublicRepo }) => {
|
|
315
306
|
if (!uploadResult?.success) return null;
|
|
316
|
-
|
|
317
307
|
const chunks = Number.isFinite(uploadResult.chunks) ? uploadResult.chunks : 1;
|
|
318
308
|
const rawUrl = uploadResult.rawUrl || null;
|
|
319
309
|
const pageUrl = uploadResult.url || null;
|
|
320
310
|
const canUseRawUrl = chunks === 1 && rawUrl && (isPublicRepo || uploadResult.type !== 'repository');
|
|
321
|
-
|
|
322
311
|
return canUseRawUrl ? rawUrl : pageUrl;
|
|
323
312
|
};
|
|
324
|
-
|
|
325
313
|
const isUsableLogUrl = value => typeof value === 'string' && /^https:\/\/[^\s)]+$/u.test(value);
|
|
326
|
-
|
|
327
314
|
const getLogUploadTerminalStatus = ({ errorMessage, errorDuringExecution, isUsageLimit }) => {
|
|
328
315
|
if (errorMessage) return { emoji: 'š', label: 'Failure log' };
|
|
329
316
|
if (errorDuringExecution) return { emoji: 'š', label: 'Finished-with-errors log' };
|
|
330
317
|
if (isUsageLimit) return { emoji: 'š', label: 'Usage-limit execution log' };
|
|
331
318
|
return { emoji: 'ā
', label: 'Solution draft log' };
|
|
332
319
|
};
|
|
333
|
-
|
|
334
320
|
/** Attaches a log file to a GitHub PR or issue as a comment. Returns true if upload succeeded. */
|
|
335
321
|
export async function attachLogToGitHub(options) {
|
|
336
322
|
const fs = (await use('fs')).promises;
|
|
@@ -393,8 +379,7 @@ export async function attachLogToGitHub(options) {
|
|
|
393
379
|
}
|
|
394
380
|
let totalCostUSD = publicPricingEstimate; // Issue #1225: token usage + actual model IDs
|
|
395
381
|
let actualModelIds = null;
|
|
396
|
-
// Issue #2037 (review): per-model output-token map, used to report the share of
|
|
397
|
-
// output tokens produced by the fallback model in the "Models used:" section.
|
|
382
|
+
// Issue #2037 (review): per-model output-token map, used to report the share of output tokens produced by the fallback model in the "Models used:" section.
|
|
398
383
|
let modelUsageForComment = null;
|
|
399
384
|
if (totalCostUSD === null && sessionId && tempDir && !errorMessage) {
|
|
400
385
|
try {
|
|
@@ -438,8 +423,7 @@ export async function attachLogToGitHub(options) {
|
|
|
438
423
|
let modelInfoString = '';
|
|
439
424
|
if (requestedModel || tool || actualModelIds) {
|
|
440
425
|
try {
|
|
441
|
-
// Issue #1949: prefer an explicit thinkingInfo, otherwise derive it from argv
|
|
442
|
-
// (e.g. "high (~24000 tokens)"). null when the run used the tool's default.
|
|
426
|
+
// Issue #1949: prefer an explicit thinkingInfo, otherwise derive it from argv (e.g. "high (~24000 tokens)"). null when the run used the tool's default.
|
|
443
427
|
const resolvedThinkingInfo = thinkingInfo ?? describeRequestedThinking(argv);
|
|
444
428
|
modelInfoString = await getModelInfoForComment({ requestedModel, tool, pricingInfo, actualModelIds, thinkingInfo: resolvedThinkingInfo, fallbackModel: argv?.fallbackModel ?? null, modelUsage: modelUsageForComment });
|
|
445
429
|
if (verbose && modelInfoString) {
|
|
@@ -458,7 +442,6 @@ export async function attachLogToGitHub(options) {
|
|
|
458
442
|
await log(' š Sanitizing log content to mask GitHub tokens...', { verbose: true });
|
|
459
443
|
}
|
|
460
444
|
let logContent = await sanitizeForPublication(rawLogContent);
|
|
461
|
-
|
|
462
445
|
// Escape code blocks in the log content to prevent them from breaking markdown formatting
|
|
463
446
|
if (verbose) {
|
|
464
447
|
await log(' š§ Escaping code blocks in log content for safe embedding...', { verbose: true });
|
|
@@ -467,8 +450,7 @@ export async function attachLogToGitHub(options) {
|
|
|
467
450
|
const failureAction = normalizeFailureActionSection(failureActionSection ?? buildIssueFailureActionSection(targetType));
|
|
468
451
|
// Create formatted comment
|
|
469
452
|
let logComment;
|
|
470
|
-
// Usage limit comments should be shown whenever isUsageLimit is true,
|
|
471
|
-
// regardless of whether a generic errorMessage is provided.
|
|
453
|
+
// Usage limit comments should be shown whenever isUsageLimit is true, regardless of whether a generic errorMessage is provided.
|
|
472
454
|
if (isUsageLimit) {
|
|
473
455
|
// Usage limit error format - separate from general failures
|
|
474
456
|
logComment = `## ā³ ${USAGE_LIMIT_REACHED_MARKER}
|
|
@@ -478,27 +460,19 @@ The automated solution draft was interrupted because the ${toolName} usage limit
|
|
|
478
460
|
### š Limit Information
|
|
479
461
|
- **Tool**: ${toolName}
|
|
480
462
|
- **Limit Type**: Usage limit exceeded`;
|
|
481
|
-
|
|
482
463
|
if (limitResetTime) {
|
|
483
|
-
// Format reset time with relative time and UTC for better user understanding
|
|
484
|
-
// Shows "in 14m (Feb 6, 3:00 PM UTC)" instead of just "4:00 PM"
|
|
485
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
464
|
+
// Format reset time with relative time and UTC for better user understanding Shows "in 14m (Feb 6, 3:00 PM UTC)" instead of just "4:00 PM" See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
486
465
|
const formattedResetTime = formatResetTimeWithRelative(limitResetTime, global.limitTimezone || null) || limitResetTime;
|
|
487
466
|
logComment += `\n- **Reset Time**: ${formattedResetTime}`;
|
|
488
467
|
}
|
|
489
|
-
|
|
490
468
|
if (sessionId) {
|
|
491
469
|
logComment += `\n- **Session ID**: ${sessionId}`;
|
|
492
470
|
}
|
|
493
|
-
|
|
494
471
|
logComment += '\n\n### š How to Continue\n';
|
|
495
|
-
|
|
496
|
-
// If auto-resume/auto-restart is enabled, show automatic continuation message instead of CLI commands
|
|
497
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
472
|
+
// If auto-resume/auto-restart is enabled, show automatic continuation message instead of CLI commands See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
498
473
|
if (isAutoResumeEnabled) {
|
|
499
474
|
const modeName = autoResumeMode === 'restart' ? 'restart' : 'resume';
|
|
500
475
|
const modeDescription = autoResumeMode === 'restart' ? 'The session will automatically restart (fresh start) when the limit resets.' : 'The session will automatically resume (with context preserved) when the limit resets.';
|
|
501
|
-
|
|
502
476
|
logComment += `**Auto-${modeName} is enabled.** ${modeDescription}`;
|
|
503
477
|
} else {
|
|
504
478
|
// Manual resume mode - show CLI commands
|
|
@@ -507,7 +481,6 @@ The automated solution draft was interrupted because the ${toolName} usage limit
|
|
|
507
481
|
} else {
|
|
508
482
|
logComment += 'Once the limit resets, ';
|
|
509
483
|
}
|
|
510
|
-
|
|
511
484
|
if (resumeCommand) {
|
|
512
485
|
logComment += `you can resume this session by running:
|
|
513
486
|
\`\`\`bash
|
|
@@ -519,9 +492,7 @@ ${resumeCommand}
|
|
|
519
492
|
logComment += 'you can retry the operation.';
|
|
520
493
|
}
|
|
521
494
|
}
|
|
522
|
-
|
|
523
495
|
const footerNote = isAutoResumeEnabled ? (autoResumeMode === 'restart' ? '*This session was interrupted due to usage limits. The session will automatically restart when the limit resets.*' : '*This session was interrupted due to usage limits. The session will automatically resume when the limit resets.*') : '*This session was interrupted due to usage limits. You can resume once the limit resets.*';
|
|
524
|
-
|
|
525
496
|
logComment += `${modelInfoString}
|
|
526
497
|
|
|
527
498
|
<details>
|
|
@@ -575,10 +546,7 @@ ${logContent}
|
|
|
575
546
|
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
576
547
|
} else {
|
|
577
548
|
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo, { includeTokenUsage: !budgetStats });
|
|
578
|
-
// Determine title based on session type (Issue #1152)
|
|
579
|
-
// Issue #1625: Every title variant embeds SOLUTION_DRAFT_LOG_MARKER so
|
|
580
|
-
// the filter in checkForAiCreatedComments matches every variant with a
|
|
581
|
-
// single substring check against the centralized marker constant.
|
|
549
|
+
// Determine title based on session type (Issue #1152) Issue #1625: Every title variant embeds SOLUTION_DRAFT_LOG_MARKER so the filter in checkForAiCreatedComments matches every variant with a single substring check against the centralized marker constant.
|
|
582
550
|
let title = customTitle;
|
|
583
551
|
let sessionNote = '';
|
|
584
552
|
if (sessionType === 'auto-resume') {
|
|
@@ -606,8 +574,7 @@ ${logContent}
|
|
|
606
574
|
---
|
|
607
575
|
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
608
576
|
}
|
|
609
|
-
// Check GitHub comment size limit or large file mode
|
|
610
|
-
// Issue #1173: Also use gh-upload-log for large files, not just long comments
|
|
577
|
+
// Check GitHub comment size limit or large file mode Issue #1173: Also use gh-upload-log for large files, not just long comments
|
|
611
578
|
if (useLargeFileMode || logComment.length > githubLimits.commentMaxSize) {
|
|
612
579
|
if (useLargeFileMode) {
|
|
613
580
|
await log(` š Log file too large for inline comment (${Math.round(logStats.size / 1024 / 1024)}MB), using gh-upload-log`);
|
|
@@ -616,8 +583,7 @@ ${logContent}
|
|
|
616
583
|
}
|
|
617
584
|
await log(' š Uploading log using gh-upload-log...');
|
|
618
585
|
try {
|
|
619
|
-
// Check if repository is public or private
|
|
620
|
-
// Issue #1173: Use public upload for public repos, private for private repos
|
|
586
|
+
// Check if repository is public or private Issue #1173: Use public upload for public repos, private for private repos
|
|
621
587
|
let isPublicRepo = true;
|
|
622
588
|
try {
|
|
623
589
|
const repoVisibilityResult = await $(QUIET_PROBE)`gh api repos/${owner}/${repo} --jq .visibility`;
|
|
@@ -643,7 +609,6 @@ ${logContent}
|
|
|
643
609
|
const tempLogFile = `/tmp/solution-draft-log-${targetType}-${Date.now()}.txt`;
|
|
644
610
|
// Use the original sanitized content for upload since it's a plain text file
|
|
645
611
|
await writeSanitizedPublicationFile(tempLogFile, rawLogContent);
|
|
646
|
-
|
|
647
612
|
// Use gh-upload-log default auto mode and shared repository fallback.
|
|
648
613
|
const uploadDescription = `Solution draft log for https://github.com/${owner}/${repo}/${targetType === 'pr' ? 'pull' : 'issues'}/${targetNumber}`;
|
|
649
614
|
let uploadResult;
|
|
@@ -657,12 +622,8 @@ ${logContent}
|
|
|
657
622
|
} finally {
|
|
658
623
|
await fs.unlink(tempLogFile).catch(() => {});
|
|
659
624
|
}
|
|
660
|
-
|
|
661
625
|
if (uploadResult.success) {
|
|
662
|
-
// Use rawUrl for direct file access (single chunk) or url for repository (multiple chunks)
|
|
663
|
-
// Requirements: 1 chunk = direct raw link, >1 chunks = repo link
|
|
664
|
-
// Private repository raw URLs can contain short-lived tokens, so keep
|
|
665
|
-
// private uploads on the stable repository/tree page URL.
|
|
626
|
+
// Use rawUrl for direct file access (single chunk) or url for repository (multiple chunks) Requirements: 1 chunk = direct raw link, >1 chunks = repo link Private repository raw URLs can contain short-lived tokens, so keep private uploads on the stable repository/tree page URL.
|
|
666
627
|
const logUrl = selectLogUploadUrl({ uploadResult, isPublicRepo });
|
|
667
628
|
if (!isUsableLogUrl(logUrl)) {
|
|
668
629
|
await log(' ā gh-upload-log completed but no usable log URL was resolved');
|
|
@@ -670,10 +631,8 @@ ${logContent}
|
|
|
670
631
|
await log(` š Full log remains available locally at: ${logFile}`);
|
|
671
632
|
return false;
|
|
672
633
|
}
|
|
673
|
-
|
|
674
634
|
const uploadTypeLabel = uploadResult.type === 'gist' ? 'Gist' : 'Repository';
|
|
675
635
|
const chunkInfo = uploadResult.chunks > 1 ? ` (${uploadResult.chunks} chunks)` : '';
|
|
676
|
-
|
|
677
636
|
// Create comment with log link
|
|
678
637
|
let logUploadComment;
|
|
679
638
|
// For usage limit cases, always use the dedicated format regardless of errorMessage
|
|
@@ -686,27 +645,19 @@ The automated solution draft was interrupted because the ${toolName} usage limit
|
|
|
686
645
|
### š Limit Information
|
|
687
646
|
- **Tool**: ${toolName}
|
|
688
647
|
- **Limit Type**: Usage limit exceeded`;
|
|
689
|
-
|
|
690
648
|
if (limitResetTime) {
|
|
691
|
-
// Format reset time with relative time and UTC for better user understanding
|
|
692
|
-
// Shows "in 14m (Feb 6, 3:00 PM UTC)" instead of just "4:00 PM"
|
|
693
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
649
|
+
// Format reset time with relative time and UTC for better user understanding Shows "in 14m (Feb 6, 3:00 PM UTC)" instead of just "4:00 PM" See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
694
650
|
const formattedUploadResetTime = formatResetTimeWithRelative(limitResetTime, global.limitTimezone || null) || limitResetTime;
|
|
695
651
|
logUploadComment += `\n- **Reset Time**: ${formattedUploadResetTime}`;
|
|
696
652
|
}
|
|
697
|
-
|
|
698
653
|
if (sessionId) {
|
|
699
654
|
logUploadComment += `\n- **Session ID**: ${sessionId}`;
|
|
700
655
|
}
|
|
701
|
-
|
|
702
656
|
logUploadComment += '\n\n### š How to Continue\n';
|
|
703
|
-
|
|
704
|
-
// If auto-resume/auto-restart is enabled, show automatic continuation message instead of CLI commands
|
|
705
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
657
|
+
// If auto-resume/auto-restart is enabled, show automatic continuation message instead of CLI commands See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
706
658
|
if (isAutoResumeEnabled) {
|
|
707
659
|
const modeName = autoResumeMode === 'restart' ? 'restart' : 'resume';
|
|
708
660
|
const modeDescription = autoResumeMode === 'restart' ? 'The session will automatically restart (fresh start) when the limit resets.' : 'The session will automatically resume (with context preserved) when the limit resets.';
|
|
709
|
-
|
|
710
661
|
logUploadComment += `**Auto-${modeName} is enabled.** ${modeDescription}`;
|
|
711
662
|
} else {
|
|
712
663
|
// Manual resume mode - show CLI commands
|
|
@@ -715,7 +666,6 @@ The automated solution draft was interrupted because the ${toolName} usage limit
|
|
|
715
666
|
} else {
|
|
716
667
|
logUploadComment += 'Once the limit resets, ';
|
|
717
668
|
}
|
|
718
|
-
|
|
719
669
|
if (resumeCommand) {
|
|
720
670
|
logUploadComment += `you can resume this session by running:
|
|
721
671
|
\`\`\`bash
|
|
@@ -727,9 +677,7 @@ ${resumeCommand}
|
|
|
727
677
|
logUploadComment += 'you can retry the operation.';
|
|
728
678
|
}
|
|
729
679
|
}
|
|
730
|
-
|
|
731
680
|
const uploadFooterNote = isAutoResumeEnabled ? (autoResumeMode === 'restart' ? '*This session was interrupted due to usage limits. The session will automatically restart when the limit resets.*' : '*This session was interrupted due to usage limits. The session will automatically resume when the limit resets.*') : '*This session was interrupted due to usage limits. You can resume once the limit resets.*';
|
|
732
|
-
|
|
733
681
|
logUploadComment += `${modelInfoString}
|
|
734
682
|
|
|
735
683
|
### š **Execution log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
|
|
@@ -766,9 +714,7 @@ This log file contains the complete execution trace of the AI ${targetType === '
|
|
|
766
714
|
} else {
|
|
767
715
|
// Success log format - use helper function for cost info
|
|
768
716
|
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo, { includeTokenUsage: !budgetStats });
|
|
769
|
-
// Determine title based on session type
|
|
770
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
771
|
-
// Issue #1625: titles embed SOLUTION_DRAFT_LOG_MARKER (single source).
|
|
717
|
+
// Determine title based on session type See: https://github.com/link-assistant/hive-mind/issues/1152 Issue #1625: titles embed SOLUTION_DRAFT_LOG_MARKER (single source).
|
|
772
718
|
let title = customTitle;
|
|
773
719
|
let sessionNote = '';
|
|
774
720
|
if (sessionType === 'auto-resume') {
|
|
@@ -792,9 +738,7 @@ ${sessionNote}
|
|
|
792
738
|
}
|
|
793
739
|
const tempCommentFile = `/tmp/log-upload-comment-${targetType}-${Date.now()}.md`;
|
|
794
740
|
await writeSanitizedPublicationFile(tempCommentFile, logUploadComment);
|
|
795
|
-
// Issue #1625: post via postTrackedCommentFromFile so the returned
|
|
796
|
-
// comment ID is registered in-memory and excluded from the
|
|
797
|
-
// "did the AI post anything?" check.
|
|
741
|
+
// Issue #1625: post via postTrackedCommentFromFile so the returned comment ID is registered in-memory and excluded from the "did the AI post anything?" check.
|
|
798
742
|
let posted;
|
|
799
743
|
try {
|
|
800
744
|
posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber, bodyFile: tempCommentFile });
|
|
@@ -806,8 +750,7 @@ ${sessionNote}
|
|
|
806
750
|
await log(` ${status.emoji} ${status.label} uploaded to ${targetName} as ${isPublicRepo ? 'public' : 'private'} ${uploadTypeLabel}${chunkInfo}${posted.commentId ? ` (comment id=${posted.commentId})` : ''}`);
|
|
807
751
|
await log(` š Log URL: ${logUrl}`);
|
|
808
752
|
await log(` š Log size: ${Math.round(logStats.size / 1024)}KB`);
|
|
809
|
-
// Issue #1952: Record that a session log was attached anywhere in this process so the
|
|
810
|
-
// top-level --attach-logs safety net can guarantee no session finishes with no logs.
|
|
753
|
+
// Issue #1952: Record that a session log was attached anywhere in this process so the top-level --attach-logs safety net can guarantee no session finishes with no logs.
|
|
811
754
|
global.logAttachedToGitHub = true;
|
|
812
755
|
return true;
|
|
813
756
|
} else {
|
|
@@ -852,24 +795,19 @@ ${sessionNote}
|
|
|
852
795
|
async function attachRegularComment(options, logComment) {
|
|
853
796
|
const fs = (await use('fs')).promises;
|
|
854
797
|
const { targetType, targetNumber, owner, repo, $, log, logFile, errorMessage, errorDuringExecution, isUsageLimit } = options;
|
|
855
|
-
|
|
856
798
|
const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
|
|
857
799
|
const ghCommand = targetType === 'pr' ? 'pr' : 'issue';
|
|
858
800
|
void ghCommand;
|
|
859
801
|
const logStats = await fs.stat(logFile);
|
|
860
|
-
|
|
861
802
|
const tempFile = `/tmp/log-comment-${targetType}-${Date.now()}.md`;
|
|
862
803
|
await writeSanitizedPublicationFile(tempFile, logComment);
|
|
863
|
-
|
|
864
|
-
// Issue #1625: track the posted comment ID so it's excluded from the
|
|
865
|
-
// AI-authored-comment check in --auto-attach-solution-summary.
|
|
804
|
+
// Issue #1625: track the posted comment ID so it's excluded from the AI-authored-comment check in --auto-attach-solution-summary.
|
|
866
805
|
let posted;
|
|
867
806
|
try {
|
|
868
807
|
posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber, bodyFile: tempFile });
|
|
869
808
|
} finally {
|
|
870
809
|
await fs.unlink(tempFile).catch(() => {});
|
|
871
810
|
}
|
|
872
|
-
|
|
873
811
|
if (posted.ok) {
|
|
874
812
|
const status = getLogUploadTerminalStatus({ errorMessage, errorDuringExecution, isUsageLimit });
|
|
875
813
|
await log(` ${status.emoji} ${status.label} uploaded to ${targetName} as comment${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
|
|
@@ -978,8 +916,7 @@ export async function fetchProjectIssues(projectNumber, owner, statusFilter) {
|
|
|
978
916
|
await log(`š Fetching issues from GitHub Project #${projectNumber} (owner: ${owner}, status: ${statusFilter})`);
|
|
979
917
|
// Check for project scope in GitHub CLI authentication
|
|
980
918
|
try {
|
|
981
|
-
// Issue #2130: --show-token prints the token in clear text; mirroring it
|
|
982
|
-
// would put a live credential on stdout and in the log file.
|
|
919
|
+
// Issue #2130: --show-token prints the token in clear text; mirroring it would put a live credential on stdout and in the log file.
|
|
983
920
|
const authStatus = await $(QUIET_PROBE)`gh auth status --show-token`;
|
|
984
921
|
if (!authStatus.stdout.includes('project')) {
|
|
985
922
|
throw new Error('Missing project scope. Run: gh auth refresh -s project');
|
|
@@ -1055,264 +992,6 @@ export async function fetchProjectIssues(projectNumber, owner, statusFilter) {
|
|
|
1055
992
|
}
|
|
1056
993
|
// Re-export batch operations from separate module
|
|
1057
994
|
export const batchCheckPullRequestsForIssues = batchCheckPRs;
|
|
1058
|
-
/**
|
|
1059
|
-
* Universal GitHub URL parser that handles various formats
|
|
1060
|
-
* @param {string} url - The GitHub URL to parse
|
|
1061
|
-
* @returns {Object} Parsed URL information including:
|
|
1062
|
-
* - valid: boolean indicating if the URL is valid
|
|
1063
|
-
* - normalized: the normalized URL (https://github.com/...)
|
|
1064
|
-
* - type: 'user', 'repo', 'issue', 'pull', 'gist', 'actions', etc.
|
|
1065
|
-
* - owner: repository owner/organization
|
|
1066
|
-
* - repo: repository name (if applicable)
|
|
1067
|
-
* - number: issue/PR number (if applicable)
|
|
1068
|
-
* - path: additional path components
|
|
1069
|
-
* - error: error message if invalid
|
|
1070
|
-
*/
|
|
1071
|
-
export function parseGitHubUrl(url) {
|
|
1072
|
-
if (!url || typeof url !== 'string') {
|
|
1073
|
-
return {
|
|
1074
|
-
valid: false,
|
|
1075
|
-
error: 'Invalid input: URL must be a non-empty string',
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
// Trim whitespace and remove trailing slashes
|
|
1079
|
-
let normalizedUrl = url.trim().replace(/\/+$/, '');
|
|
1080
|
-
// Check if this looks like a valid GitHub-related input
|
|
1081
|
-
// Reject clearly invalid inputs (spaces in the URL, special chars at the start, etc.)
|
|
1082
|
-
if (/\s/.test(normalizedUrl) || /^[!@#$%^&*()[\]{}|\\:;"'<>,?`~]/.test(normalizedUrl)) {
|
|
1083
|
-
return {
|
|
1084
|
-
valid: false,
|
|
1085
|
-
error: 'Invalid GitHub URL format',
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
// Handle protocol normalization
|
|
1089
|
-
if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) {
|
|
1090
|
-
// Check if it starts with github.com
|
|
1091
|
-
if (normalizedUrl.startsWith('github.com/')) {
|
|
1092
|
-
normalizedUrl = 'https://' + normalizedUrl;
|
|
1093
|
-
} else if (!normalizedUrl.includes('github.com')) {
|
|
1094
|
-
// Assume it's a shorthand format (owner, owner/repo, owner/repo/issues/123, etc.)
|
|
1095
|
-
normalizedUrl = 'https://github.com/' + normalizedUrl;
|
|
1096
|
-
} else {
|
|
1097
|
-
// Has github.com somewhere but not at the start - likely malformed
|
|
1098
|
-
return {
|
|
1099
|
-
valid: false,
|
|
1100
|
-
error: 'Invalid GitHub URL format',
|
|
1101
|
-
};
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
// Convert http to https
|
|
1105
|
-
if (normalizedUrl.startsWith('http://')) {
|
|
1106
|
-
normalizedUrl = normalizedUrl.replace(/^http:\/\//, 'https://');
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
// Check for backslashes in the URL path (excluding query params and hash)
|
|
1110
|
-
// According to RFC 3986, backslash is not a valid character in URL paths
|
|
1111
|
-
const urlBeforeQueryAndHash = normalizedUrl.split('?')[0].split('#')[0];
|
|
1112
|
-
if (urlBeforeQueryAndHash.includes('\\')) {
|
|
1113
|
-
// Generate suggested URL by replacing backslashes with forward slashes
|
|
1114
|
-
const suggestedUrl = urlBeforeQueryAndHash.replace(/\\/g, '/');
|
|
1115
|
-
const urlAfterPath = normalizedUrl.substring(urlBeforeQueryAndHash.length);
|
|
1116
|
-
|
|
1117
|
-
return {
|
|
1118
|
-
valid: false,
|
|
1119
|
-
error: 'Invalid character in URL: backslash (\\) is not allowed in URL paths',
|
|
1120
|
-
suggestion: suggestedUrl + urlAfterPath,
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
// Parse the URL
|
|
1125
|
-
let urlObj;
|
|
1126
|
-
try {
|
|
1127
|
-
urlObj = new globalThis.URL(normalizedUrl);
|
|
1128
|
-
} catch (e) {
|
|
1129
|
-
if (global.verboseMode) {
|
|
1130
|
-
reportError(e, {
|
|
1131
|
-
context: 'github.lib.mjs - URL parsing',
|
|
1132
|
-
level: 'debug',
|
|
1133
|
-
url: normalizedUrl,
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
return {
|
|
1137
|
-
valid: false,
|
|
1138
|
-
error: 'Invalid URL format',
|
|
1139
|
-
};
|
|
1140
|
-
}
|
|
1141
|
-
// Ensure it's a GitHub URL
|
|
1142
|
-
if (urlObj.hostname !== 'github.com' && urlObj.hostname !== 'www.github.com') {
|
|
1143
|
-
return {
|
|
1144
|
-
valid: false,
|
|
1145
|
-
error: 'Not a GitHub URL',
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
// Normalize hostname
|
|
1149
|
-
if (urlObj.hostname === 'www.github.com') {
|
|
1150
|
-
normalizedUrl = normalizedUrl.replace('www.github.com', 'github.com');
|
|
1151
|
-
urlObj = new globalThis.URL(normalizedUrl);
|
|
1152
|
-
}
|
|
1153
|
-
// Parse the pathname
|
|
1154
|
-
const pathParts = urlObj.pathname.split('/').filter(p => p);
|
|
1155
|
-
// Handle different GitHub URL patterns
|
|
1156
|
-
const result = {
|
|
1157
|
-
valid: true,
|
|
1158
|
-
normalized: normalizedUrl,
|
|
1159
|
-
hostname: 'github.com',
|
|
1160
|
-
protocol: 'https',
|
|
1161
|
-
path: urlObj.pathname,
|
|
1162
|
-
};
|
|
1163
|
-
// No path - just github.com
|
|
1164
|
-
if (pathParts.length === 0) {
|
|
1165
|
-
result.type = 'home';
|
|
1166
|
-
return result;
|
|
1167
|
-
}
|
|
1168
|
-
// User/Organization page: /owner
|
|
1169
|
-
if (pathParts.length === 1) {
|
|
1170
|
-
result.type = 'user';
|
|
1171
|
-
result.owner = pathParts[0];
|
|
1172
|
-
return result;
|
|
1173
|
-
}
|
|
1174
|
-
// Set owner for all other cases
|
|
1175
|
-
result.owner = pathParts[0];
|
|
1176
|
-
// Repository page: /owner/repo
|
|
1177
|
-
if (pathParts.length === 2) {
|
|
1178
|
-
result.type = 'repo';
|
|
1179
|
-
result.repo = pathParts[1];
|
|
1180
|
-
return result;
|
|
1181
|
-
}
|
|
1182
|
-
// Set repo for paths with 3+ parts
|
|
1183
|
-
result.repo = pathParts[1];
|
|
1184
|
-
// Handle specific GitHub paths
|
|
1185
|
-
const thirdPart = pathParts[2];
|
|
1186
|
-
switch (thirdPart) {
|
|
1187
|
-
case 'issues':
|
|
1188
|
-
if (pathParts.length === 3) {
|
|
1189
|
-
// /owner/repo/issues - issues list
|
|
1190
|
-
result.type = 'issues_list';
|
|
1191
|
-
} else if (pathParts.length === 4 && /^\d+$/.test(pathParts[3])) {
|
|
1192
|
-
// /owner/repo/issues/123 - specific issue
|
|
1193
|
-
result.type = 'issue';
|
|
1194
|
-
result.number = parseInt(pathParts[3]);
|
|
1195
|
-
} else {
|
|
1196
|
-
result.type = 'issues_page';
|
|
1197
|
-
result.subpath = pathParts.slice(3).join('/');
|
|
1198
|
-
}
|
|
1199
|
-
break;
|
|
1200
|
-
case 'pull':
|
|
1201
|
-
if (pathParts.length === 4 && /^\d+$/.test(pathParts[3])) {
|
|
1202
|
-
// /owner/repo/pull/456 - specific PR
|
|
1203
|
-
result.type = 'pull';
|
|
1204
|
-
result.number = parseInt(pathParts[3]);
|
|
1205
|
-
} else {
|
|
1206
|
-
result.type = 'pull_page';
|
|
1207
|
-
result.subpath = pathParts.slice(3).join('/');
|
|
1208
|
-
}
|
|
1209
|
-
break;
|
|
1210
|
-
case 'pulls':
|
|
1211
|
-
// /owner/repo/pulls - PR list
|
|
1212
|
-
result.type = 'pulls_list';
|
|
1213
|
-
if (pathParts.length > 3) {
|
|
1214
|
-
result.subpath = pathParts.slice(3).join('/');
|
|
1215
|
-
}
|
|
1216
|
-
break;
|
|
1217
|
-
case 'actions':
|
|
1218
|
-
// /owner/repo/actions - GitHub Actions
|
|
1219
|
-
result.type = 'actions';
|
|
1220
|
-
if (pathParts.length > 3) {
|
|
1221
|
-
result.subpath = pathParts.slice(3).join('/');
|
|
1222
|
-
if (pathParts[3] === 'runs' && pathParts[4] && /^\d+$/.test(pathParts[4])) {
|
|
1223
|
-
result.type = 'action_run';
|
|
1224
|
-
result.runId = parseInt(pathParts[4]);
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
break;
|
|
1228
|
-
case 'releases':
|
|
1229
|
-
// /owner/repo/releases
|
|
1230
|
-
result.type = 'releases';
|
|
1231
|
-
if (pathParts.length > 3) {
|
|
1232
|
-
result.subpath = pathParts.slice(3).join('/');
|
|
1233
|
-
if (pathParts[3] === 'tag' && pathParts[4]) {
|
|
1234
|
-
result.type = 'release';
|
|
1235
|
-
result.tag = pathParts[4];
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
break;
|
|
1239
|
-
case 'tree':
|
|
1240
|
-
case 'blob':
|
|
1241
|
-
// /owner/repo/tree/branch or /owner/repo/blob/branch/file
|
|
1242
|
-
result.type = thirdPart === 'tree' ? 'tree' : 'file';
|
|
1243
|
-
if (pathParts.length > 3) {
|
|
1244
|
-
result.branch = pathParts[3];
|
|
1245
|
-
if (pathParts.length > 4) {
|
|
1246
|
-
result.filepath = pathParts.slice(4).join('/');
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
break;
|
|
1250
|
-
case 'commit':
|
|
1251
|
-
case 'commits':
|
|
1252
|
-
// /owner/repo/commit/sha or /owner/repo/commits/branch
|
|
1253
|
-
result.type = thirdPart === 'commit' ? 'commit' : 'commits';
|
|
1254
|
-
if (pathParts.length > 3) {
|
|
1255
|
-
result.ref = pathParts[3]; // Could be SHA or branch
|
|
1256
|
-
}
|
|
1257
|
-
break;
|
|
1258
|
-
case 'compare':
|
|
1259
|
-
// /owner/repo/compare/base...head
|
|
1260
|
-
result.type = 'compare';
|
|
1261
|
-
if (pathParts.length > 3) {
|
|
1262
|
-
result.comparison = pathParts[3];
|
|
1263
|
-
}
|
|
1264
|
-
break;
|
|
1265
|
-
case 'wiki':
|
|
1266
|
-
// /owner/repo/wiki
|
|
1267
|
-
result.type = 'wiki';
|
|
1268
|
-
if (pathParts.length > 3) {
|
|
1269
|
-
result.subpath = pathParts.slice(3).join('/');
|
|
1270
|
-
}
|
|
1271
|
-
break;
|
|
1272
|
-
case 'settings':
|
|
1273
|
-
// /owner/repo/settings
|
|
1274
|
-
result.type = 'settings';
|
|
1275
|
-
if (pathParts.length > 3) {
|
|
1276
|
-
result.subpath = pathParts.slice(3).join('/');
|
|
1277
|
-
}
|
|
1278
|
-
break;
|
|
1279
|
-
case 'projects':
|
|
1280
|
-
// /owner/repo/projects or /owner/repo/projects/1
|
|
1281
|
-
result.type = 'projects';
|
|
1282
|
-
if (pathParts.length > 3 && /^\d+$/.test(pathParts[3])) {
|
|
1283
|
-
result.type = 'project';
|
|
1284
|
-
result.projectNumber = parseInt(pathParts[3]);
|
|
1285
|
-
}
|
|
1286
|
-
break;
|
|
1287
|
-
default:
|
|
1288
|
-
// Unknown path structure but still valid GitHub URL
|
|
1289
|
-
result.type = 'other';
|
|
1290
|
-
result.subpath = pathParts.slice(2).join('/');
|
|
1291
|
-
}
|
|
1292
|
-
return result;
|
|
1293
|
-
}
|
|
1294
|
-
/**
|
|
1295
|
-
* Normalize a GitHub URL to standard https://github.com format
|
|
1296
|
-
* This is a convenience function that uses parseGitHubUrl
|
|
1297
|
-
* @param {string} url - The URL to normalize
|
|
1298
|
-
* @returns {string|null} The normalized URL or null if invalid
|
|
1299
|
-
*/
|
|
1300
|
-
export function normalizeGitHubUrl(url) {
|
|
1301
|
-
const parsed = parseGitHubUrl(url);
|
|
1302
|
-
return parsed.valid ? parsed.normalized : null;
|
|
1303
|
-
}
|
|
1304
|
-
/**
|
|
1305
|
-
* Check if a URL is a valid GitHub URL of a specific type
|
|
1306
|
-
* @param {string} url - The URL to check
|
|
1307
|
-
* @param {string|Array} types - The type(s) to check for ('issue', 'pull', 'repo', etc.)
|
|
1308
|
-
* @returns {boolean} True if the URL matches the specified type(s)
|
|
1309
|
-
*/
|
|
1310
|
-
export function isGitHubUrlType(url, types) {
|
|
1311
|
-
const parsed = parseGitHubUrl(url);
|
|
1312
|
-
if (!parsed.valid) return false;
|
|
1313
|
-
const typeArray = Array.isArray(types) ? types : [types];
|
|
1314
|
-
return typeArray.includes(parsed.type);
|
|
1315
|
-
}
|
|
1316
995
|
/**
|
|
1317
996
|
* Universal function to view a pull request using gh pr view
|
|
1318
997
|
* @param {Object} options - Configuration options
|