@link-assistant/hive-mind 2.11.5 → 2.11.7

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.
@@ -7,6 +7,7 @@
7
7
  import { reportError } from './sentry.lib.mjs';
8
8
 
9
9
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
10
+ import { QUIET_PROBE, quietProbe } from './quiet-probe.lib.mjs'; // issue #2130: keep read-only probe payloads out of the attached log
10
11
  // Issue #1827: tool-generated comments (markers + in-memory tracked IDs) must
11
12
  // not count as feedback in watch/continue mode, mirroring checkForNonBotComments.
12
13
  import { isToolGeneratedComment, isToolTrackedCommentId } from './tool-comments.lib.mjs';
@@ -22,7 +23,7 @@ export const detectAndCountFeedback = async params => {
22
23
 
23
24
  // Get current GitHub user to filter out own comments
24
25
  try {
25
- const userResult = await $`gh api user --jq .login`;
26
+ const userResult = await quietProbe($)`gh api user --jq .login`;
26
27
  if (userResult.code === 0) {
27
28
  currentUser = userResult.stdout.toString().trim();
28
29
  await log(formatAligned('👤', 'Current user:', currentUser, 2));
@@ -63,7 +64,11 @@ export const detectAndCountFeedback = async params => {
63
64
 
64
65
  // Get the last commit timestamp from the PR branch
65
66
  let lastCommitTime = null;
66
- const git$ = repositoryPath ? $({ cwd: repositoryPath }) : $;
67
+ // Issue #2130: quiet. These probes answer "when was the last commit?", and
68
+ // the answer is logged in words below; mirroring them put bare ISO dates
69
+ // and `git log`'s "unknown revision" complaint - the expected outcome for
70
+ // a branch that has never been pushed - into the attached log.
71
+ const git$ = repositoryPath ? $({ cwd: repositoryPath, ...QUIET_PROBE }) : quietProbe($);
67
72
  let lastCommitResult = await git$`git log -1 --format="%aI" origin/${branchName}`;
68
73
  if (lastCommitResult.code !== 0) {
69
74
  // Fallback to local branch if remote doesn't exist
@@ -76,7 +81,7 @@ export const detectAndCountFeedback = async params => {
76
81
  } else {
77
82
  // Fallback: Get last commit time from GitHub API
78
83
  try {
79
- const prCommitsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/commits --paginate --jq 'last.commit.author.date'`;
84
+ const prCommitsResult = await quietProbe($)`gh api repos/${owner}/${repo}/pulls/${prNumber}/commits --paginate --jq 'last.commit.author.date'`;
80
85
  if (prCommitsResult.code === 0 && prCommitsResult.stdout) {
81
86
  lastCommitTime = new Date(prCommitsResult.stdout.toString().trim());
82
87
  await log(formatAligned('📅', 'Last commit time (from API):', lastCommitTime.toISOString(), 2));
@@ -109,14 +114,14 @@ export const detectAndCountFeedback = async params => {
109
114
  let prConversationComments = [];
110
115
 
111
116
  // Get PR code review comments (use --paginate to get all comments, not just first page)
112
- const prReviewCommentsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
117
+ const prReviewCommentsResult = await quietProbe($)`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
113
118
  if (prReviewCommentsResult.code === 0) {
114
119
  prReviewComments = JSON.parse(prReviewCommentsResult.stdout.toString());
115
120
  }
116
121
 
117
122
  // Get PR conversation comments (PR is also an issue)
118
123
  // Use --paginate to get all comments - GitHub API returns max 30 per page by default
119
- const prConversationCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
124
+ const prConversationCommentsResult = await quietProbe($)`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
120
125
  if (prConversationCommentsResult.code === 0) {
121
126
  prConversationComments = JSON.parse(prConversationCommentsResult.stdout.toString());
122
127
  }
@@ -156,7 +161,7 @@ export const detectAndCountFeedback = async params => {
156
161
 
157
162
  // Count new issue comments after last commit
158
163
  // Use --paginate to get all comments - GitHub API returns max 30 per page by default
159
- const issueCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
164
+ const issueCommentsResult = await quietProbe($)`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
160
165
  if (issueCommentsResult.code === 0) {
161
166
  const issueComments = JSON.parse(issueCommentsResult.stdout.toString());
162
167
  const filteredIssueComments = issueComments.filter(comment => {
@@ -242,7 +247,7 @@ export const detectAndCountFeedback = async params => {
242
247
  // started) should be considered feedback.
243
248
  try {
244
249
  // Check PR description edit time
245
- const prDetailsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}`;
250
+ const prDetailsResult = await quietProbe($)`gh api repos/${owner}/${repo}/pulls/${prNumber}`;
246
251
  if (prDetailsResult.code === 0) {
247
252
  const prDetails = JSON.parse(prDetailsResult.stdout.toString());
248
253
  const prUpdatedAt = new Date(prDetails.updated_at);
@@ -265,7 +270,7 @@ export const detectAndCountFeedback = async params => {
265
270
 
266
271
  // Check issue description edit time if we have an issue
267
272
  if (issueNumber) {
268
- const issueDetailsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}`;
273
+ const issueDetailsResult = await quietProbe($)`gh api repos/${owner}/${repo}/issues/${issueNumber}`;
269
274
  if (issueDetailsResult.code === 0) {
270
275
  const issueDetails = JSON.parse(issueDetailsResult.stdout.toString());
271
276
  const issueUpdatedAt = new Date(issueDetails.updated_at);
@@ -300,12 +305,12 @@ export const detectAndCountFeedback = async params => {
300
305
 
301
306
  // 3. Check for new commits on default branch
302
307
  try {
303
- const defaultBranchResult = await $`gh api repos/${owner}/${repo}`;
308
+ const defaultBranchResult = await quietProbe($)`gh api repos/${owner}/${repo}`;
304
309
  if (defaultBranchResult.code === 0) {
305
310
  const repoData = JSON.parse(defaultBranchResult.stdout.toString());
306
311
  const defaultBranch = repoData.default_branch;
307
312
 
308
- const commitsResult = await $`gh api repos/${owner}/${repo}/commits --paginate --field sha=${defaultBranch} --field since=${lastCommitTime.toISOString()}`;
313
+ const commitsResult = await quietProbe($)`gh api repos/${owner}/${repo}/commits --paginate --field sha=${defaultBranch} --field since=${lastCommitTime.toISOString()}`;
309
314
  if (commitsResult.code === 0) {
310
315
  const commits = JSON.parse(commitsResult.stdout.toString());
311
316
  if (commits.length > 0) {
@@ -353,10 +358,10 @@ export const detectAndCountFeedback = async params => {
353
358
 
354
359
  // 6. Check for failed PR checks
355
360
  try {
356
- const prHeadResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.head.sha'`;
361
+ const prHeadResult = await quietProbe($)`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.head.sha'`;
357
362
  if (prHeadResult.code === 0) {
358
363
  const prHeadSha = prHeadResult.stdout.toString().trim();
359
- const checksResult = await $`gh api repos/${owner}/${repo}/commits/${prHeadSha}/check-runs --paginate --slurp`;
364
+ const checksResult = await quietProbe($)`gh api repos/${owner}/${repo}/commits/${prHeadSha}/check-runs --paginate --slurp`;
360
365
  const checkRuns = checksResult.code === 0 ? JSON.parse(checksResult.stdout.toString() || '[]').flatMap(page => page.check_runs || []) : [];
361
366
  const failedChecks = checkRuns.filter(check => check.conclusion === 'failure' && new Date(check.completed_at) > lastCommitTime);
362
367
 
@@ -379,7 +384,7 @@ export const detectAndCountFeedback = async params => {
379
384
 
380
385
  // 7. Check for review requests with changes requested
381
386
  try {
382
- const reviewsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/reviews --paginate`;
387
+ const reviewsResult = await quietProbe($)`gh api repos/${owner}/${repo}/pulls/${prNumber}/reviews --paginate`;
383
388
  if (reviewsResult.code === 0) {
384
389
  const reviews = JSON.parse(reviewsResult.stdout.toString());
385
390
  const changesRequestedReviews = reviews.filter(review => review.state === 'CHANGES_REQUESTED' && new Date(review.submitted_at) > lastCommitTime);
@@ -14,6 +14,7 @@ const use = globalThis.use;
14
14
 
15
15
  // Use command-stream for consistent $ behavior; wrap with rate-limit retry (#1726)
16
16
  const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
17
+ const { QUIET_PROBE } = await import('./quiet-probe.lib.mjs'); // issue #2130: keep read-only probe payloads out of the attached log
17
18
  const $ = wrapDollarWithGhRetry((await use('command-stream')).$);
18
19
 
19
20
  // Import shared library functions
@@ -179,7 +180,7 @@ export const setupUpstreamAndSync = async (tempDir, forkedRepo, upstreamRemote,
179
180
  // branch. Attempting the push there is guaranteed to be rejected
180
181
  // with "permission denied" and is unnecessary, so we skip it and
181
182
  // keep working on the PR branch.
182
- const currentUserResult = await $`gh api user --jq .login`;
183
+ const currentUserResult = await $(QUIET_PROBE)`gh api user --jq .login`;
183
184
  const currentUser = currentUserResult.code === 0 ? currentUserResult.stdout.toString().trim() : null;
184
185
  const pushDecision = shouldPushDefaultBranchToFork({ currentUser, forkedRepo });
185
186
 
package/src/solve.mjs CHANGED
@@ -1238,8 +1238,6 @@ try {
1238
1238
  prNumber,
1239
1239
  issueNumber,
1240
1240
  success,
1241
- publicPricingEstimate,
1242
- anthropicTotalCostUSD,
1243
1241
  pricingInfo,
1244
1242
  sessionUsage: { sessionId, tempDir, resultModelUsage, streamTokenUsage, subAgentCalls },
1245
1243
  });
@@ -6,6 +6,7 @@
6
6
  // Issue #1625: centralized markers + tracked posting for the "empty repo"
7
7
  // issue comment so it's excluded from --auto-attach-solution-summary's check.
8
8
  import { REPOSITORY_INITIALIZATION_REQUIRED_MARKER, postTrackedComment } from './tool-comments.lib.mjs';
9
+ import { QUIET_PROBE } from './quiet-probe.lib.mjs'; // issue #2130: keep read-only probe payloads out of the attached log
9
10
 
10
11
  export async function setupRepositoryAndClone({ argv, owner, repo, forkOwner, forkRepoName, tempDir, isContinueMode, issueUrl, log, $, needsClone = true }) {
11
12
  // Set up repository and handle forking
@@ -28,14 +29,49 @@ export async function setupRepositoryAndClone({ argv, owner, repo, forkOwner, fo
28
29
  const prForkRemote = await setupPrForkRemote(tempDir, argv, prForkOwner, repo, isContinueMode, owner);
29
30
 
30
31
  // Set up git authentication using gh
31
- const authSetupResult = await $({ cwd: tempDir })`gh auth setup-git 2>&1`;
32
- if (authSetupResult.code !== 0) {
33
- await log('Note: gh auth setup-git had issues, continuing anyway\n');
34
- }
32
+ await setupGitCredentialHelper({ tempDir, log, $ });
35
33
 
36
34
  return { repoToClone, forkedRepo, upstreamRemote, prForkRemote, prForkOwner };
37
35
  }
38
36
 
37
+ /**
38
+ * Point git at `gh` for GitHub credentials.
39
+ *
40
+ * Issue #2130: `gh auth setup-git` only ever writes the *global* gitconfig. In
41
+ * a container where `~/.gitconfig` is bind-mounted, git cannot replace it and
42
+ * gh reports `failed to set up git credential helper: failed to run git: error:
43
+ * could not write config file /home/box/.gitconfig: Device or resource busy`.
44
+ * Hive Mind used to mirror that line raw and continue with no credential helper
45
+ * at all, so the real failure only surfaced later as a push error.
46
+ *
47
+ * The clone-local config is equivalent for our purposes and is never
48
+ * bind-mounted, so it is used as the fallback. The empty `helper` entry first
49
+ * clears any inherited helper, exactly as `gh auth setup-git` does.
50
+ */
51
+ export async function setupGitCredentialHelper({ tempDir, log, $, hosts = ['github.com'] }) {
52
+ const authSetupResult = await $({ cwd: tempDir, ...QUIET_PROBE })`gh auth setup-git 2>&1`;
53
+ if (authSetupResult.code === 0) return { scope: 'global', hosts };
54
+
55
+ const reason = (authSetupResult.stdout?.toString() || authSetupResult.stderr?.toString() || '').trim();
56
+ await log(`ℹ️ gh auth setup-git could not write the global gitconfig${reason ? `: ${reason.split('\n')[0]}` : ''}`, { verbose: true });
57
+
58
+ const failures = [];
59
+ for (const host of hosts) {
60
+ const key = `credential.https://${host}.helper`;
61
+ const cleared = await $({ cwd: tempDir, ...QUIET_PROBE })`git config --local --replace-all ${key} ""`;
62
+ const configured = await $({ cwd: tempDir, ...QUIET_PROBE })`git config --local --add ${key} ${'!gh auth git-credential'}`;
63
+ if (cleared.code !== 0 || configured.code !== 0) failures.push(host);
64
+ }
65
+
66
+ if (failures.length) {
67
+ await log(`⚠️ Could not configure a git credential helper for ${failures.join(', ')} - pushes may require credentials in the remote URL`, { level: 'warning' });
68
+ return { scope: 'none', hosts: failures };
69
+ }
70
+
71
+ await log('🔑 Configured the gh credential helper for this clone (the global gitconfig is not writable)', { verbose: true });
72
+ return { scope: 'local', hosts };
73
+ }
74
+
39
75
  async function setupRepository(argv, owner, repo, forkOwner, issueUrl, forkRepoName) {
40
76
  const repository = await import('./solve.repository.lib.mjs');
41
77
  const { setupRepository: setupRepoFn } = repository;
@@ -14,6 +14,7 @@ const use = globalThis.use;
14
14
 
15
15
  // Use command-stream for consistent $ behavior; wrap with rate-limit retry (#1726)
16
16
  const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
17
+ const { QUIET_PROBE } = await import('./quiet-probe.lib.mjs'); // issue #2130: keep read-only probe payloads out of the attached log
17
18
  const $ = wrapDollarWithGhRetry((await use('command-stream')).$);
18
19
  const os = (await use('os')).default;
19
20
  const path = (await use('path')).default;
@@ -57,7 +58,7 @@ export const getRootRepository = async (owner, repo) => {
57
58
  // Check if current user has a fork of the given root repository
58
59
  export const checkExistingForkOfRoot = async rootRepo => {
59
60
  try {
60
- const userResult = await lib.ghCmdRetry(() => $`gh api user --jq .login`, { label: 'get user (fork check)' });
61
+ const userResult = await lib.ghCmdRetry(() => $(QUIET_PROBE)`gh api user --jq .login`, { label: 'get user (fork check)' });
61
62
  if (userResult.code !== 0) return null;
62
63
  const currentUser = userResult.stdout.toString().trim();
63
64
  // Issue #2119: build the jq expression in JS. Its double quotes belong to jq,
@@ -378,7 +379,7 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
378
379
  await log(`${formatAligned('', 'Checking fork status...', '')}\n`);
379
380
 
380
381
  // Get current user (issue #1536: retry on transient network errors)
381
- const userResult = await lib.ghCmdRetry(() => $`gh api user --jq .login`, { label: 'get current user' });
382
+ const userResult = await lib.ghCmdRetry(() => $(QUIET_PROBE)`gh api user --jq .login`, { label: 'get current user' });
382
383
  if (userResult.code !== 0) {
383
384
  await log(`${formatAligned('❌', 'Error:', 'Failed to get current user')}`);
384
385
  await safeExit(1, 'Repository setup failed');
@@ -1191,7 +1192,7 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont
1191
1192
 
1192
1193
  // Get current user to check if it's someone else's fork
1193
1194
  await log(`\n${formatAligned('🔍', 'Checking PR fork:', 'Determining if branch is in another fork...')}`);
1194
- const userResult = await $`gh api user --jq .login`;
1195
+ const userResult = await $(QUIET_PROBE)`gh api user --jq .login`;
1195
1196
  if (userResult.code !== 0) {
1196
1197
  await log(`${formatAligned('⚠️', 'Warning:', 'Failed to get current user, cannot set up pr-fork remote')}`);
1197
1198
  return null;
@@ -15,6 +15,7 @@ const use = globalThis.use;
15
15
  // Use command-stream for consistent $ behavior across runtimes
16
16
  const { $: __rawDollar$ } = await use('command-stream');
17
17
  const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
18
+ const { QUIET_PROBE } = await import('./quiet-probe.lib.mjs'); // issue #2130: keep read-only probe payloads out of the attached log
18
19
  const $ = wrapDollarWithGhRetry(__rawDollar$);
19
20
  const path = (await use('path')).default;
20
21
 
@@ -28,8 +29,6 @@ import { safeExit } from './exit-handler.lib.mjs';
28
29
  // Import GitHub-related functions
29
30
  const githubLib = await import('./github.lib.mjs');
30
31
  const { sanitizeLogContent, attachLogToGitHub } = githubLib;
31
- const { buildCostInfoString } = await import('./github-cost-info.lib.mjs');
32
- const { buildBudgetStatsString } = await import('./claude.budget-stats.lib.mjs');
33
32
 
34
33
  // Issue #1745: process-wide sanitization counters used to print a one-line
35
34
  // "we masked N secrets" summary at the end of each run.
@@ -673,7 +672,18 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
673
672
  // same observed facts (working-session summary and attached log alike).
674
673
  export const buildSessionBudgetStatsData = async ({ argv, sessionId = null, tempDir = null, resultModelUsage = null, streamTokenUsage = null, subAgentCalls = null, pricingInfo = null }) => {
675
674
  let budgetStatsData = null;
676
- if (argv.tokensBudgetStats && sessionId && tempDir) {
675
+ // Issue #2132: budget stats are a property of the working session **log**.
676
+ // With `--attach-logs` disabled there is no log comment, so they must not be
677
+ // computed or published anywhere.
678
+ const { shouldPublishBudgetStats, isAttachLogsEnabled, isTokensBudgetStatsEnabled } = await import('./budget-stats-policy.lib.mjs');
679
+ if (!shouldPublishBudgetStats(argv)) {
680
+ if (argv?.verbose) {
681
+ const reason = !isTokensBudgetStatsEnabled(argv) ? '--no-tokens-budget-stats' : !isAttachLogsEnabled(argv) ? '--attach-logs is disabled' : 'unknown';
682
+ await log(` ℹ️ Skipping context/cost budget stats publication (${reason})`, { verbose: true });
683
+ }
684
+ return null;
685
+ }
686
+ if (sessionId && tempDir) {
677
687
  try {
678
688
  const { calculateSessionTokens } = await import('./claude.lib.mjs');
679
689
  const tokenUsage = await calculateSessionTokens(sessionId, tempDir, resultModelUsage);
@@ -685,7 +695,7 @@ export const buildSessionBudgetStatsData = async ({ argv, sessionId = null, temp
685
695
  }
686
696
  }
687
697
  // Issue #1526: Build budget stats from Agent CLI token/context data when no JSONL session available
688
- if (!budgetStatsData && argv.tokensBudgetStats && pricingInfo?.tokenUsage) {
698
+ if (!budgetStatsData && pricingInfo?.tokenUsage) {
689
699
  try {
690
700
  const { buildAgentBudgetStats } = await import('./claude.budget-stats.lib.mjs');
691
701
  const agentBudgetData = buildAgentBudgetStats(pricingInfo.tokenUsage, pricingInfo);
@@ -719,7 +729,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
719
729
 
720
730
  try {
721
731
  // Get the current user's GitHub username
722
- const userResult = await $`gh api user --jq .login`;
732
+ const userResult = await $(QUIET_PROBE)`gh api user --jq .login`;
723
733
 
724
734
  if (userResult.code !== 0) {
725
735
  throw new Error(`Failed to get current user: ${userResult.stderr ? userResult.stderr.toString() : 'Unknown error'}`);
@@ -1139,7 +1149,7 @@ export const { TOOL_GENERATED_COMMENT_MARKERS, isToolGeneratedComment, trackTool
1139
1149
  export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, prNumber, issueNumber) => {
1140
1150
  try {
1141
1151
  // Get the current user's GitHub username
1142
- const userResult = await $`gh api user --jq .login`;
1152
+ const userResult = await $(QUIET_PROBE)`gh api user --jq .login`;
1143
1153
  if (userResult.code !== 0) {
1144
1154
  return false; // Cannot determine, default to not attaching
1145
1155
  }
@@ -1263,15 +1273,19 @@ export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, p
1263
1273
  * @param {string} options.repo - Repository name
1264
1274
  * @returns {Promise<boolean>} - True if comment was posted successfully
1265
1275
  */
1266
- export const buildWorkingSessionSummaryDetails = ({ publicPricingEstimate = null, anthropicTotalCostUSD = null, pricingInfo = null, budgetStatsData = null } = {}) => {
1267
- const costInfo = buildCostInfoString(publicPricingEstimate, anthropicTotalCostUSD, pricingInfo, {
1268
- includeTokenUsage: false,
1269
- });
1270
- const budgetStats = budgetStatsData ? buildBudgetStatsString(budgetStatsData.tokenUsage, budgetStatsData.subAgentCalls) : '';
1271
- return `${costInfo}${budgetStats}`.trim();
1272
- };
1276
+ /**
1277
+ * Issue #2132: the working session summary must describe *what the AI did* and
1278
+ * nothing else. Cost estimation and context/token budget statistics belong to
1279
+ * the working session log comment (`--attach-logs`), where they are already
1280
+ * published once per working session. Rendering them in the summary as well
1281
+ * duplicated the very same block in two consecutive comments.
1282
+ *
1283
+ * Kept as an exported function returning an empty string so the invariant is
1284
+ * directly testable and any future caller cannot silently re-add the block.
1285
+ */
1286
+ export const buildWorkingSessionSummaryDetails = () => '';
1273
1287
 
1274
- export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumber, owner, repo, publicPricingEstimate = null, anthropicTotalCostUSD = null, pricingInfo = null, budgetStatsData = null, changeStats = null }) => {
1288
+ export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumber, owner, repo, changeStats = null }) => {
1275
1289
  if (!resultSummary || typeof resultSummary !== 'string') {
1276
1290
  await log('⚠️ No working session summary available to attach', { verbose: true });
1277
1291
  return false;
@@ -1286,12 +1300,6 @@ export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumb
1286
1300
  }
1287
1301
 
1288
1302
  try {
1289
- const usageDetails = buildWorkingSessionSummaryDetails({
1290
- publicPricingEstimate,
1291
- anthropicTotalCostUSD,
1292
- pricingInfo,
1293
- budgetStatsData,
1294
- });
1295
1303
  // Issue #2119: publish what the session actually produced. The reported
1296
1304
  // summary said "The `pwd` command completed" and printed the solver's own
1297
1305
  // /tmp workspace, on a pull request that was still empty.
@@ -1301,7 +1309,7 @@ export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumb
1301
1309
  const comment = `${toolComments.WORKING_SESSION_SUMMARY_AUTOMATION_MARKER}
1302
1310
  ## ${toolComments.WORKING_SESSION_SUMMARY_MARKER}
1303
1311
 
1304
- ${summaryBody}${noChangesNotice ? `\n\n${noChangesNotice}` : ''}${usageDetails ? `\n\n${usageDetails}` : ''}
1312
+ ${summaryBody}${noChangesNotice ? `\n\n${noChangesNotice}` : ''}
1305
1313
 
1306
1314
  ---
1307
1315
  *${toolComments.WORKING_SESSION_SUMMARY_AUTOMATED_FOOTER}*`;
@@ -1358,7 +1366,7 @@ ${summaryBody}${noChangesNotice ? `\n\n${noChangesNotice}` : ''}${usageDetails ?
1358
1366
  * @param {boolean} [options.success=true] - skip attachment for failed iterations
1359
1367
  * @returns {Promise<{attached: boolean, reason: string}>}
1360
1368
  */
1361
- export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, workStartTime, owner, repo, prNumber, issueNumber, success = true, publicPricingEstimate = null, anthropicTotalCostUSD = null, pricingInfo = null, budgetStatsData = null, sessionUsage = null }) => {
1369
+ export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, workStartTime, owner, repo, prNumber, issueNumber, success = true, pricingInfo = null, budgetStatsData = null, sessionUsage = null }) => {
1362
1370
  if (!success) {
1363
1371
  return { attached: false, reason: 'iteration_failed' };
1364
1372
  }
@@ -1407,16 +1415,14 @@ export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, wo
1407
1415
  // say so, instead of reading as a report of completed work.
1408
1416
  const changeStats = prNumber ? await getPullRequestChangeStats({ owner, repo, prNumber, $ }) : null;
1409
1417
 
1418
+ // Issue #2132: the summary carries no cost/budget block. `resolvedBudgetStatsData`
1419
+ // is computed only so the caller can reuse it for this session's log comment.
1410
1420
  const ok = await attachSolutionSummary({
1411
1421
  resultSummary,
1412
1422
  prNumber,
1413
1423
  issueNumber,
1414
1424
  owner,
1415
1425
  repo,
1416
- publicPricingEstimate,
1417
- anthropicTotalCostUSD,
1418
- pricingInfo,
1419
- budgetStatsData: resolvedBudgetStatsData,
1420
1426
  changeStats,
1421
1427
  });
1422
1428
  return { attached: !!ok, reason: ok ? 'attached' : 'post_failed', budgetStatsData: resolvedBudgetStatsData };
@@ -308,13 +308,20 @@ export const performSystemChecks = async (minDiskSpace = 10240, skipToolConnecti
308
308
  const { validateFormalAiToolConnection } = await import('./formal-ai.lib.mjs');
309
309
  const formalAiValidation = await validateFormalAiToolConnection(argv.tool || 'claude');
310
310
  isToolConnected = formalAiValidation.valid;
311
+ // Record the wrapper version on both paths: the wrapper builds the tool's
312
+ // argv, so its version is the first thing needed to explain a failure
313
+ // (issue #2130's logs recorded neither, which left the round-2 claude
314
+ // failure unattributable).
315
+ const formalAiVersionLine = `📦 Formal AI wrapper version: ${formalAiValidation.formalAiVersion || 'unknown'}`;
311
316
  if (isToolConnected) {
312
317
  await log(`✅ Formal AI wrapper and ${argv.tool || 'claude'} CLI are available`);
318
+ await log(formalAiVersionLine);
313
319
  if (formalAiValidation.version) {
314
320
  await log(`📦 ${argv.tool || 'claude'} CLI version: ${formalAiValidation.version}`);
315
321
  }
316
322
  } else {
317
323
  await log(`❌ Formal AI dispatch validation failed: ${formalAiValidation.error}`, { level: 'error' });
324
+ await log(` ${formalAiVersionLine}`, { level: 'error' });
318
325
  await log(' Install or update the wrapper with: cargo install formal-ai', { level: 'error' });
319
326
  return false;
320
327
  }
@@ -567,19 +567,21 @@ export const watchForFeedback = async params => {
567
567
  latestResultModelUsage = toolResult.resultModelUsage;
568
568
  }
569
569
 
570
- // Issue #1508: Compute budget stats for auto-restart log comment
571
- let autoRestartBudgetStatsData = null;
572
- if (argv.tokensBudgetStats && latestSessionId && tempDir) {
573
- try {
574
- const { calculateSessionTokens } = await import('./claude.lib.mjs');
575
- const tokenUsage = await calculateSessionTokens(latestSessionId, tempDir, toolResult.resultModelUsage);
576
- if (tokenUsage) {
577
- autoRestartBudgetStatsData = { tokenUsage, streamTokenUsage: toolResult.streamTokenUsage || null, subAgentCalls: toolResult.subAgentCalls || null };
578
- }
579
- } catch (budgetError) {
580
- if (argv.verbose) await log(` ⚠️ Could not calculate budget stats: ${budgetError.message}`, { verbose: true });
581
- }
582
- }
570
+ // Issue #1508: Compute budget stats for auto-restart log comment.
571
+ // Issue #2132: shared with the top-level run and the
572
+ // auto-restart-until-mergeable loop via buildSessionBudgetStatsData,
573
+ // so every working session derives its own stats the same way (and
574
+ // skips them entirely when `--attach-logs` is disabled).
575
+ const { buildSessionBudgetStatsData } = await import('./solve.results.lib.mjs');
576
+ const autoRestartBudgetStatsData = await buildSessionBudgetStatsData({
577
+ argv,
578
+ sessionId: latestSessionId,
579
+ tempDir,
580
+ resultModelUsage: toolResult.resultModelUsage,
581
+ streamTokenUsage: toolResult.streamTokenUsage || null,
582
+ subAgentCalls: toolResult.subAgentCalls || null,
583
+ pricingInfo: toolResult.pricingInfo || null,
584
+ });
583
585
 
584
586
  // Issue #1761: Post the working session **summary** BEFORE uploading
585
587
  // the working session **log** so the summary always appears above
@@ -604,8 +606,6 @@ export const watchForFeedback = async params => {
604
606
  prNumber,
605
607
  issueNumber,
606
608
  success: true,
607
- publicPricingEstimate: toolResult.publicPricingEstimate,
608
- anthropicTotalCostUSD: latestAnthropicCost,
609
609
  pricingInfo: toolResult.pricingInfo,
610
610
  budgetStatsData: autoRestartBudgetStatsData,
611
611
  });
@@ -24,6 +24,7 @@ import { reportError } from './sentry.lib.mjs';
24
24
  export { createCredentialStreamSanitizer };
25
25
 
26
26
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
27
+ import { QUIET_PROBE } from './quiet-probe.lib.mjs'; // issue #2130: never mirror the call that discovers which secrets must be masked
27
28
  // Dynamic imports for runtime dependencies
28
29
  const getOsModule = async () => (await import('os')).default;
29
30
  const getPathModule = async () => (await import('path')).default;
@@ -255,8 +256,12 @@ export const getGitHubTokensFromCommand = async () => {
255
256
  const tokens = [];
256
257
 
257
258
  try {
258
- // Run gh auth status to get token info
259
- const authResult = await $`gh auth status 2>&1`.catch(() => ({ stdout: '', stderr: '' }));
259
+ // Run gh auth status to get token info.
260
+ // Issue #2130: never mirror this. The whole point of the call is to learn
261
+ // which secrets have to be masked, so its output is the one thing that is
262
+ // guaranteed to be unmasked at this moment - and it was being echoed into
263
+ // the log that later gets attached to the pull request.
264
+ const authResult = await $(QUIET_PROBE)`gh auth status 2>&1`.catch(() => ({ stdout: '', stderr: '' }));
260
265
  const authOutput = authResult.stdout?.toString() + authResult.stderr?.toString() || '';
261
266
 
262
267
  // Look for token patterns in the output