@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.
- package/CHANGELOG.md +12 -0
- package/README.hi.md +2 -0
- package/README.md +2 -0
- package/README.ru.md +2 -0
- package/README.zh.md +2 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +0 -4
- package/src/cleanup.mjs +18 -6
- package/src/codex.lib.mjs +0 -4
- package/src/configure-claude.mjs +3 -0
- package/src/credential-sanitization-core.lib.mjs +231 -0
- package/src/development-log.lib.mjs +39 -6
- package/src/fix.mjs +3 -0
- package/src/github-error-reporter.lib.mjs +13 -8
- package/src/github-issue-auto-close.lib.mjs +2 -1
- package/src/github-merge-issue-close.lib.mjs +2 -1
- package/src/github.lib.mjs +29 -18
- package/src/hive-screens.mjs +3 -0
- package/src/instrument.mjs +14 -0
- package/src/interactive-mode.lib.mjs +25 -40
- package/src/lib.mjs +89 -50
- package/src/log-upload.lib.mjs +22 -4
- package/src/post-finish-sanitization-sweep.lib.mjs +5 -5
- package/src/review.mjs +3 -1
- package/src/sentry.lib.mjs +27 -8
- package/src/solve.auto-merge.lib.mjs +4 -0
- package/src/solve.auto-pr.lib.mjs +11 -21
- package/src/solve.error-handlers.lib.mjs +2 -1
- package/src/solve.mjs +7 -8
- package/src/solve.progress-monitoring.lib.mjs +20 -9
- package/src/solve.results.lib.mjs +72 -25
- package/src/solve.watch.lib.mjs +4 -0
- package/src/start-screen.mjs +3 -0
- package/src/task.issue-creation.lib.mjs +5 -3
- package/src/task.mjs +21 -8
- package/src/telegram-bot.mjs +4 -1
- package/src/telegram-log-command.lib.mjs +38 -4
- package/src/telegram-safe-reply.lib.mjs +7 -4
- package/src/telegram-tokens-command.lib.mjs +1 -1
- package/src/token-sanitization.lib.mjs +177 -14
- package/src/tool-comments.lib.mjs +5 -5
- package/src/youtrack/youtrack-sync.mjs +4 -3
package/src/sentry.lib.mjs
CHANGED
|
@@ -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
|
-
|
|
912
|
-
await
|
|
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
|
-
|
|
919
|
-
await
|
|
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:
|
|
1232
|
-
|
|
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
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
37
|
-
//
|
|
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
|
|
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.
|
|
642
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
673
|
-
|
|
674
|
-
|
|
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
|
|
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
|
|
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
|
|
1362
|
-
|
|
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
|
};
|
package/src/solve.watch.lib.mjs
CHANGED
|
@@ -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, {
|
package/src/start-screen.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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));
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -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 ?
|
|
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 {
|