@link-assistant/hive-mind 2.10.1 → 2.10.3

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.hi.md +2 -0
  3. package/README.md +2 -0
  4. package/README.ru.md +2 -0
  5. package/README.zh.md +2 -0
  6. package/package.json +1 -1
  7. package/src/claude.lib.mjs +0 -4
  8. package/src/cleanup.mjs +18 -6
  9. package/src/codex.lib.mjs +0 -4
  10. package/src/configure-claude.mjs +3 -0
  11. package/src/credential-sanitization-core.lib.mjs +231 -0
  12. package/src/development-log.lib.mjs +39 -6
  13. package/src/fix.mjs +3 -0
  14. package/src/github-error-reporter.lib.mjs +13 -8
  15. package/src/github-issue-auto-close.lib.mjs +2 -1
  16. package/src/github-merge-issue-close.lib.mjs +2 -1
  17. package/src/github.lib.mjs +29 -18
  18. package/src/hive-screens.mjs +3 -0
  19. package/src/instrument.mjs +14 -0
  20. package/src/interactive-mode.lib.mjs +25 -40
  21. package/src/lib.mjs +89 -50
  22. package/src/log-upload.lib.mjs +22 -4
  23. package/src/post-finish-sanitization-sweep.lib.mjs +5 -5
  24. package/src/review.mjs +3 -1
  25. package/src/sentry.lib.mjs +27 -8
  26. package/src/solve.auto-merge.lib.mjs +4 -0
  27. package/src/solve.auto-pr.lib.mjs +11 -21
  28. package/src/solve.error-handlers.lib.mjs +2 -1
  29. package/src/solve.mjs +7 -8
  30. package/src/solve.progress-monitoring.lib.mjs +20 -9
  31. package/src/solve.results.lib.mjs +72 -25
  32. package/src/solve.watch.lib.mjs +4 -0
  33. package/src/start-screen.mjs +3 -0
  34. package/src/task.issue-creation.lib.mjs +5 -3
  35. package/src/task.mjs +21 -8
  36. package/src/telegram-bot.mjs +4 -1
  37. package/src/telegram-log-command.lib.mjs +38 -4
  38. package/src/telegram-safe-reply.lib.mjs +7 -4
  39. package/src/telegram-tokens-command.lib.mjs +1 -1
  40. package/src/token-sanitization.lib.mjs +177 -14
  41. package/src/tool-comments.lib.mjs +5 -5
  42. package/src/youtrack/youtrack-sync.mjs +4 -3
@@ -1,5 +1,24 @@
1
1
  // Sentry integration library for hive-mind
2
2
  import { isSentryEnabled, captureException, captureMessage, startTransaction } from './instrument.mjs';
3
+ import { sanitizeCredentialText } from './credential-sanitization-core.lib.mjs';
4
+
5
+ const sanitizeError = error => {
6
+ const source = error instanceof Error ? error : new Error(String(error));
7
+ const sanitized = new Error(sanitizeCredentialText(source.message));
8
+ sanitized.name = source.name;
9
+ if (source.stack) sanitized.stack = sanitizeCredentialText(source.stack);
10
+ return sanitized;
11
+ };
12
+
13
+ const sanitizeContext = (value, seen = new WeakSet()) => {
14
+ if (typeof value === 'string') return sanitizeCredentialText(value);
15
+ if (value instanceof Error) return sanitizeError(value);
16
+ if (!value || typeof value !== 'object') return value;
17
+ if (seen.has(value)) return '[Circular]';
18
+ seen.add(value);
19
+ if (Array.isArray(value)) return value.map(item => sanitizeContext(item, seen));
20
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeContext(item, seen)]));
21
+ };
3
22
 
4
23
  // Lazy import of Sentry to handle cases where it's not installed
5
24
  let Sentry = null;
@@ -84,7 +103,7 @@ export const withSentry = (fn, name, op = 'task') => {
84
103
  return result;
85
104
  } catch (error) {
86
105
  transaction.setStatus('internal_error');
87
- captureException(error, {
106
+ captureException(sanitizeError(error), {
88
107
  operation: name,
89
108
  args: args.length > 0 ? `${args.length} arguments` : 'no arguments',
90
109
  });
@@ -139,7 +158,7 @@ export const logToSentry = (message, level = 'info', context = {}) => {
139
158
  return;
140
159
  }
141
160
 
142
- captureMessage(message, level, context);
161
+ captureMessage(sanitizeCredentialText(message), level, sanitizeContext(context));
143
162
  };
144
163
 
145
164
  /**
@@ -153,7 +172,7 @@ export const reportError = (error, context = {}) => {
153
172
  return;
154
173
  }
155
174
 
156
- captureException(error, { ...context, level: 'error' });
175
+ captureException(sanitizeError(error), { ...sanitizeContext(context), level: 'error' });
157
176
  };
158
177
 
159
178
  /**
@@ -169,7 +188,7 @@ export const reportWarning = (warning, context = {}) => {
169
188
 
170
189
  // Convert string warnings to Error objects for better stack traces
171
190
  const warningError = typeof warning === 'string' ? new Error(warning) : warning;
172
- captureException(warningError, { ...context, level: 'warning' });
191
+ captureException(sanitizeError(warningError), { ...sanitizeContext(context), level: 'warning' });
173
192
  };
174
193
 
175
194
  /**
@@ -183,7 +202,7 @@ export const addBreadcrumb = async breadcrumb => {
183
202
 
184
203
  const sentry = await getSentry();
185
204
  if (sentry) {
186
- sentry.addBreadcrumb(breadcrumb);
205
+ sentry.addBreadcrumb(sanitizeContext(breadcrumb));
187
206
  }
188
207
  };
189
208
 
@@ -198,7 +217,7 @@ export const setUserContext = async user => {
198
217
 
199
218
  const sentry = await getSentry();
200
219
  if (sentry) {
201
- sentry.setUser(user);
220
+ sentry.setUser(sanitizeContext(user));
202
221
  }
203
222
  };
204
223
 
@@ -214,7 +233,7 @@ export const setExtraContext = async (key, value) => {
214
233
 
215
234
  const sentry = await getSentry();
216
235
  if (sentry) {
217
- sentry.setExtra(key, value);
236
+ sentry.setExtra(key, sanitizeContext(value));
218
237
  }
219
238
  };
220
239
 
@@ -229,7 +248,7 @@ export const setTags = async tags => {
229
248
 
230
249
  const sentry = await getSentry();
231
250
  if (sentry) {
232
- sentry.setTags(tags);
251
+ sentry.setTags(sanitizeContext(tags));
233
252
  }
234
253
  };
235
254
 
@@ -1068,6 +1068,10 @@ No further AI sessions will be started automatically for this run. Please review
1068
1068
  prNumber,
1069
1069
  issueNumber,
1070
1070
  success: true,
1071
+ publicPricingEstimate: toolResult.publicPricingEstimate,
1072
+ anthropicTotalCostUSD: latestAnthropicCost,
1073
+ pricingInfo: toolResult.pricingInfo,
1074
+ budgetStatsData: autoMergeBudgetStatsData,
1071
1075
  });
1072
1076
  } catch (summaryError) {
1073
1077
  reportError(summaryError, {
@@ -11,6 +11,7 @@ import { handleCompareApiNotReady } from './solve.auto-pr-compare-readiness.lib.
11
11
 
12
12
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry, isTransientCompareApiError } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. Issue #1756: execGhWithRetry retries on transient 5xx (504) too. Issue #1829: isTransientCompareApiError lets the compare-API readiness gate degrade gracefully on transient diff-render failures.
13
13
  import { stagePlaceholderFileOrExplain, explainNothingStagedAndThrow } from './solve.auto-pr-placeholder.lib.mjs'; // Issue #1825: handles the seed placeholder when the target repo gitignores it.
14
+ import { sanitizeForPublication, writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
14
15
 
15
16
  export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNumber, owner, repo, defaultBranch, forkedRepo, isContinueMode, prNumber, log, formatAligned, $, reportError, path, fs }) {
16
17
  // Skip auto-PR creation if:
@@ -906,17 +907,19 @@ ${prBody}`,
906
907
  // single transient 5xx (e.g. `HTTP 504: 504 Gateway Timeout
907
908
  // (https://api.github.com/graphql)`) or rate-limit response retries
908
909
  // instead of aborting the whole solve session.
910
+ let prBodyFile = null;
911
+ let prTitleFile = null;
909
912
  try {
910
913
  // Write PR body to temp file to avoid shell escaping issues
911
- const prBodyFile = `/tmp/pr-body-${Date.now()}.md`;
912
- await fs.writeFile(prBodyFile, prBody);
914
+ prBodyFile = `/tmp/pr-body-${Date.now()}.md`;
915
+ await writeSanitizedPublicationFile(prBodyFile, prBody);
913
916
 
914
917
  // Write PR title to temp file to avoid shell escaping issues with quotes/apostrophes
915
918
  // This solves the issue where titles containing apostrophes (e.g., "don't") would cause
916
919
  // "Unterminated quoted string" errors
917
- const prTitle = `[WIP] ${issueTitle}`;
918
- const prTitleFile = `/tmp/pr-title-${Date.now()}.txt`;
919
- await fs.writeFile(prTitleFile, prTitle);
920
+ const prTitle = await sanitizeForPublication(`[WIP] ${issueTitle}`);
921
+ prTitleFile = `/tmp/pr-title-${Date.now()}.txt`;
922
+ await writeSanitizedPublicationFile(prTitleFile, prTitle);
920
923
 
921
924
  // Build command with optional assignee and handle forks
922
925
  // Note: targetBranch is already defined above
@@ -994,22 +997,6 @@ ${prBody}`,
994
997
  }
995
998
  }
996
999
 
997
- // Clean up temp files
998
- await fs.unlink(prBodyFile).catch(unlinkError => {
999
- reportError(unlinkError, {
1000
- context: 'pr_body_file_cleanup',
1001
- prBodyFile,
1002
- operation: 'delete_temp_file',
1003
- });
1004
- });
1005
- await fs.unlink(prTitleFile).catch(unlinkError => {
1006
- reportError(unlinkError, {
1007
- context: 'pr_title_file_cleanup',
1008
- prTitleFile,
1009
- operation: 'delete_temp_file',
1010
- });
1011
- });
1012
-
1013
1000
  // Log gh pr create output for debugging (Issue #1462)
1014
1001
  if (argv.verbose) {
1015
1002
  await log(` gh pr create stdout: ${(output || '').trim() || '(empty)'}`, { verbose: true });
@@ -1279,6 +1266,9 @@ ${prBody}`,
1279
1266
  } else {
1280
1267
  throw new Error(`PR creation failed: ${cleanError}`, { cause: prCreateError });
1281
1268
  }
1269
+ } finally {
1270
+ if (prBodyFile) await fs.unlink(prBodyFile).catch(() => {});
1271
+ if (prTitleFile) await fs.unlink(prTitleFile).catch(() => {});
1282
1272
  }
1283
1273
  }
1284
1274
  }
@@ -11,6 +11,7 @@ import { reportError } from './sentry.lib.mjs';
11
11
 
12
12
  // Import GitHub error reporter
13
13
  import { handleErrorWithIssueCreation } from './github-error-reporter.lib.mjs';
14
+ import { sanitizeForPublication } from './token-sanitization.lib.mjs';
14
15
 
15
16
  export const isErrorIssueAutoCreationDisabled = argv => !!(argv?.disableReportIssue || argv?.disableIssueAutoCreationOnError);
16
17
 
@@ -115,7 +116,7 @@ export const handleFailure = async options => {
115
116
  if (argv.autoClosePullRequestOnFail && global.createdPR && global.createdPR.number) {
116
117
  await log('\n🔒 Auto-closing pull request due to failure...');
117
118
  try {
118
- const closeMessage = errorType === 'uncaughtException' ? 'Auto-closed due to uncaught exception. Logs have been attached for debugging.' : errorType === 'unhandledRejection' ? 'Auto-closed due to unhandled rejection. Logs have been attached for debugging.' : 'Auto-closed due to execution failure. Logs have been attached for debugging.';
119
+ const closeMessage = await sanitizeForPublication(errorType === 'uncaughtException' ? 'Auto-closed due to uncaught exception. Logs have been attached for debugging.' : errorType === 'unhandledRejection' ? 'Auto-closed due to unhandled rejection. Logs have been attached for debugging.' : 'Auto-closed due to execution failure. Logs have been attached for debugging.');
119
120
 
120
121
  const result = await $`gh pr close ${global.createdPR.number} --repo ${global.owner || owner}/${global.repo || repo} --comment ${closeMessage}`;
121
122
  if (result.exitCode === 0) {
package/src/solve.mjs CHANGED
@@ -1228,13 +1228,8 @@ try {
1228
1228
 
1229
1229
  // Issue #2048: commit+push dev log BEFORE any PR readiness signal so its CI gates readiness (was last, breaking CI post-signal; PR #2046, docs/case-studies/issue-2048). Idempotent: trailing call is a no-op. prettier-ignore
1230
1230
  await finalizeDevelopmentLog();
1231
- // Issue #1263 / #1728: Working session summary attachment.
1232
- // Routed through the shared maybeAttachWorkingSessionSummary helper so that
1233
- // top-level solve, auto-restart-until-mergeable, and watch-mode iterations
1234
- // all use identical attach logic. The helper internally honours
1235
- // --attach-solution-summary (always attach) and --auto-attach-solution-summary
1236
- // (attach only if no AI comment was posted during the session).
1237
- await maybeAttachWorkingSessionSummary({
1231
+ // Issue #1263 / #1728 / #2115: shared summary attachment and usage display.
1232
+ const { budgetStatsData: workingSessionBudgetStatsData = null } = await maybeAttachWorkingSessionSummary({
1238
1233
  argv,
1239
1234
  resultSummary,
1240
1235
  workStartTime,
@@ -1243,10 +1238,14 @@ try {
1243
1238
  prNumber,
1244
1239
  issueNumber,
1245
1240
  success,
1241
+ publicPricingEstimate,
1242
+ anthropicTotalCostUSD,
1243
+ pricingInfo,
1244
+ sessionUsage: { sessionId, tempDir, resultModelUsage, streamTokenUsage, subAgentCalls },
1246
1245
  });
1247
1246
 
1248
1247
  // Search for newly created pull requests and comments
1249
- const verifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution, sessionType, resultModelUsage, streamTokenUsage, subAgentCalls);
1248
+ const verifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution, sessionType, resultModelUsage, streamTokenUsage, subAgentCalls, workingSessionBudgetStatsData);
1250
1249
  const logsAlreadyUploaded = verifyResult?.logUploadSuccess || false;
1251
1250
 
1252
1251
  // Issue #1162: Auto-restart when PR title/description still has placeholder content
@@ -27,6 +27,7 @@
27
27
  // Issue #1625: centralized markers + tracking helpers so the live-progress
28
28
  // comment is excluded from --auto-attach-solution-summary's AI-comment check.
29
29
  import { LIVE_PROGRESS_SECTION_START_MARKER, LIVE_PROGRESS_SECTION_END_MARKER, postTrackedCommentFromFile, trackToolCommentId } from './tool-comments.lib.mjs';
30
+ import { writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
30
31
 
31
32
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
32
33
  /**
@@ -225,9 +226,12 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
225
226
  // Edit existing comment
226
227
  const fs = (await import('fs')).promises;
227
228
  const tempFile = `/tmp/pr-progress-comment-${prNumber}-${Date.now()}.md`;
228
- await fs.writeFile(tempFile, progressSection);
229
- await $`gh api repos/${owner}/${repo}/issues/comments/${state.commentId} --method PATCH --field body=@${tempFile}`;
230
- await fs.unlink(tempFile).catch(() => {});
229
+ await writeSanitizedPublicationFile(tempFile, progressSection);
230
+ try {
231
+ await $`gh api repos/${owner}/${repo}/issues/comments/${state.commentId} --method PATCH --field body=@${tempFile}`;
232
+ } finally {
233
+ await fs.unlink(tempFile).catch(() => {});
234
+ }
231
235
  } else {
232
236
  // Create new comment. Issue #1625: post via postTrackedCommentFromFile
233
237
  // so the comment ID is captured directly from the GitHub API response
@@ -235,9 +239,13 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
235
239
  // posted comments from the "did the AI post anything?" check).
236
240
  const fs = (await import('fs')).promises;
237
241
  const tempFile = `/tmp/pr-progress-comment-${prNumber}-${Date.now()}.md`;
238
- await fs.writeFile(tempFile, progressSection);
239
- const posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber: prNumber, bodyFile: tempFile });
240
- await fs.unlink(tempFile).catch(() => {});
242
+ await writeSanitizedPublicationFile(tempFile, progressSection);
243
+ let posted;
244
+ try {
245
+ posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber: prNumber, bodyFile: tempFile });
246
+ } finally {
247
+ await fs.unlink(tempFile).catch(() => {});
248
+ }
241
249
 
242
250
  if (posted.ok && posted.commentId) {
243
251
  state.commentId = posted.commentId;
@@ -300,9 +308,12 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
300
308
  // Write to temp file and update PR
301
309
  const fs = (await import('fs')).promises;
302
310
  const tempBodyFile = `/tmp/pr-progress-${prNumber}-${Date.now()}.md`;
303
- await fs.writeFile(tempBodyFile, updatedBody);
304
- await $`gh pr edit ${prNumber} --repo ${owner}/${repo} --body-file ${tempBodyFile}`;
305
- await fs.unlink(tempBodyFile).catch(() => {});
311
+ await writeSanitizedPublicationFile(tempBodyFile, updatedBody);
312
+ try {
313
+ await $`gh pr edit ${prNumber} --repo ${owner}/${repo} --body-file ${tempBodyFile}`;
314
+ } finally {
315
+ await fs.unlink(tempBodyFile).catch(() => {});
316
+ }
306
317
 
307
318
  const stats = calculateProgress(todos);
308
319
  await log(`📊 Updated PR progress: ${stats.percentage}% (${stats.completed}/${stats.total} tasks completed)`);
@@ -28,13 +28,15 @@ import { safeExit } from './exit-handler.lib.mjs';
28
28
  // Import GitHub-related functions
29
29
  const githubLib = await import('./github.lib.mjs');
30
30
  const { sanitizeLogContent, attachLogToGitHub } = githubLib;
31
+ const { buildCostInfoString } = await import('./github-cost-info.lib.mjs');
32
+ const { buildBudgetStatsString } = await import('./claude.budget-stats.lib.mjs');
31
33
 
32
34
  // Issue #1745: process-wide sanitization counters used to print a one-line
33
35
  // "we masked N secrets" summary at the end of each run.
34
- const { formatSanitizationSummary } = await import('./token-sanitization.lib.mjs');
36
+ const { formatSanitizationSummary, sanitizeForPublication, writeSanitizedPublicationFile } = await import('./token-sanitization.lib.mjs');
35
37
  // Issue #1745: post-finish retroactive sanitization of bot-authored PR
36
- // comments and the PR description. Runs by default; can be skipped via
37
- // --dangerously-skip-output-sanitization.
38
+ // comments and the PR description. This external repair boundary always runs
39
+ // when PR coordinates are available.
38
40
  const { runPostFinishSweep } = await import('./post-finish-sanitization-sweep.lib.mjs');
39
41
 
40
42
  // Import continuation functions (session resumption, PR detection)
@@ -153,7 +155,7 @@ export const ensurePullRequestIssueLink = async ({ prNumber, issueNumber, owner,
153
155
 
154
156
  const fs = (await use('fs')).promises;
155
157
  const tempBodyFile = `/tmp/pr-body-update-${prNumber}-${Date.now()}.md`;
156
- await fs.writeFile(tempBodyFile, linkResult.body);
158
+ await writeSanitizedPublicationFile(tempBodyFile, linkResult.body);
157
159
 
158
160
  try {
159
161
  const updateResult = await command`gh pr edit ${prNumber} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
@@ -638,25 +640,19 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
638
640
  // Issue #1745: post-finish retroactive sanitization sweep. Re-reads
639
641
  // bot-authored PR comments and the PR description, runs them through
640
642
  // sanitizeOutput, and edits in place if a leak slipped past the live
641
- // sanitizer. Honors --dangerously-skip-output-sanitization and the related
642
- // active-tokens flag.
643
+ // sanitizer. Publication repair is a strict external boundary, so local
644
+ // diagnostic bypass flags never disable it.
643
645
  try {
644
646
  const owner = argv.owner;
645
647
  const repo = argv.repo;
646
648
  const prNumber = argv.prNumber;
647
- const skipOutputSanitization = argv['dangerously-skip-output-sanitization'] === true;
648
- const skipActiveTokensOutputSanitization = argv['dangerously-skip-active-tokens-output-sanitization'] === true;
649
- if (owner && repo && prNumber && !skipOutputSanitization) {
649
+ if (owner && repo && prNumber) {
650
650
  const sweepResult = await runPostFinishSweep({
651
651
  $,
652
652
  owner,
653
653
  repo,
654
654
  prNumber,
655
655
  log,
656
- sanitizationOptions: {
657
- warnOnMismatch: false,
658
- skipActiveTokensOutputSanitization,
659
- },
660
656
  });
661
657
  if (sweepResult.totalEdited > 0) {
662
658
  await log(`🔒 Post-finish sweep: edited ${sweepResult.totalEdited} bot-authored item(s) to mask leaked tokens.`);
@@ -669,11 +665,9 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
669
665
  }
670
666
  };
671
667
 
672
- // Verify results by searching for new PRs and comments
673
- export const verifyResults = async (owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart = false, sessionId = null, tempDir = null, anthropicTotalCostUSD = null, publicPricingEstimate = null, pricingInfo = null, errorDuringExecution = false, sessionType = 'new', resultModelUsage = null, streamTokenUsage = null, subAgentCalls = null) => {
674
- await log('\n🔍 Searching for created pull requests or comments...');
675
-
676
- // Issue #1491, #1526: Build budget stats data for GitHub comment (computed once, used in both PR and issue paths)
668
+ // Build token/context data once so every end-of-session publication can use the
669
+ // same observed facts (working-session summary and attached log alike).
670
+ export const buildSessionBudgetStatsData = async ({ argv, sessionId = null, tempDir = null, resultModelUsage = null, streamTokenUsage = null, subAgentCalls = null, pricingInfo = null }) => {
677
671
  let budgetStatsData = null;
678
672
  if (argv.tokensBudgetStats && sessionId && tempDir) {
679
673
  try {
@@ -698,6 +692,26 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
698
692
  if (argv.verbose) await log(` ⚠️ Could not build agent budget stats: ${agentBudgetError.message}`, { verbose: true });
699
693
  }
700
694
  }
695
+ return budgetStatsData;
696
+ };
697
+
698
+ // Verify results by searching for new PRs and comments
699
+ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart = false, sessionId = null, tempDir = null, anthropicTotalCostUSD = null, publicPricingEstimate = null, pricingInfo = null, errorDuringExecution = false, sessionType = 'new', resultModelUsage = null, streamTokenUsage = null, subAgentCalls = null, precomputedBudgetStatsData = null) => {
700
+ await log('\n🔍 Searching for created pull requests or comments...');
701
+
702
+ // Issue #1491, #1526, #2115: reuse data already calculated for the working
703
+ // session summary; retain the fallback for callers that do not precompute it.
704
+ const budgetStatsData =
705
+ precomputedBudgetStatsData ??
706
+ (await buildSessionBudgetStatsData({
707
+ argv,
708
+ sessionId,
709
+ tempDir,
710
+ resultModelUsage,
711
+ streamTokenUsage,
712
+ subAgentCalls,
713
+ pricingInfo,
714
+ }));
701
715
 
702
716
  try {
703
717
  // Get the current user's GitHub username
@@ -771,7 +785,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
771
785
  // Skip cleanup if auto-restart-on-non-updated-pull-request-description is enabled
772
786
  // (let the agent handle it on restart instead)
773
787
  if (prTitleHasPlaceholder && !argv.autoRestartOnNonUpdatedPullRequestDescription) {
774
- const updatedTitle = pr.title.replace(/^\[WIP\]\s*/, '');
788
+ const updatedTitle = await sanitizeForPublication(pr.title.replace(/^\[WIP\]\s*/, ''));
775
789
  await log(` 📝 Removing [WIP] prefix from PR title...`);
776
790
  const titleResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --title "${updatedTitle}"`;
777
791
  if (titleResult.code === 0) {
@@ -819,7 +833,7 @@ Fixes ${issueRef}
819
833
  *This PR was created automatically by the AI issue solver*`;
820
834
 
821
835
  const tempBodyFile = `/tmp/pr-body-finalize-${pr.number}-${Date.now()}.md`;
822
- await fs.writeFile(tempBodyFile, newDescription);
836
+ await writeSanitizedPublicationFile(tempBodyFile, newDescription);
823
837
 
824
838
  try {
825
839
  const descResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
@@ -1247,7 +1261,15 @@ export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, p
1247
1261
  * @param {string} options.repo - Repository name
1248
1262
  * @returns {Promise<boolean>} - True if comment was posted successfully
1249
1263
  */
1250
- export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumber, owner, repo }) => {
1264
+ export const buildWorkingSessionSummaryDetails = ({ publicPricingEstimate = null, anthropicTotalCostUSD = null, pricingInfo = null, budgetStatsData = null } = {}) => {
1265
+ const costInfo = buildCostInfoString(publicPricingEstimate, anthropicTotalCostUSD, pricingInfo, {
1266
+ includeTokenUsage: false,
1267
+ });
1268
+ const budgetStats = budgetStatsData ? buildBudgetStatsString(budgetStatsData.tokenUsage, budgetStatsData.subAgentCalls) : '';
1269
+ return `${costInfo}${budgetStats}`.trim();
1270
+ };
1271
+
1272
+ export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumber, owner, repo, publicPricingEstimate = null, anthropicTotalCostUSD = null, pricingInfo = null, budgetStatsData = null }) => {
1251
1273
  if (!resultSummary || typeof resultSummary !== 'string') {
1252
1274
  await log('⚠️ No working session summary available to attach', { verbose: true });
1253
1275
  return false;
@@ -1262,10 +1284,16 @@ export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumb
1262
1284
  }
1263
1285
 
1264
1286
  try {
1287
+ const usageDetails = buildWorkingSessionSummaryDetails({
1288
+ publicPricingEstimate,
1289
+ anthropicTotalCostUSD,
1290
+ pricingInfo,
1291
+ budgetStatsData,
1292
+ });
1265
1293
  const comment = `${toolComments.WORKING_SESSION_SUMMARY_AUTOMATION_MARKER}
1266
1294
  ## ${toolComments.WORKING_SESSION_SUMMARY_MARKER}
1267
1295
 
1268
- ${resultSummary}
1296
+ ${resultSummary}${usageDetails ? `\n\n${usageDetails}` : ''}
1269
1297
 
1270
1298
  ---
1271
1299
  *${toolComments.WORKING_SESSION_SUMMARY_AUTOMATED_FOOTER}*`;
@@ -1322,7 +1350,7 @@ ${resultSummary}
1322
1350
  * @param {boolean} [options.success=true] - skip attachment for failed iterations
1323
1351
  * @returns {Promise<{attached: boolean, reason: string}>}
1324
1352
  */
1325
- export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, workStartTime, owner, repo, prNumber, issueNumber, success = true }) => {
1353
+ export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, workStartTime, owner, repo, prNumber, issueNumber, success = true, publicPricingEstimate = null, anthropicTotalCostUSD = null, pricingInfo = null, budgetStatsData = null, sessionUsage = null }) => {
1326
1354
  if (!success) {
1327
1355
  return { attached: false, reason: 'iteration_failed' };
1328
1356
  }
@@ -1358,6 +1386,25 @@ export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, wo
1358
1386
  return { attached: false, reason: 'no_attach_decision' };
1359
1387
  }
1360
1388
 
1361
- const ok = await attachSolutionSummary({ resultSummary, prNumber, issueNumber, owner, repo });
1362
- return { attached: !!ok, reason: ok ? 'attached' : 'post_failed' };
1389
+ const resolvedBudgetStatsData =
1390
+ budgetStatsData ??
1391
+ (sessionUsage
1392
+ ? await buildSessionBudgetStatsData({
1393
+ argv,
1394
+ pricingInfo,
1395
+ ...sessionUsage,
1396
+ })
1397
+ : null);
1398
+ const ok = await attachSolutionSummary({
1399
+ resultSummary,
1400
+ prNumber,
1401
+ issueNumber,
1402
+ owner,
1403
+ repo,
1404
+ publicPricingEstimate,
1405
+ anthropicTotalCostUSD,
1406
+ pricingInfo,
1407
+ budgetStatsData: resolvedBudgetStatsData,
1408
+ });
1409
+ return { attached: !!ok, reason: ok ? 'attached' : 'post_failed', budgetStatsData: resolvedBudgetStatsData };
1363
1410
  };
@@ -580,6 +580,10 @@ export const watchForFeedback = async params => {
580
580
  prNumber,
581
581
  issueNumber,
582
582
  success: true,
583
+ publicPricingEstimate: toolResult.publicPricingEstimate,
584
+ anthropicTotalCostUSD: latestAnthropicCost,
585
+ pricingInfo: toolResult.pricingInfo,
586
+ budgetStatsData: autoRestartBudgetStatsData,
583
587
  });
584
588
  } catch (summaryError) {
585
589
  reportError(summaryError, {
@@ -4,6 +4,9 @@
4
4
  import { exec } from 'child_process';
5
5
  import { promisify } from 'util';
6
6
  import { parseCliArgumentsWithLino } from './cli-arguments.lib.mjs';
7
+ import { setupStdioLogInterceptor } from './lib.mjs';
8
+
9
+ setupStdioLogInterceptor();
7
10
 
8
11
  const execAsync = promisify(exec);
9
12
 
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { spawn } from 'child_process';
4
4
  import { promises as fs } from 'fs';
5
5
  import { parseGitHubUrl } from './github.lib.mjs';
6
+ import { sanitizeForPublication, writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
6
7
 
7
8
  export const TASK_ISSUE_TITLE_MAX_LENGTH = 256;
8
9
 
@@ -216,9 +217,10 @@ export async function createTaskIssue({ repository, title, body, issueType = nul
216
217
  const bodyFile = path.join(tempDir, 'body.md');
217
218
 
218
219
  try {
219
- await fs.writeFile(bodyFile, body);
220
+ await writeSanitizedPublicationFile(bodyFile, body);
221
+ const sanitizedTitle = await sanitizeForPublication(title);
220
222
 
221
- const result = await run('gh', buildCreateIssueArgs({ repository, title, bodyFile, issueType, labels }));
223
+ const result = await run('gh', buildCreateIssueArgs({ repository, title: sanitizedTitle, bodyFile, issueType, labels }));
222
224
  if (result.code === 0) return parseCreatedTaskIssueOutput(result.stdout);
223
225
 
224
226
  const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
@@ -228,7 +230,7 @@ export async function createTaskIssue({ repository, title, body, issueType = nul
228
230
  }
229
231
 
230
232
  await log?.(`⚠️ Could not create issue with type/labels (${output || `exit code ${result.code}`}); retrying without them`);
231
- const retry = await run('gh', buildCreateIssueArgs({ repository, title, bodyFile }));
233
+ const retry = await run('gh', buildCreateIssueArgs({ repository, title: sanitizedTitle, bodyFile }));
232
234
  if (retry.code !== 0) {
233
235
  const retryOutput = `${retry.stderr || ''}${retry.stdout || ''}`.trim();
234
236
  throw new Error(retryOutput || `gh issue create exited with code ${retry.code}`);
package/src/task.mjs CHANGED
@@ -8,6 +8,11 @@ import { buildStartAgentArgs, resolveStartAgentCommand } from './task.agent-comm
8
8
  import { getDefaultTaskModel, parseTaskArguments } from './task.config.lib.mjs';
9
9
  import { validateModelName } from './models/index.mjs';
10
10
  import { appendOrReplaceParentSplitSection, buildAddSubIssueApiArgs, buildIssueRestIdApiArgs, buildTaskSplitPrompt, buildTaskSplitSystemPrompt, extractTaskSplitJson, formatChildIssueBody, normalizeSplitTasks, parseCreatedIssueUrl, parseTaskIssueUrl } from './task.split.lib.mjs';
11
+ import { setupStdioLogInterceptor } from './lib.mjs';
12
+ import { sanitizeCredentialText } from './credential-sanitization-core.lib.mjs';
13
+ import { sanitizeForPublication } from './token-sanitization.lib.mjs';
14
+
15
+ setupStdioLogInterceptor();
11
16
 
12
17
  const earlyArgs = process.argv.slice(2);
13
18
 
@@ -73,10 +78,14 @@ const logFile = path.join(scriptDir, `task-${timestamp}.log`);
73
78
  async function log(message, options = {}) {
74
79
  const { level = 'info', verbose = false } = options;
75
80
  if (verbose && !argv.verbose) return;
76
- await fs.appendFile(logFile, `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}\n`).catch(() => {});
77
- if (level === 'error') console.error(message);
78
- else if (level === 'warning' || level === 'warn') console.warn(message);
79
- else console.log(message);
81
+ const sanitizedMessage = sanitizeCredentialText(message);
82
+ await fs
83
+ .appendFile(logFile, `[${new Date().toISOString()}] [${level.toUpperCase()}] ${sanitizedMessage}\n`, { mode: 0o600 })
84
+ .then(() => fs.chmod(logFile, 0o600))
85
+ .catch(() => {});
86
+ if (level === 'error') console.error(sanitizedMessage);
87
+ else if (level === 'warning' || level === 'warn') console.warn(sanitizedMessage);
88
+ else console.log(sanitizedMessage);
80
89
  }
81
90
 
82
91
  function formatAligned(icon, label, value, indent = 0) {
@@ -172,7 +181,8 @@ async function fetchIssueRestId(issue) {
172
181
  }
173
182
 
174
183
  async function createChildIssue(parentIssue, task, index, splitCount) {
175
- const args = ['issue', 'create', '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--title', task.title, '--body', formatChildIssueBody({ parentIssue, task, index, splitCount })];
184
+ const [safeTitle, safeBody] = await Promise.all([sanitizeForPublication(task.title), sanitizeForPublication(formatChildIssueBody({ parentIssue, task, index, splitCount }))]);
185
+ const args = ['issue', 'create', '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--title', safeTitle, '--body', safeBody];
176
186
  if (parentIssue.labels.length > 0) {
177
187
  args.push('--label', parentIssue.labels.join(','));
178
188
  }
@@ -196,9 +206,11 @@ async function linkChildIssue(parentIssue, childIssue) {
196
206
 
197
207
  async function updateParentIssue(parentIssue, childIssues) {
198
208
  const body = appendOrReplaceParentSplitSection(parentIssue.body, childIssues);
199
- await commandOutput('gh', ['issue', 'edit', String(parentIssue.number), '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--body', body]);
209
+ const safeBody = await sanitizeForPublication(body);
210
+ await commandOutput('gh', ['issue', 'edit', String(parentIssue.number), '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--body', safeBody]);
200
211
  const childList = childIssues.map(issue => `- #${issue.number} ${issue.title}`).join('\n');
201
- await commandOutput('gh', ['issue', 'comment', String(parentIssue.number), '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--body', `Split into ${childIssues.length} tasks:\n\n${childList}`]);
212
+ const safeComment = await sanitizeForPublication(`Split into ${childIssues.length} tasks:\n\n${childList}`);
213
+ await commandOutput('gh', ['issue', 'comment', String(parentIssue.number), '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--body', safeComment]);
202
214
  }
203
215
 
204
216
  async function runSplitMode() {
@@ -268,7 +280,8 @@ ${results.clarification ? `Clarification analysis:\n${results.clarification}\n\n
268
280
  }
269
281
 
270
282
  try {
271
- await fs.writeFile(logFile, `# Task Log - ${new Date().toISOString()}\n\n`);
283
+ await fs.writeFile(logFile, `# Task Log - ${new Date().toISOString()}\n\n`, { mode: 0o600 });
284
+ await fs.chmod(logFile, 0o600);
272
285
  await log(`📁 Log file: ${logFile}`);
273
286
  await log('\n🎯 Task Processing Started');
274
287
  await log(formatAligned('📝', 'Task input:', taskInput));
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { ensureUseM } from './use-m-bootstrap.lib.mjs';
3
+ import { maskToken, setupStdioLogInterceptor } from './lib.mjs';
4
+
5
+ setupStdioLogInterceptor();
3
6
  // Early exit for --version (issue #1318: avoid dotenvx MISSING_ENV_FILE warnings)
4
7
  if (process.argv.includes('--version')) {
5
8
  const v = await import('./version.lib.mjs').then(m => m.getVersion()).catch(() => 'unknown');
@@ -212,7 +215,7 @@ if (hiveEnabled && hiveOverrides.length > 0) {
212
215
  if (config.dryRun) {
213
216
  console.log('\n✅ Dry-run mode: All validations passed successfully!');
214
217
  console.log('\nConfiguration summary:');
215
- console.log(' Token:', BOT_TOKEN ? `${BOT_TOKEN.substring(0, 10)}...` : 'not set');
218
+ console.log(' Token:', BOT_TOKEN ? maskToken(BOT_TOKEN) : 'not set');
216
219
  if (allowedChats && allowedChats.length > 0) {
217
220
  console.log(' Allowed chats:', lino.format(allowedChats));
218
221
  } else {