@link-assistant/hive-mind 2.11.8 → 2.11.10

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.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { QUIET_PROBE } from './quiet-probe.lib.mjs';
2
3
  import { ensureUseM } from './use-m-bootstrap.lib.mjs';
3
4
 
4
5
  /**
@@ -68,7 +69,8 @@ export const collectDeferredWorkSources = async ({ owner, repo, prNumber, result
68
69
 
69
70
  // 1. Pull request description
70
71
  try {
71
- const prResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.body // ""'`;
72
+ // Issue #2135: `mirror: false` - the description is scanned here, not shown.
73
+ const prResult = await $(QUIET_PROBE)`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.body // ""'`;
72
74
  if (prResult.code === 0) {
73
75
  const body = prResult.stdout.toString();
74
76
  if (body && body.trim()) {
@@ -86,7 +88,10 @@ export const collectDeferredWorkSources = async ({ owner, repo, prNumber, result
86
88
 
87
89
  // 3. Changed markdown documents (scan only added lines from the diff)
88
90
  try {
89
- const filesResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/files --paginate`;
91
+ // Issue #2135: `mirror: false`. Every entry carries the file's patch, so
92
+ // this answer is as large as the pull request's diff - and it was being
93
+ // echoed into the log that gets attached to that same pull request.
94
+ const filesResult = await $(QUIET_PROBE)`gh api repos/${owner}/${repo}/pulls/${prNumber}/files --paginate`;
90
95
  if (filesResult.code === 0) {
91
96
  const files = JSON.parse(filesResult.stdout.toString() || '[]');
92
97
  for (const file of files) {
@@ -9,6 +9,8 @@
9
9
  * @see case-studies/issue-661-session-resume-cost-optimization/
10
10
  */
11
11
 
12
+ import { QUIET_PROBE } from './quiet-probe.lib.mjs';
13
+
12
14
  // Note: This module does not import $ directly
13
15
  // Functions receive $ as a parameter from the calling module
14
16
  // This ensures consistent command executor usage across the codebase
@@ -28,9 +30,11 @@ export const generateMinimalRestartPrompt = async (tempDir, $) => {
28
30
  const uncommittedFiles = gitStatus.stdout.toString().trim();
29
31
 
30
32
  // Get brief diff summaries (not full diffs to keep the prompt minimal)
31
- const gitDiffStat = await $({ cwd: tempDir })`git diff --stat`;
33
+ // Issue #2135: `mirror: false` - the summaries go into the prompt below, so
34
+ // echoing them into the log only duplicates them into the attached log file.
35
+ const gitDiffStat = await $({ cwd: tempDir, ...QUIET_PROBE })`git diff --stat`;
32
36
  const unstagedDiffSummary = gitDiffStat.stdout.toString().trim();
33
- const gitCachedDiffStat = await $({ cwd: tempDir })`git diff --cached --stat`;
37
+ const gitCachedDiffStat = await $({ cwd: tempDir, ...QUIET_PROBE })`git diff --cached --stat`;
34
38
  const stagedDiffSummary = gitCachedDiffStat.stdout.toString().trim();
35
39
  const summarySections = [];
36
40
  if (unstagedDiffSummary) summarySections.push(`Unstaged changes:\n${unstagedDiffSummary}`);
@@ -69,7 +73,11 @@ export const generateFullRestartPrompt = async (issueUrl, issueBody, prNumber, f
69
73
  const gitStatus = await $({ cwd: tempDir })`git status --porcelain`;
70
74
  const uncommittedFiles = gitStatus.stdout.toString().trim();
71
75
 
72
- const gitDiff = await $({ cwd: tempDir })`git diff`;
76
+ // Issue #2135: `mirror: false`. This is the working tree's whole diff and it
77
+ // is embedded in the prompt below; mirroring it wrote a second copy into the
78
+ // session log, which is attached to the pull request and (with
79
+ // --development-log) committed into the branch the diff is taken from.
80
+ const gitDiff = await $({ cwd: tempDir, ...QUIET_PROBE })`git diff`;
73
81
  const fullDiff = gitDiff.stdout.toString();
74
82
 
75
83
  let prompt = `
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
7
+ import { quietProbe } from './quiet-probe.lib.mjs';
7
8
  // Import feedback detection functionality
8
9
  const feedback = await import('./solve.feedback.lib.mjs');
9
10
  const { detectAndCountFeedback } = feedback;
@@ -45,7 +46,10 @@ export async function prepareFeedbackAndTimestamps({ tempDir = null, prNumber, b
45
46
 
46
47
  // Get the last comment's timestamp (if any)
47
48
  // Use --paginate to get all comments - GitHub API returns max 30 per page by default
48
- const commentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
49
+ // Issue #2135: `mirror: false`. Only the last comment's timestamp is read
50
+ // from this answer, but the answer itself is every comment body on the
51
+ // issue - tens of kilobytes echoed into the log on every run.
52
+ const commentsResult = await quietProbe($)`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
49
53
 
50
54
  if (commentsResult.code !== 0) {
51
55
  await log(`Warning: Failed to get comments: ${commentsResult.stderr ? commentsResult.stderr.toString() : 'Unknown error'}`, { level: 'warning' });
@@ -30,6 +30,7 @@ import { LIVE_PROGRESS_SECTION_START_MARKER, LIVE_PROGRESS_SECTION_END_MARKER, p
30
30
  import { writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
31
31
 
32
32
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
33
+ import { quietProbe } from './quiet-probe.lib.mjs'; // issue #2135: keep large read-only probe payloads out of the attached log
33
34
  /**
34
35
  * Configuration constants for progress monitoring
35
36
  */
@@ -280,7 +281,10 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
280
281
  state.currentTodos = todos;
281
282
 
282
283
  // Fetch current PR description
283
- const prData = await $`gh pr view ${prNumber} --repo ${owner}/${repo} --json body`;
284
+ // Issue #2135: `mirror: false`. This runs on every progress update and
285
+ // the answer is the whole pull-request description, which by then holds
286
+ // the progress section itself.
287
+ const prData = await quietProbe($)`gh pr view ${prNumber} --repo ${owner}/${repo} --json body`;
284
288
  const prInfo = JSON.parse(prData.stdout);
285
289
  let currentBody = prInfo.body || '';
286
290
 
@@ -65,7 +65,8 @@ export const checkExistingForkOfRoot = async rootRepo => {
65
65
  // not to the shell, and command-stream quotes interpolated values itself - so
66
66
  // interpolating inside the quotes would leak shell quotes into the comparison.
67
67
  const forkFilter = `.[] | select(.owner.login == ${JSON.stringify(currentUser)}) | .full_name`;
68
- const forksResult = await lib.ghCmdRetry(() => $`gh api repos/${rootRepo}/forks --paginate --jq ${forkFilter}`, { label: `check forks of ${rootRepo}` });
68
+ // Issue #2135: `mirror: false` - see the fork-name lookup below.
69
+ const forksResult = await lib.ghCmdRetry(() => $(QUIET_PROBE)`gh api repos/${rootRepo}/forks --paginate --jq ${forkFilter}`, { label: `check forks of ${rootRepo}` });
69
70
  if (forksResult.code !== 0) return null;
70
71
 
71
72
  const forks = forksResult.stdout
@@ -1225,7 +1226,9 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont
1225
1226
  // Issue #2119: the double quotes here are jq syntax, so the expression is
1226
1227
  // built in JS and interpolated as one already-escaped argument.
1227
1228
  const forkNameFilter = `.[] | select(.owner.login == ${JSON.stringify(prForkOwner)}) | .name`;
1228
- const forksResult = await $`gh api repos/${owner}/${repo}/forks --paginate --jq ${forkNameFilter}`;
1229
+ // Issue #2135: `mirror: false` - a popular repository has thousands of
1230
+ // forks, and only the matching name is used.
1231
+ const forksResult = await $(QUIET_PROBE)`gh api repos/${owner}/${repo}/forks --paginate --jq ${forkNameFilter}`;
1229
1232
  if (forksResult.code === 0 && forksResult.stdout) {
1230
1233
  const forkName = forksResult.stdout.toString().trim().split('\n')[0]; // Take first match
1231
1234
  if (forkName) {
@@ -407,7 +407,9 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
407
407
  // APPROACH 3: Check for modifications before reverting (proactive detection)
408
408
  // This is the main strategy - detect if the file was modified after initial commit
409
409
  await log(` Checking if ${fileName} was modified since initial commit...`, { verbose: true });
410
- const diffResult = await $({ cwd: tempDir })`git diff ${commitToRevert} HEAD -- ${fileName} 2>&1`;
410
+ // Issue #2135: `mirror: false`. Only "is it non-empty" is asked here, and
411
+ // the answer is a file's whole diff.
412
+ const diffResult = await $({ cwd: tempDir, ...QUIET_PROBE })`git diff ${commitToRevert} HEAD -- ${fileName} 2>&1`;
411
413
 
412
414
  if (diffResult.stdout && diffResult.stdout.trim()) {
413
415
  // File was modified after initial commit - use manual approach to avoid conflicts
@@ -746,7 +748,8 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
746
748
  // First, get all PRs from our branch
747
749
  // IMPORTANT: Use --state all to find PRs that may have been merged during the session (Issue #1008)
748
750
  // Without --state all, gh pr list only returns OPEN PRs, missing merged ones
749
- const allBranchPrsResult = await $`gh pr list --repo ${owner}/${repo} --head ${branchName} --state all --json number,url,createdAt,headRefName,title,state,updatedAt,isDraft`;
751
+ // Issue #2135: `mirror: false` - the pull requests found are named below.
752
+ const allBranchPrsResult = await $(QUIET_PROBE)`gh pr list --repo ${owner}/${repo} --head ${branchName} --state all --json number,url,createdAt,headRefName,title,state,updatedAt,isDraft`;
750
753
 
751
754
  if (allBranchPrsResult.code !== 0) {
752
755
  await log(' ⚠️ Failed to check pull requests');
@@ -819,7 +822,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
819
822
  // "1 file(s) modified, 1 line(s) added" for a pull request that
820
823
  // changed nothing, because the stats were never checked for being
821
824
  // empty.
822
- const changeStats = await getPullRequestChangeStats({ owner, repo, prNumber: pr.number, $ });
825
+ const changeStats = await getPullRequestChangeStats({ owner, repo, prNumber: pr.number, $, log });
823
826
  if (!changeStats.hasChanges) {
824
827
  await log(` ⚠️ PR #${pr.number} has an empty diff - the description will say so instead of claiming changes`, { level: 'warning' });
825
828
  }
@@ -948,7 +951,9 @@ Fixes ${issueRef}
948
951
 
949
952
  // Get all comments and filter them
950
953
  // Use --paginate to get all comments - GitHub API returns max 30 per page by default
951
- const allCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
954
+ // Issue #2135: `mirror: false` - the counts below are the report; the raw
955
+ // answer is every comment body on the issue.
956
+ const allCommentsResult = await $(QUIET_PROBE)`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
952
957
 
953
958
  if (allCommentsResult.code !== 0) {
954
959
  await log(' ⚠️ Failed to check comments');
@@ -1209,7 +1214,7 @@ export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, p
1209
1214
  // Check comments on the PR first (if we have a PR)
1210
1215
  if (prNumber) {
1211
1216
  // Check PR conversation comments
1212
- const prCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
1217
+ const prCommentsResult = await $(QUIET_PROBE)`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
1213
1218
  if (prCommentsResult.code === 0) {
1214
1219
  const prComments = JSON.parse(prCommentsResult.stdout.toString().trim() || '[]');
1215
1220
  const newPrComments = filterNewAiComments(prComments, 'pr');
@@ -1220,7 +1225,7 @@ export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, p
1220
1225
  }
1221
1226
 
1222
1227
  // Check PR review comments (inline code comments)
1223
- const reviewCommentsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
1228
+ const reviewCommentsResult = await $(QUIET_PROBE)`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
1224
1229
  if (reviewCommentsResult.code === 0) {
1225
1230
  const reviewComments = JSON.parse(reviewCommentsResult.stdout.toString().trim() || '[]');
1226
1231
  const newReviewComments = filterNewAiComments(reviewComments, 'review');
@@ -1233,7 +1238,7 @@ export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, p
1233
1238
 
1234
1239
  // Check issue comments (if different from PR number or no PR)
1235
1240
  if (issueNumber && issueNumber !== prNumber) {
1236
- const issueCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
1241
+ const issueCommentsResult = await $(QUIET_PROBE)`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
1237
1242
  if (issueCommentsResult.code === 0) {
1238
1243
  const issueComments = JSON.parse(issueCommentsResult.stdout.toString().trim() || '[]');
1239
1244
  const newIssueComments = filterNewAiComments(issueComments, 'issue');
@@ -1413,7 +1418,7 @@ export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, wo
1413
1418
  : null);
1414
1419
  // Issue #2119: a summary posted on a pull request that changed nothing must
1415
1420
  // say so, instead of reading as a report of completed work.
1416
- const changeStats = prNumber ? await getPullRequestChangeStats({ owner, repo, prNumber, $ }) : null;
1421
+ const changeStats = prNumber ? await getPullRequestChangeStats({ owner, repo, prNumber, $, log }) : null;
1417
1422
 
1418
1423
  // Issue #2132: the summary carries no cost/budget block. `resolvedBudgetStatsData`
1419
1424
  // is computed only so the caller can reuse it for this session's log comment.
package/src/task.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
  import crypto from 'crypto';
4
4
  import path from 'path';
5
5
  import { spawn } from 'child_process';
6
+ import { describeChildExit } from './child-exit.lib.mjs';
6
7
  import { promises as fs } from 'fs';
7
8
  import { buildStartAgentArgs, resolveStartAgentCommand } from './task.agent-command.lib.mjs';
8
9
  import { getDefaultTaskModel, parseTaskArguments } from './task.config.lib.mjs';
@@ -112,8 +113,8 @@ function runCommand(command, args, options = {}) {
112
113
  child.on('error', error => {
113
114
  resolve({ code: 1, stdout, stderr: stderr || error.message });
114
115
  });
115
- child.on('close', code => {
116
- resolve({ code, stdout, stderr });
116
+ child.on('close', (code, signal) => {
117
+ resolve({ code, stdout, stderr, signal });
117
118
  });
118
119
  });
119
120
  }
@@ -122,7 +123,8 @@ async function commandOutput(command, args, options = {}) {
122
123
  const result = await runCommand(command, args, options);
123
124
  if (result.code !== 0) {
124
125
  const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
125
- throw new Error(output || `${command} exited with code ${result.code}`);
126
+ // Issue #2135: `describeChildExit` names a signal instead of "code null".
127
+ throw new Error(output || describeChildExit({ command, code: result.code, signal: result.signal }));
126
128
  }
127
129
  return result.stdout.trim();
128
130
  }
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'child_process';
2
+ import { describeChildExit } from './child-exit.lib.mjs';
2
3
  import { promisify } from 'util';
3
4
  import { exec as execCallback } from 'child_process';
4
5
  import { t } from './i18n.lib.mjs';
@@ -57,7 +58,7 @@ function executeWithCommand(startScreenCmd, command, args, verbose = false) {
57
58
  });
58
59
  });
59
60
 
60
- child.on('close', code => {
61
+ child.on('close', (code, signal) => {
61
62
  if (code === 0) {
62
63
  resolve({
63
64
  success: true,
@@ -67,7 +68,9 @@ function executeWithCommand(startScreenCmd, command, args, verbose = false) {
67
68
  resolve({
68
69
  success: false,
69
70
  output: stdout,
70
- error: stderr || `Command exited with code ${code}`,
71
+ // Issue #2135: name the signal, so an out-of-memory abort is not
72
+ // reported as "code null".
73
+ error: stderr || describeChildExit({ command: 'Command', code, signal }),
71
74
  });
72
75
  }
73
76
  });