@link-assistant/hive-mind 1.56.11 → 1.56.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.56.12
4
+
5
+ ### Patch Changes
6
+
7
+ - 71e1ef5: Prevent `--attach-logs` from posting truncated fallback comments when full `gh-upload-log` uploads fail, and parse newer `gh-upload-log` repository output including shared-repository paths.
8
+
3
9
  ## 1.56.11
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.56.11",
3
+ "version": "1.56.12",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  "hive-telegram-bot": "./src/telegram-bot.mjs"
16
16
  },
17
17
  "scripts": {
18
- "test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-auto-restart-limits-1664.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-telegram-options-before-url.mjs && node tests/test-telegram-bot-configuration-isolation-links-notation.mjs && node tests/test-extract-isolation-from-args.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-issue-1670-screen-status-monitoring.mjs && node tests/test-telegram-bot-launcher.mjs",
18
+ "test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-auto-restart-limits-1664.mjs && node tests/test-log-upload-output-1678.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-telegram-options-before-url.mjs && node tests/test-telegram-bot-configuration-isolation-links-notation.mjs && node tests/test-extract-isolation-from-args.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-issue-1670-screen-status-monitoring.mjs && node tests/test-telegram-bot-launcher.mjs",
19
19
  "test:queue": "node tests/solve-queue.test.mjs",
20
20
  "test:limits-display": "node tests/limits-display.test.mjs",
21
21
  "test:usage-limit": "node tests/test-usage-limit.mjs",
@@ -614,7 +614,10 @@ ${logContent}
614
614
  if (uploadResult.success) {
615
615
  // Use rawUrl for direct file access (single chunk) or url for repository (multiple chunks)
616
616
  // Requirements: 1 chunk = direct raw link, >1 chunks = repo link
617
- const logUrl = uploadResult.chunks === 1 ? uploadResult.rawUrl : uploadResult.url;
617
+ // Private repository raw URLs can contain short-lived tokens, so keep
618
+ // private uploads on the stable repository/tree page URL.
619
+ const useRawLogUrl = uploadResult.chunks === 1 && uploadResult.rawUrl && (isPublicRepo || uploadResult.type !== 'repository');
620
+ const logUrl = useRawLogUrl ? uploadResult.rawUrl : uploadResult.url;
618
621
  const uploadTypeLabel = uploadResult.type === 'gist' ? 'Gist' : 'Repository';
619
622
  const chunkInfo = uploadResult.chunks > 1 ? ` (${uploadResult.chunks} chunks)` : '';
620
623
 
@@ -752,10 +755,9 @@ ${sessionNote}
752
755
  }
753
756
  } else {
754
757
  await log(' ❌ gh-upload-log failed');
755
-
756
- // Fallback to truncated comment
757
- await log(' 🔄 Falling back to truncated comment...');
758
- return await attachTruncatedLog(options);
758
+ await log(' ⚠️ Full log upload failed; not posting a truncated log because --attach-logs must preserve complete logs');
759
+ await log(` 📁 Full log remains available locally at: ${logFile}`);
760
+ return false;
759
761
  }
760
762
  } catch (uploadError) {
761
763
  reportError(uploadError, {
@@ -763,8 +765,9 @@ ${sessionNote}
763
765
  level: 'error',
764
766
  });
765
767
  await log(` ❌ Error uploading log: ${uploadError.message}`);
766
- // Try regular comment as last resort
767
- return await attachRegularComment(options, logComment);
768
+ await log(' ⚠️ Full log upload failed; not posting a truncated log because --attach-logs must preserve complete logs');
769
+ await log(` 📁 Full log remains available locally at: ${logFile}`);
770
+ return false;
768
771
  }
769
772
  } else {
770
773
  // Comment fits within limit
@@ -777,60 +780,6 @@ ${sessionNote}
777
780
  return false;
778
781
  }
779
782
  }
780
- /**
781
- * Helper to attach a truncated log when full log is too large
782
- */
783
- async function attachTruncatedLog(options) {
784
- const fs = (await use('fs')).promises;
785
- const { logFile, targetType, targetNumber, owner, repo, $, log, sanitizeLogContent } = options;
786
-
787
- const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
788
- const ghCommand = targetType === 'pr' ? 'pr' : 'issue';
789
-
790
- const rawLogContent = await fs.readFile(logFile, 'utf8');
791
- let logContent = await sanitizeLogContent(rawLogContent);
792
- // Escape code blocks to prevent markdown breaking
793
- logContent = escapeCodeBlocksInLog(logContent);
794
- const logStats = await fs.stat(logFile);
795
-
796
- const GITHUB_COMMENT_LIMIT = 65536;
797
- const maxContentLength = GITHUB_COMMENT_LIMIT - 500;
798
- const truncatedContent = logContent.substring(0, maxContentLength) + '\n\n[... Log truncated due to length ...]';
799
-
800
- const truncatedComment = `## 🤖 ${SOLUTION_DRAFT_LOG_MARKER} (Truncated)
801
- This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.
802
- ⚠️ **Log was truncated** due to GitHub comment size limits.
803
-
804
- <details>
805
- <summary>Click to expand solution draft log (${Math.round(logStats.size / 1024)}KB, truncated)</summary>
806
-
807
- \`\`\`
808
- ${truncatedContent}
809
- \`\`\`
810
-
811
- </details>
812
-
813
- ---
814
- *${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
815
- const tempFile = `/tmp/log-truncated-comment-${targetType}-${Date.now()}.md`;
816
- await fs.writeFile(tempFile, truncatedComment);
817
-
818
- // Issue #1625: track the posted comment ID so it's excluded from the
819
- // AI-authored-comment check in --auto-attach-solution-summary.
820
- const posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber, bodyFile: tempFile });
821
- await fs.unlink(tempFile).catch(() => {});
822
- // ghCommand and targetName are retained in signature for symmetry with
823
- // attachLogToGitHub's logging vocabulary.
824
- void ghCommand;
825
- if (posted.ok) {
826
- await log(` ✅ Truncated solution draft log uploaded to ${targetName}${posted.commentId ? ` (comment id=${posted.commentId})` : ''}`);
827
- await log(` 📊 Log size: ${Math.round(logStats.size / 1024)}KB (truncated)`);
828
- return true;
829
- } else {
830
- await log(` ❌ Failed to upload truncated log: ${posted.stderr || 'unknown error'}`);
831
- return false;
832
- }
833
- }
834
783
  /**
835
784
  * Helper to attach a regular comment when it fits within limits
836
785
  */
@@ -27,6 +27,54 @@ const summarizeCommandOutput = value => {
27
27
  return text.length > 500 ? `${text.slice(0, 500)}... [truncated ${text.length - 500} chars]` : text;
28
28
  };
29
29
 
30
+ export const parseGhUploadLogOutput = outputValue => {
31
+ const output = outputValue?.toString?.() || '';
32
+ const parsed = {
33
+ url: null,
34
+ rawUrl: null,
35
+ type: null,
36
+ chunks: 1,
37
+ repositoryName: null,
38
+ repositoryPath: null,
39
+ };
40
+
41
+ const urlMatch = output.match(/(?:^|\n)🔗\s+(https:\/\/[^\s\n]+)/u);
42
+ if (urlMatch) {
43
+ parsed.url = urlMatch[1].trim();
44
+ }
45
+
46
+ const rawUrlMatch = output.match(/(?:^|\n)📄\s+(https:\/\/[^\s\n]+)/u);
47
+ if (rawUrlMatch) {
48
+ parsed.rawUrl = rawUrlMatch[1].trim();
49
+ }
50
+
51
+ if (output.includes('Type: 📝 Gist') || parsed.url?.includes('gist.github.com')) {
52
+ parsed.type = 'gist';
53
+ } else if (output.includes('Type: 📦 Repository') || (parsed.url?.includes('github.com') && !parsed.url?.includes('gist'))) {
54
+ parsed.type = 'repository';
55
+ }
56
+
57
+ const fileCountMatch = output.match(/File count:\s*(\d+)/i);
58
+ const chunkMatch = output.match(/split into (\d+) chunks/i);
59
+ if (fileCountMatch) {
60
+ parsed.chunks = parseInt(fileCountMatch[1], 10);
61
+ } else if (chunkMatch) {
62
+ parsed.chunks = parseInt(chunkMatch[1], 10);
63
+ }
64
+
65
+ const repositoryMatch = output.match(/Repository:\s*([^\s\n]+)/i);
66
+ if (repositoryMatch) {
67
+ parsed.repositoryName = repositoryMatch[1].trim();
68
+ }
69
+
70
+ const pathMatch = output.match(/Path:\s*([^\s\n]+)/i);
71
+ if (pathMatch) {
72
+ parsed.repositoryPath = pathMatch[1].trim();
73
+ }
74
+
75
+ return parsed;
76
+ };
77
+
30
78
  /**
31
79
  * Upload a log file using gh-upload-log command
32
80
  * @param {Object} options - Upload options
@@ -34,7 +82,7 @@ const summarizeCommandOutput = value => {
34
82
  * @param {boolean} options.isPublic - Whether to make the upload public
35
83
  * @param {string} options.description - Description for the upload
36
84
  * @param {boolean} [options.verbose=false] - Enable verbose logging
37
- * @returns {Promise<{success: boolean, url: string|null, rawUrl: string|null, type: 'gist'|'repository'|null, chunks: number}>}
85
+ * @returns {Promise<{success: boolean, url: string|null, rawUrl: string|null, type: 'gist'|'repository'|null, chunks: number, repositoryName?: string|null, repositoryPath?: string|null}>}
38
86
  */
39
87
  export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description, verbose = false }) => {
40
88
  const result = { success: false, url: null, rawUrl: null, type: null, chunks: 1 };
@@ -71,25 +119,7 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
71
119
  return result;
72
120
  }
73
121
 
74
- // Parse output to extract URL and type
75
- // Look for the URL line: 🔗 https://...
76
- const urlMatch = output.match(/🔗\s+(https:\/\/[^\s\n]+)/);
77
- if (urlMatch) {
78
- result.url = urlMatch[1].trim();
79
- }
80
-
81
- // Determine type from output
82
- if (output.includes('Type: 📝 Gist') || result.url?.includes('gist.github.com')) {
83
- result.type = 'gist';
84
- } else if (output.includes('Type: 📦 Repository') || (result.url?.includes('github.com') && !result.url?.includes('gist'))) {
85
- result.type = 'repository';
86
- }
87
-
88
- // Extract chunk count if mentioned
89
- const chunkMatch = output.match(/split into (\d+) chunks/i);
90
- if (chunkMatch) {
91
- result.chunks = parseInt(chunkMatch[1], 10);
92
- }
122
+ Object.assign(result, parseGhUploadLogOutput(output));
93
123
 
94
124
  // Construct raw URL based on type and chunks
95
125
  if (result.url) {
@@ -139,17 +169,23 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
139
169
  result.rawUrl = result.url;
140
170
  }
141
171
  } else if (result.type === 'repository') {
142
- if (result.chunks === 1) {
172
+ if (result.rawUrl) {
173
+ // gh-upload-log v0.8+ prints the exact raw/download URL. Prefer it
174
+ // over reconstructing paths, especially for shared repositories.
175
+ } else if (result.chunks === 1) {
143
176
  // For single chunk repository: construct raw URL to the file
144
177
  // Repository URL format: https://github.com/owner/repo
145
178
  // We need to find the actual file name in the repo
146
179
  try {
147
180
  const repoUrl = result.url;
148
- const repoPath = repoUrl.replace('https://github.com/', '');
181
+ const repoMatch = repoUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+)\/(.+))?$/);
182
+ const [, repoOwner, repoName, branchName = 'main', treePath = null] = repoMatch || [];
183
+ const repoPath = repoOwner && repoName ? `${repoOwner}/${repoName}` : repoUrl.replace('https://github.com/', '');
184
+ const apiPath = treePath ? `repos/${repoPath}/contents/${treePath}?ref=${branchName}` : `repos/${repoPath}/contents`;
149
185
  if (verbose) {
150
186
  await log(` 🔍 Fetching repository contents for raw URL resolution (repoPath=${repoPath})`, { verbose: true });
151
187
  }
152
- const contentsResult = await $silent`gh api repos/${repoPath}/contents --paginate --jq '.[].name'`;
188
+ const contentsResult = await $silent`gh api ${apiPath} --paginate --jq '.[].name'`;
153
189
  if (verbose) {
154
190
  await log(` 📥 Repository contents fetch completed (code=${contentsResult.code ?? 'unknown'})`, { verbose: true });
155
191
  }
@@ -161,7 +197,9 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
161
197
  .filter(f => f && !f.startsWith('.'));
162
198
  if (files.length > 0) {
163
199
  const fileName = files[0];
164
- result.rawUrl = `${repoUrl}/raw/main/${fileName}`;
200
+ const rawPath = treePath ? `${treePath}/${fileName}` : fileName;
201
+ const baseRepoUrl = repoOwner && repoName ? `https://github.com/${repoOwner}/${repoName}` : repoUrl;
202
+ result.rawUrl = `${baseRepoUrl}/raw/${branchName}/${rawPath}`;
165
203
  if (verbose) {
166
204
  await log(` 🧩 Repository contents resolved fileName=${fileName}`, { verbose: true });
167
205
  }
@@ -212,5 +250,6 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
212
250
 
213
251
  // Export all functions as default object too
214
252
  export default {
253
+ parseGhUploadLogOutput,
215
254
  uploadLogWithGhUploadLog,
216
255
  };