@link-assistant/hive-mind 2.11.13 → 2.12.1

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 (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +4 -1
  3. package/src/agent-command.lib.mjs +74 -0
  4. package/src/agent.lib.mjs +59 -34
  5. package/src/agentic-cli-updater.lib.mjs +241 -0
  6. package/src/claude.connection.lib.mjs +209 -0
  7. package/src/claude.lib.mjs +6 -202
  8. package/src/codex.lib.mjs +0 -128
  9. package/src/formal-ai-isolation.lib.mjs +62 -0
  10. package/src/formal-ai-maintenance.lib.mjs +106 -0
  11. package/src/formal-ai-model.lib.mjs +25 -0
  12. package/src/formal-ai-runtime.lib.mjs +10 -0
  13. package/src/formal-ai-sidecar.lib.mjs +565 -0
  14. package/src/formal-ai-updater.lib.mjs +294 -0
  15. package/src/formal-ai-version.lib.mjs +100 -0
  16. package/src/formal-ai.lib.mjs +11 -16
  17. package/src/github-rate-limit.lib.mjs +3 -0
  18. package/src/github-url-parser.lib.mjs +255 -0
  19. package/src/github.lib.mjs +22 -343
  20. package/src/hive.mjs +0 -152
  21. package/src/interactive-mode.lib.mjs +0 -43
  22. package/src/isolation-runner.lib.mjs +44 -173
  23. package/src/limits.lib.mjs +0 -89
  24. package/src/model-args.lib.mjs +32 -0
  25. package/src/models/index.mjs +5 -19
  26. package/src/session-monitor.lib.mjs +14 -172
  27. package/src/solve.auto-merge.lib.mjs +70 -164
  28. package/src/solve.mjs +31 -193
  29. package/src/solve.repository.lib.mjs +0 -83
  30. package/src/solve.results.lib.mjs +2 -92
  31. package/src/solve.session.lib.mjs +52 -19
  32. package/src/solve.tool-uncommitted.lib.mjs +22 -0
  33. package/src/state-lock.lib.mjs +82 -0
  34. package/src/telegram-bot.mjs +17 -65
  35. package/src/telegram-fix-command.lib.mjs +1 -8
  36. package/src/telegram-merge-queue.lib.mjs +3 -155
  37. package/src/telegram-solve-queue.lib.mjs +9 -168
  38. package/src/telegram-task-command.lib.mjs +1 -8
  39. package/src/use-m-bootstrap.lib.mjs +6 -5
  40. package/src/use-with-retry.lib.mjs +128 -2
  41. package/src/working-session-summary.lib.mjs +47 -1
@@ -3,7 +3,6 @@ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
3
3
 
4
4
  // Results processing module for solve command
5
5
  // Extracted from solve.mjs to keep files under 1500 lines
6
-
7
6
  // Use use-m to dynamically import modules for cross-runtime compatibility
8
7
  // Check if use is already defined globally (when imported from solve.mjs)
9
8
  // If not, fetch it (when running standalone)
@@ -18,18 +17,15 @@ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
18
17
  const { QUIET_PROBE } = await import('./quiet-probe.lib.mjs'); // issue #2130: keep read-only probe payloads out of the attached log
19
18
  const $ = wrapDollarWithGhRetry(__rawDollar$);
20
19
  const path = (await use('path')).default;
21
-
22
20
  // Import shared library functions
23
21
  const lib = await import('./lib.mjs');
24
22
  const { log, getLogFile, formatAligned } = lib;
25
23
 
26
24
  // Import exit handler
27
25
  import { safeExit } from './exit-handler.lib.mjs';
28
-
29
26
  // Import GitHub-related functions
30
27
  const githubLib = await import('./github.lib.mjs');
31
28
  const { sanitizeLogContent, attachLogToGitHub } = githubLib;
32
-
33
29
  // Issue #1745: process-wide sanitization counters used to print a one-line
34
30
  // "we masked N secrets" summary at the end of each run.
35
31
  const { formatSanitizationSummary, sanitizeForPublication, writeSanitizedPublicationFile } = await import('./token-sanitization.lib.mjs');
@@ -41,7 +37,6 @@ const { runPostFinishSweep } = await import('./post-finish-sanitization-sweep.li
41
37
  // Import continuation functions (session resumption, PR detection)
42
38
  const autoContinue = await import('./solve.auto-continue.lib.mjs');
43
39
  const { autoContinueWhenLimitResets } = autoContinue;
44
-
45
40
  // Import Claude-specific command builders
46
41
  // These are used to generate copy-pasteable Claude CLI resume commands for users
47
42
  // Pattern: (cd "/tmp/gh-issue-solver-..." && claude --resume <session-id>)
@@ -55,21 +50,18 @@ export const { buildClaudeResumeCommand, buildClaudeAutonomousResumeCommand, bui
55
50
  // imported from tool libraries (claude/codex/gemini) without circular imports.
56
51
  import { buildSolveResumeCommand } from './solve.resume-command.lib.mjs';
57
52
  export { buildSolveResumeCommand };
58
-
59
53
  // Import error handling functions
60
54
  // const errorHandlers = await import('./solve.error-handlers.lib.mjs'); // Not currently used
61
55
  // Import Sentry integration
62
56
  const sentryLib = await import('./sentry.lib.mjs');
63
57
  const { reportError } = sentryLib;
64
-
65
58
  // Import pull request issue-link preservation helpers
66
59
  const prIssueLinking = await import('./pr-issue-linking.lib.mjs');
67
60
  const { buildIssueReference, ensureIssueLinkInPullRequestBody } = prIssueLinking;
68
61
 
69
62
  // Issue #2119: the one place that decides whether a pull request changed anything.
70
63
  const { formatChangeSummary, getPullRequestChangeStats } = await import('./pull-request-changes.lib.mjs');
71
- const { buildNoChangesNotice, redactWorkspacePaths } = await import('./working-session-summary.lib.mjs');
72
-
64
+ const { buildNoChangesNotice, formatWorkingSessionSummaryMarkdown, redactWorkspacePaths } = await import('./working-session-summary.lib.mjs');
73
65
  /**
74
66
  * Placeholder patterns used to detect auto-generated PR content that was not updated by the agent.
75
67
  * These patterns match the initial WIP PR created by solve.auto-pr.lib.mjs.
@@ -77,7 +69,6 @@ const { buildNoChangesNotice, redactWorkspacePaths } = await import('./working-s
77
69
  export const PR_TITLE_PLACEHOLDER_PREFIX = '[WIP]';
78
70
 
79
71
  export const PR_BODY_PLACEHOLDER_PATTERNS = ['_Details will be added as the solution draft is developed..._', '**Work in Progress** - The AI assistant is currently analyzing and implementing the solution draft.', '### 🚧 Status'];
80
-
81
72
  /**
82
73
  * Check if PR title still contains auto-generated placeholder content
83
74
  * @param {string} title - PR title
@@ -95,7 +86,6 @@ export const hasPRTitlePlaceholder = title => {
95
86
  export const hasPRBodyPlaceholder = body => {
96
87
  return body && PR_BODY_PLACEHOLDER_PATTERNS.some(pattern => body.includes(pattern));
97
88
  };
98
-
99
89
  /**
100
90
  * Build a short factual hint for auto-restart when PR title/description was not updated.
101
91
  * Uses neutral, fact-stating language (no forcing words).
@@ -114,7 +104,6 @@ export const buildPRNotUpdatedHint = (titleNotUpdated, descriptionNotUpdated) =>
114
104
  }
115
105
  return lines;
116
106
  };
117
-
118
107
  /**
119
108
  * Ensure an existing pull request body contains a GitHub closing keyword for the issue.
120
109
  *
@@ -140,7 +129,6 @@ export const ensurePullRequestIssueLink = async ({ prNumber, issueNumber, owner,
140
129
  await logger(` āš ļø Could not read PR body for issue link check: ${error}`);
141
130
  return { checked: false, updated: false, body: prBody, issueRef: buildIssueReference({ issueNumber, owner, repo, fork: argv.fork }), error };
142
131
  }
143
-
144
132
  prBody = prBodyResult.stdout.toString();
145
133
  const linkResult = ensureIssueLinkInPullRequestBody(prBody, {
146
134
  issueNumber,
@@ -153,9 +141,7 @@ export const ensurePullRequestIssueLink = async ({ prNumber, issueNumber, owner,
153
141
  await logger(' āœ… PR body already contains issue reference');
154
142
  return { checked: true, updated: false, body: linkResult.body, issueRef: linkResult.issueRef };
155
143
  }
156
-
157
144
  await logger(` šŸ“ Updating PR body to link issue #${issueNumber}...`);
158
-
159
145
  const fs = (await use('fs')).promises;
160
146
  const tempBodyFile = `/tmp/pr-body-update-${prNumber}-${Date.now()}.md`;
161
147
  await writeSanitizedPublicationFile(tempBodyFile, linkResult.body);
@@ -163,7 +149,6 @@ export const ensurePullRequestIssueLink = async ({ prNumber, issueNumber, owner,
163
149
  try {
164
150
  const updateResult = await command`gh pr edit ${prNumber} --repo ${owner}/${repo} --body-file ${tempBodyFile}`;
165
151
  await fs.unlink(tempBodyFile).catch(() => {});
166
-
167
152
  if (updateResult.code === 0) {
168
153
  await logger(` āœ… Updated PR body to include "Fixes ${linkResult.issueRef}"`);
169
154
  return { checked: true, updated: true, body: linkResult.body, issueRef: linkResult.issueRef };
@@ -177,7 +162,6 @@ export const ensurePullRequestIssueLink = async ({ prNumber, issueNumber, owner,
177
162
  throw updateError;
178
163
  }
179
164
  };
180
-
181
165
  export const verifyPullRequestIssueLinkAfterAutoRestart = async ({ prNumber, issueNumber, owner, repo, argv = {}, cleanErrorMessage = error => error.message }) => {
182
166
  if (!prNumber) {
183
167
  return { checked: false, updated: false, body: '', issueRef: buildIssueReference({ issueNumber, owner, repo, fork: argv.fork }) };
@@ -191,7 +175,6 @@ export const verifyPullRequestIssueLinkAfterAutoRestart = async ({ prNumber, iss
191
175
  return { checked: false, updated: false, body: '', issueRef: buildIssueReference({ issueNumber, owner, repo, fork: argv.fork }), error: issueLinkError.message };
192
176
  }
193
177
  };
194
-
195
178
  /**
196
179
  * Detect the CLAUDE.md or .gitkeep commit hash from branch structure when not available in session
197
180
  * This handles continue mode where the commit hash was lost between sessions
@@ -209,7 +192,6 @@ export const verifyPullRequestIssueLinkAfterAutoRestart = async ({ prNumber, iss
209
192
  const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
210
193
  try {
211
194
  await log(' Attempting to detect CLAUDE.md or .gitkeep commit from branch structure...', { verbose: true });
212
-
213
195
  // First check if CLAUDE.md or .gitkeep exists in current branch
214
196
  const claudeMdExistsResult = await $({ cwd: tempDir })`git ls-files CLAUDE.md 2>&1`;
215
197
  const gitkeepExistsResult = await $({ cwd: tempDir })`git ls-files .gitkeep 2>&1`;
@@ -220,7 +202,6 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
220
202
  await log(' Neither CLAUDE.md nor .gitkeep exists in current branch', { verbose: true });
221
203
  return null;
222
204
  }
223
-
224
205
  // Get the default branch to find the fork point
225
206
  const defaultBranchResult = await $({ cwd: tempDir })`git symbolic-ref refs/remotes/origin/HEAD 2>&1`;
226
207
  let defaultBranch = 'main';
@@ -240,7 +221,6 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
240
221
  }
241
222
  const mergeBase = mergeBaseResult.stdout.toString().trim();
242
223
  await log(` Merge base: ${mergeBase.substring(0, 7)}`, { verbose: true });
243
-
244
224
  // Get all commits on the PR branch (commits after the merge base)
245
225
  // Format: hash|message|files_changed
246
226
  const branchCommitsResult = await $({ cwd: tempDir })`git log ${mergeBase}..HEAD --reverse --format="%H|%s" 2>&1`;
@@ -248,7 +228,6 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
248
228
  await log(' No commits found on PR branch', { verbose: true });
249
229
  return null;
250
230
  }
251
-
252
231
  const branchCommits = branchCommitsResult.stdout.toString().trim().split('\n').filter(Boolean);
253
232
  if (branchCommits.length === 0) {
254
233
  await log(' No commits found on PR branch', { verbose: true });
@@ -256,7 +235,6 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
256
235
  }
257
236
 
258
237
  await log(` Found ${branchCommits.length} commit(s) on PR branch`, { verbose: true });
259
-
260
238
  // Safety check: Must have at least 2 commits (CLAUDE.md commit + actual work)
261
239
  if (branchCommits.length < 2) {
262
240
  await log(' Only 1 commit on branch - not enough commits to safely revert CLAUDE.md', { verbose: true });
@@ -267,14 +245,12 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
267
245
  // Get the first commit on the PR branch
268
246
  const firstCommitLine = branchCommits[0];
269
247
  const [firstCommitHash, firstCommitMessage] = firstCommitLine.split('|');
270
-
271
248
  await log(` First commit on branch: ${firstCommitHash.substring(0, 7)} - "${firstCommitMessage}"`, {
272
249
  verbose: true,
273
250
  });
274
251
 
275
252
  // Safety check: Verify commit message matches expected pattern (CLAUDE.md or .gitkeep)
276
253
  const expectedMessagePatterns = [/^Initial commit with task details/i, /^Add CLAUDE\.md/i, /^CLAUDE\.md/i, /^Add \.gitkeep/i, /\.gitkeep/i];
277
-
278
254
  const messageMatches = expectedMessagePatterns.some(pattern => pattern.test(firstCommitMessage));
279
255
  if (!messageMatches) {
280
256
  await log(' First commit message does not match expected pattern', { verbose: true });
@@ -283,7 +259,6 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
283
259
  });
284
260
  return null;
285
261
  }
286
-
287
262
  // Safety check: Verify the commit ONLY adds CLAUDE.md or .gitkeep file (no other files)
288
263
  const filesChangedResult = await $({
289
264
  cwd: tempDir,
@@ -295,7 +270,6 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
295
270
 
296
271
  const filesChanged = filesChangedResult.stdout.toString().trim().split('\n').filter(Boolean);
297
272
  await log(` Files changed in first commit: ${filesChanged.join(', ')}`, { verbose: true });
298
-
299
273
  // Check if CLAUDE.md or .gitkeep is in the files changed
300
274
  const hasClaudeMd = filesChanged.includes('CLAUDE.md');
301
275
  const hasGitkeep = filesChanged.includes('.gitkeep');
@@ -305,7 +279,6 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
305
279
  }
306
280
 
307
281
  const targetFile = hasClaudeMd ? 'CLAUDE.md' : '.gitkeep';
308
-
309
282
  // CRITICAL SAFETY CHECK: Only allow revert if the target file is the ONLY file changed
310
283
  // This prevents Issue #617 where reverting a commit deleted .gitignore, LICENSE, README.md
311
284
  if (filesChanged.length > 1) {
@@ -316,7 +289,6 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
316
289
  await log(' Refusing to revert to prevent data loss (Issue #617 safety)', { verbose: true });
317
290
  return null;
318
291
  }
319
-
320
292
  // All safety checks passed!
321
293
  await log(` āœ… Detected ${targetFile} commit: ${firstCommitHash.substring(0, 7)}`, { verbose: true });
322
294
  await log(` āœ… Commit only contains ${targetFile} (safe to revert)`, { verbose: true });
@@ -334,7 +306,6 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
334
306
  return null;
335
307
  }
336
308
  };
337
-
338
309
  const wasFileTouchedAfterCommit = async (tempDir, commitHash, fileName) => {
339
310
  const changedCommitsResult = await $({ cwd: tempDir, silent: true })`git log --format=%H ${commitHash}..HEAD -- ${fileName}`;
340
311
  if (changedCommitsResult.code === 0) {
@@ -345,7 +316,6 @@ const wasFileTouchedAfterCommit = async (tempDir, commitHash, fileName) => {
345
316
  await log(` Could not inspect ${fileName} changes after initial commit`, { verbose: true });
346
317
  await log(` git log output: ${changedCommitsResult.stderr || changedCommitsResult.stdout || 'no output'}`, { verbose: true });
347
318
  }
348
-
349
319
  return true;
350
320
  };
351
321
 
@@ -357,14 +327,12 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
357
327
  if (!claudeCommitHash) {
358
328
  await log(' No initial commit hash from session, attempting to detect from branch...', { verbose: true });
359
329
  claudeCommitHash = await detectClaudeMdCommitFromBranch(tempDir, branchName);
360
-
361
330
  if (!claudeCommitHash) {
362
331
  await log(' Could not safely detect initial commit to revert', { verbose: true });
363
332
  return;
364
333
  }
365
334
  await log(` Detected initial commit: ${claudeCommitHash.substring(0, 7)}`, { verbose: true });
366
335
  }
367
-
368
336
  // Determine which file was used based on the commit message or actual files changed
369
337
  // Use %B (full message including body) instead of %s (subject only) to catch ".gitkeep" in body
370
338
  // Also check the actual files changed as a fallback (Issue #1436)
@@ -379,7 +347,6 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
379
347
  isGitkeepFile = files.includes('.gitkeep');
380
348
  }
381
349
  const fileName = isGitkeepFile ? '.gitkeep' : 'CLAUDE.md';
382
-
383
350
  await log(formatAligned('šŸ”„', 'Cleanup:', `Reverting ${fileName} commit`));
384
351
  await log(` Using saved commit hash: ${claudeCommitHash.substring(0, 7)}...`, { verbose: true });
385
352
 
@@ -391,9 +358,7 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
391
358
  } else {
392
359
  throw new Error(`git pull failed (code ${pullResult.code}): ${pullResult.stdout || pullResult.stderr || 'no output'}`);
393
360
  }
394
-
395
361
  const commitToRevert = claudeCommitHash;
396
-
397
362
  // Issue #1791: .gitkeep is a normal repository file in some projects, and
398
363
  // user work may intentionally edit or delete it. Once later PR commits touch
399
364
  // .gitkeep, final cleanup must not restore the pre-session version.
@@ -410,7 +375,6 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
410
375
  // Issue #2135: `mirror: false`. Only "is it non-empty" is asked here, and
411
376
  // the answer is a file's whole diff.
412
377
  const diffResult = await $({ cwd: tempDir, ...QUIET_PROBE })`git diff ${commitToRevert} HEAD -- ${fileName} 2>&1`;
413
-
414
378
  if (diffResult.stdout && diffResult.stdout.trim()) {
415
379
  // File was modified after initial commit - use manual approach to avoid conflicts
416
380
  await log(` ${fileName} was modified after initial commit, using manual cleanup...`, { verbose: true });
@@ -418,7 +382,6 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
418
382
  // Get the state of the file from before the initial commit (parent of the commit we're reverting)
419
383
  const parentCommit = `${commitToRevert}~1`;
420
384
  const parentFileExists = await $({ cwd: tempDir })`git cat-file -e ${parentCommit}:${fileName} 2>&1`;
421
-
422
385
  if (parentFileExists.code === 0) {
423
386
  // File existed before the initial commit - restore it to that state
424
387
  await log(` ${fileName} existed before session, restoring to previous state...`, { verbose: true });
@@ -431,10 +394,8 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
431
394
 
432
395
  // Create a manual revert commit
433
396
  const commitResult = await $({ cwd: tempDir })`git commit -m "Revert: Remove ${fileName} changes from initial commit" 2>&1`;
434
-
435
397
  if (commitResult.code === 0) {
436
398
  await log(formatAligned('šŸ“¦', 'Committed:', `${fileName} revert (manual)`));
437
-
438
399
  // Push the revert
439
400
  const pushRevertResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
440
401
  if (pushRevertResult.code === 0) {
@@ -454,7 +415,6 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
454
415
  const revertResult = await $({ cwd: tempDir })`git revert ${commitToRevert} --no-edit 2>&1`;
455
416
  if (revertResult.code === 0) {
456
417
  await log(formatAligned('šŸ“¦', 'Committed:', `${fileName} revert`));
457
-
458
418
  // Push the revert
459
419
  const pushRevertResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
460
420
  if (pushRevertResult.code === 0) {
@@ -469,11 +429,9 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
469
429
 
470
430
  if (hasConflict) {
471
431
  await log(' Unexpected conflict detected, attempting automatic resolution...', { verbose: true });
472
-
473
432
  // Check git status to see what files are in conflict
474
433
  const statusResult = await $({ cwd: tempDir })`git status --short 2>&1`;
475
434
  const statusOutput = statusResult.stdout || '';
476
-
477
435
  // Check if the file is in the conflict
478
436
  if (statusOutput.includes(fileName)) {
479
437
  await log(` Resolving ${fileName} conflict by restoring pre-session state...`, { verbose: true });
@@ -481,7 +439,6 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
481
439
  // Get the state of the file from before the initial commit (parent of the commit we're reverting)
482
440
  const parentCommit = `${commitToRevert}~1`;
483
441
  const parentFileExists = await $({ cwd: tempDir })`git cat-file -e ${parentCommit}:${fileName} 2>&1`;
484
-
485
442
  if (parentFileExists.code === 0) {
486
443
  // File existed before the initial commit - restore it to that state
487
444
  await log(` ${fileName} existed before session, restoring to previous state...`, { verbose: true });
@@ -497,7 +454,6 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
497
454
 
498
455
  // Complete the revert with the resolved conflict
499
456
  const continueResult = await $({ cwd: tempDir })`git revert --continue --no-edit 2>&1`;
500
-
501
457
  if (continueResult.code === 0) {
502
458
  await log(formatAligned('šŸ“¦', 'Committed:', `${fileName} revert (conflict resolved)`));
503
459
 
@@ -559,11 +515,9 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
559
515
  await log(' Initial commit revert failed or not needed', { verbose: true });
560
516
  }
561
517
  };
562
-
563
518
  // Show session summary and handle limit reached scenarios
564
519
  export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl, tempDir, shouldAttachLogs = false) => {
565
520
  await log('\n=== Session Summary ===');
566
-
567
521
  // Issue #1745: report how many tokens were masked during this run, with the
568
522
  // "use --dangerously-skip-output-sanitization to skip" hint when > 0.
569
523
  try {
@@ -580,7 +534,6 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
580
534
  // Always use absolute path for log file display
581
535
  const absoluteLogPath = path.resolve(getLogFile());
582
536
  await log(`āœ… Complete log file: ${absoluteLogPath}`);
583
-
584
537
  // Show three resume options:
585
538
  // 1. Interactive claude - opens Claude Code interactively (claude only)
586
539
  // 2. Autonomous claude - one-shot claude --resume w/ --dangerously-skip-permissions -p (claude only)
@@ -601,7 +554,6 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
601
554
 
602
555
  if (limitReached) {
603
556
  await log('ā° LIMIT REACHED DETECTED!');
604
-
605
557
  if ((argv.autoResumeOnLimitReset || argv.autoRestartOnLimitReset) && global.limitResetTime) {
606
558
  const isRestart = !!argv.autoRestartOnLimitReset;
607
559
  await log(`\nšŸ”„ AUTO-${isRestart ? 'RESTART' : 'RESUME'} ON LIMIT RESET ENABLED - Will ${isRestart ? 'restart' : 'resume'} at ${global.limitResetTime}`);
@@ -612,7 +564,6 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
612
564
  if (global.limitResetTime) {
613
565
  await log(`\nā° Limit resets at: ${global.limitResetTime}`);
614
566
  }
615
-
616
567
  await log('\nšŸ’” After the limit resets, resume using the command above.');
617
568
 
618
569
  if (argv.autoCleanup !== false) {
@@ -628,7 +579,6 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
628
579
  await log(' To keep the directory for debugging or resuming, use --no-auto-cleanup');
629
580
  }
630
581
  }
631
-
632
582
  // Don't show log preview, it's too technical
633
583
  } else {
634
584
  // For agent tool, session IDs may not be meaningful for resuming, so don't show as error
@@ -669,7 +619,6 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
669
619
  await log(`āš ļø Post-finish sanitization sweep failed: ${sweepErr.message || sweepErr}`);
670
620
  }
671
621
  };
672
-
673
622
  // Build token/context data once so every end-of-session publication can use the
674
623
  // same observed facts (working-session summary and attached log alike).
675
624
  export const buildSessionBudgetStatsData = async ({ argv, sessionId = null, tempDir = null, resultModelUsage = null, streamTokenUsage = null, subAgentCalls = null, pricingInfo = null }) => {
@@ -714,7 +663,6 @@ export const buildSessionBudgetStatsData = async ({ argv, sessionId = null, temp
714
663
  // Verify results by searching for new PRs and comments
715
664
  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) => {
716
665
  await log('\nšŸ” Searching for created pull requests or comments...');
717
-
718
666
  // Issue #1491, #1526, #2115: reuse data already calculated for the working
719
667
  // session summary; retain the fallback for callers that do not precompute it.
720
668
  const budgetStatsData =
@@ -728,7 +676,6 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
728
676
  subAgentCalls,
729
677
  pricingInfo,
730
678
  }));
731
-
732
679
  try {
733
680
  // Get the current user's GitHub username
734
681
  const userResult = await $(QUIET_PROBE)`gh api user --jq .login`;
@@ -736,7 +683,6 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
736
683
  if (userResult.code !== 0) {
737
684
  throw new Error(`Failed to get current user: ${userResult.stderr ? userResult.stderr.toString() : 'Unknown error'}`);
738
685
  }
739
-
740
686
  const currentUser = userResult.stdout.toString().trim();
741
687
  if (!currentUser) {
742
688
  throw new Error('Unable to determine current GitHub user');
@@ -744,20 +690,17 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
744
690
 
745
691
  // Search for pull requests created from our branch
746
692
  await log('\nšŸ” Checking for pull requests from branch ' + branchName + '...');
747
-
748
693
  // First, get all PRs from our branch
749
694
  // IMPORTANT: Use --state all to find PRs that may have been merged during the session (Issue #1008)
750
695
  // Without --state all, gh pr list only returns OPEN PRs, missing merged ones
751
696
  // Issue #2135: `mirror: false` - the pull requests found are named below.
752
697
  const allBranchPrsResult = await $(QUIET_PROBE)`gh pr list --repo ${owner}/${repo} --head ${branchName} --state all --json number,url,createdAt,headRefName,title,state,updatedAt,isDraft`;
753
-
754
698
  if (allBranchPrsResult.code !== 0) {
755
699
  await log(' āš ļø Failed to check pull requests');
756
700
  // Continue with empty list
757
701
  }
758
702
 
759
703
  const allBranchPrs = allBranchPrsResult.stdout.toString().trim() ? JSON.parse(allBranchPrsResult.stdout.toString().trim()) : [];
760
-
761
704
  // Check if we have any PRs from our branch
762
705
  // If auto-PR was created, it should be the one we're working on
763
706
  if (allBranchPrs.length > 0) {
@@ -766,7 +709,6 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
766
709
  // If we created a PR earlier in this session, it would be prNumber
767
710
  // Or if the PR was updated during the session (updatedAt > referenceTime)
768
711
  const isPrFromSession = (prNumber && pr.number.toString() === prNumber) || (prUrl && pr.url === prUrl) || new Date(pr.updatedAt) > referenceTime || new Date(pr.createdAt) > referenceTime;
769
-
770
712
  if (isPrFromSession) {
771
713
  await log(` āœ… Found pull request #${pr.number}: "${pr.title}"`);
772
714
 
@@ -775,11 +717,9 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
775
717
  if (isPrMerged) {
776
718
  await log(` ā„¹ļø PR #${pr.number} was merged during the session`);
777
719
  }
778
-
779
720
  // Declare placeholder detection variables outside block scopes for use in return value
780
721
  let prTitleHasPlaceholder = false;
781
722
  let prBodyHasPlaceholder = false;
782
-
783
723
  // Skip PR body update and ready conversion for merged PRs (they can't be edited)
784
724
  if (!isPrMerged) {
785
725
  const issueLinkResult = await ensurePullRequestIssueLink({
@@ -797,7 +737,6 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
797
737
  // Track this before cleanup for --auto-restart-on-non-updated-pull-request-description
798
738
  prTitleHasPlaceholder = hasPRTitlePlaceholder(pr.title);
799
739
  prBodyHasPlaceholder = hasPRBodyPlaceholder(prBody);
800
-
801
740
  // Issue #1162: Remove [WIP] prefix from title if still present
802
741
  // Skip cleanup if auto-restart-on-non-updated-pull-request-description is enabled
803
742
  // (let the agent handle it on restart instead)
@@ -817,7 +756,6 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
817
756
  const hasPlaceholder = prBodyHasPlaceholder;
818
757
  if (hasPlaceholder && !argv.autoRestartOnNonUpdatedPullRequestDescription) {
819
758
  await log(` šŸ“ Updating PR description to remove placeholder text...`);
820
-
821
759
  // Issue #2119: measure the net diff. The reproduction PRs published
822
760
  // "1 file(s) modified, 1 line(s) added" for a pull request that
823
761
  // changed nothing, because the stats were never checked for being
@@ -826,7 +764,6 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
826
764
  if (!changeStats.hasChanges) {
827
765
  await log(` āš ļø PR #${pr.number} has an empty diff - the description will say so instead of claiming changes`, { level: 'warning' });
828
766
  }
829
-
830
767
  // Get the issue title for context
831
768
  const issueTitleResult = await $`gh issue view ${issueNumber} --repo ${owner}/${repo} --json title --jq .title 2>&1`;
832
769
  const issueTitle = issueTitleResult.code === 0 ? issueTitleResult.stdout.toString().trim() : 'the issue';
@@ -846,10 +783,8 @@ Fixes ${issueRef}
846
783
 
847
784
  ---
848
785
  *This PR was created automatically by the AI issue solver*`;
849
-
850
786
  const tempBodyFile = `/tmp/pr-body-finalize-${pr.number}-${Date.now()}.md`;
851
787
  await writeSanitizedPublicationFile(tempBodyFile, newDescription);
852
-
853
788
  try {
854
789
  const descResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --body-file ${tempBodyFile}`;
855
790
  await fs.unlink(tempBodyFile).catch(() => {});
@@ -864,7 +799,6 @@ Fixes ${issueRef}
864
799
  await log(` āš ļø Error updating PR description: ${descError.message}`);
865
800
  }
866
801
  }
867
-
868
802
  // Check if PR is ready for review (convert from draft if necessary)
869
803
  if (pr.isDraft) {
870
804
  await log(' šŸ”„ Converting PR from draft to ready for review...');
@@ -913,7 +847,6 @@ Fixes ${issueRef}
913
847
  budgetStatsData,
914
848
  });
915
849
  }
916
-
917
850
  await log('\nšŸŽ‰ SUCCESS: A solution draft has been prepared as a pull request');
918
851
  await log(`šŸ“ URL: ${pr.url}`);
919
852
  if (shouldAttachLogs && logUploadSuccess) {
@@ -945,7 +878,6 @@ Fixes ${issueRef}
945
878
  } else {
946
879
  await log(` ā„¹ļø No pull requests found from branch ${branchName}`);
947
880
  }
948
-
949
881
  // If no PR found, search for recent comments on the issue
950
882
  await log('\nšŸ” Checking for new comments on issue #' + issueNumber + '...');
951
883
 
@@ -954,21 +886,18 @@ Fixes ${issueRef}
954
886
  // Issue #2135: `mirror: false` - the counts below are the report; the raw
955
887
  // answer is every comment body on the issue.
956
888
  const allCommentsResult = await $(QUIET_PROBE)`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
957
-
958
889
  if (allCommentsResult.code !== 0) {
959
890
  await log(' āš ļø Failed to check comments');
960
891
  // Continue with empty list
961
892
  }
962
893
 
963
894
  const allComments = JSON.parse(allCommentsResult.stdout.toString().trim() || '[]');
964
-
965
895
  // Filter for new comments by current user
966
896
  const newCommentsByUser = allComments.filter(comment => comment.user.login === currentUser && new Date(comment.created_at) > referenceTime);
967
897
 
968
898
  if (newCommentsByUser.length > 0) {
969
899
  const lastComment = newCommentsByUser[newCommentsByUser.length - 1];
970
900
  await log(` āœ… Found new comment by ${currentUser}`);
971
-
972
901
  // Upload log file to issue if requested
973
902
  if (shouldAttachLogs) {
974
903
  await log('\nšŸ“Ž Uploading solution draft log to issue...');
@@ -1002,7 +931,6 @@ Fixes ${issueRef}
1002
931
  budgetStatsData,
1003
932
  });
1004
933
  }
1005
-
1006
934
  await log('\nšŸ’¬ SUCCESS: Comment posted on issue');
1007
935
  await log(`šŸ“ URL: ${lastComment.html_url}`);
1008
936
  if (shouldAttachLogs) {
@@ -1059,7 +987,6 @@ Fixes ${issueRef}
1059
987
  return { logUploadSuccess: false }; // Return for watch mode
1060
988
  }
1061
989
  };
1062
-
1063
990
  // Handle execution errors with log attachment
1064
991
  export const handleExecutionError = async (error, shouldAttachLogs, owner, repo, argv = {}) => {
1065
992
  const { cleanErrorMessage } = await import('./lib.mjs');
@@ -1069,7 +996,6 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
1069
996
  // If --attach-logs is enabled, try to attach failure logs
1070
997
  if (shouldAttachLogs && getLogFile()) {
1071
998
  await log('\nšŸ“„ Attempting to attach failure logs...');
1072
-
1073
999
  // Try to attach to existing PR first
1074
1000
  if (global.createdPR && global.createdPR.number) {
1075
1001
  try {
@@ -1089,7 +1015,6 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
1089
1015
  requestedModel: argv.originalModel || argv.model,
1090
1016
  tool: argv.tool || 'claude',
1091
1017
  });
1092
-
1093
1018
  if (logUploadSuccess) {
1094
1019
  await log('šŸ“Ž Failure log attached to Pull Request');
1095
1020
  }
@@ -1123,7 +1048,6 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
1123
1048
  await log(`āš ļø Could not close pull request: ${closeError.message}`, { level: 'warning' });
1124
1049
  }
1125
1050
  }
1126
-
1127
1051
  await safeExit(1, 'Execution error');
1128
1052
  };
1129
1053
 
@@ -1134,7 +1058,6 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
1134
1058
  // from solve.results.lib.mjs.
1135
1059
  const toolComments = await import('./tool-comments.lib.mjs');
1136
1060
  export const { TOOL_GENERATED_COMMENT_MARKERS, isToolGeneratedComment, trackToolCommentId, isToolTrackedCommentId, getTrackedToolCommentIds, postTrackedComment } = toolComments;
1137
-
1138
1061
  /**
1139
1062
  * Check if new comments were created by the AI during the session.
1140
1063
  * This is used by --auto-attach-solution-summary to determine if the AI
@@ -1164,7 +1087,6 @@ export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, p
1164
1087
  }
1165
1088
 
1166
1089
  await log(`šŸ”Ž Checking comments by '${currentUser}' after session start ${sessionStartTime.toISOString()} (PR #${prNumber ?? 'none'}, issue #${issueNumber ?? 'none'})`, { verbose: true });
1167
-
1168
1090
  // Issue #1625: A comment counts as an "AI comment" only if it was posted
1169
1091
  // by the current user AFTER sessionStartTime AND solve.mjs did NOT post it
1170
1092
  // itself. We identify tool-posted comments in two ways, in order:
@@ -1184,7 +1106,6 @@ export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, p
1184
1106
  for (const comment of comments) {
1185
1107
  if (!comment || !comment.user || comment.user.login !== currentUser) continue;
1186
1108
  if (!(new Date(comment.created_at) > sessionStartTime)) continue;
1187
-
1188
1109
  const isReview = kind === 'review';
1189
1110
  if (!isReview) {
1190
1111
  if (isToolTrackedCommentId(comment.id)) {
@@ -1223,7 +1144,6 @@ export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, p
1223
1144
  return true;
1224
1145
  }
1225
1146
  }
1226
-
1227
1147
  // Check PR review comments (inline code comments)
1228
1148
  const reviewCommentsResult = await $(QUIET_PROBE)`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
1229
1149
  if (reviewCommentsResult.code === 0) {
@@ -1248,7 +1168,6 @@ export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, p
1248
1168
  }
1249
1169
  }
1250
1170
  }
1251
-
1252
1171
  return false;
1253
1172
  } catch (error) {
1254
1173
  // On error, default to not attaching (safer choice)
@@ -1256,7 +1175,6 @@ export const checkForAiCreatedComments = async (sessionStartTime, owner, repo, p
1256
1175
  return false;
1257
1176
  }
1258
1177
  };
1259
-
1260
1178
  /**
1261
1179
  * Attach the AI's working session summary as a comment to the PR or issue.
1262
1180
  * The summary is extracted from the tool's result field and posted
@@ -1295,7 +1213,6 @@ export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumb
1295
1213
  await log('āš ļø No working session summary available to attach', { verbose: true });
1296
1214
  return false;
1297
1215
  }
1298
-
1299
1216
  const targetNumber = prNumber || issueNumber;
1300
1217
  const targetType = prNumber ? 'pr' : 'issue';
1301
1218
 
@@ -1303,13 +1220,12 @@ export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumb
1303
1220
  await log('āš ļø No PR or issue number to attach working session summary to', { verbose: true });
1304
1221
  return false;
1305
1222
  }
1306
-
1307
1223
  try {
1308
1224
  // Issue #2119: publish what the session actually produced. The reported
1309
1225
  // summary said "The `pwd` command completed" and printed the solver's own
1310
1226
  // /tmp workspace, on a pull request that was still empty.
1311
1227
  const noChangesNotice = buildNoChangesNotice(changeStats);
1312
- const summaryBody = redactWorkspacePaths(resultSummary);
1228
+ const summaryBody = formatWorkingSessionSummaryMarkdown(redactWorkspacePaths(resultSummary));
1313
1229
 
1314
1230
  const comment = `${toolComments.WORKING_SESSION_SUMMARY_AUTOMATION_MARKER}
1315
1231
  ## ${toolComments.WORKING_SESSION_SUMMARY_MARKER}
@@ -1320,7 +1236,6 @@ ${summaryBody}${noChangesNotice ? `\n\n${noChangesNotice}` : ''}
1320
1236
  *${toolComments.WORKING_SESSION_SUMMARY_AUTOMATED_FOOTER}*`;
1321
1237
 
1322
1238
  const { ok, commentId, stderr } = await postTrackedComment({ $, owner, repo, targetNumber, body: comment });
1323
-
1324
1239
  if (ok) {
1325
1240
  await log(`āœ… Working session summary attached to ${targetType} #${targetNumber}${commentId ? ` (id=${commentId})` : ''}`);
1326
1241
  return true;
@@ -1375,10 +1290,8 @@ export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, wo
1375
1290
  if (!success) {
1376
1291
  return { attached: false, reason: 'iteration_failed' };
1377
1292
  }
1378
-
1379
1293
  const attachFlag = argv && (argv.attachSolutionSummary || argv['attach-solution-summary']);
1380
1294
  const autoAttachFlag = argv && (argv.autoAttachSolutionSummary || argv['auto-attach-solution-summary']);
1381
-
1382
1295
  if (!attachFlag && !autoAttachFlag) {
1383
1296
  return { attached: false, reason: 'flag_disabled' };
1384
1297
  }
@@ -1387,7 +1300,6 @@ export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, wo
1387
1300
  await log('ā„¹ļø No working session summary available from AI tool output', { verbose: true });
1388
1301
  return { attached: false, reason: 'no_result_summary' };
1389
1302
  }
1390
-
1391
1303
  let shouldAttach = false;
1392
1304
  if (attachFlag) {
1393
1305
  shouldAttach = true;
@@ -1406,7 +1318,6 @@ export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, wo
1406
1318
  if (!shouldAttach) {
1407
1319
  return { attached: false, reason: 'no_attach_decision' };
1408
1320
  }
1409
-
1410
1321
  const resolvedBudgetStatsData =
1411
1322
  budgetStatsData ??
1412
1323
  (sessionUsage
@@ -1419,7 +1330,6 @@ export const maybeAttachWorkingSessionSummary = async ({ argv, resultSummary, wo
1419
1330
  // Issue #2119: a summary posted on a pull request that changed nothing must
1420
1331
  // say so, instead of reading as a report of completed work.
1421
1332
  const changeStats = prNumber ? await getPullRequestChangeStats({ owner, repo, prNumber, $, log }) : null;
1422
-
1423
1333
  // Issue #2132: the summary carries no cost/budget block. `resolvedBudgetStatsData`
1424
1334
  // is computed only so the caller can reuse it for this session's log comment.
1425
1335
  const ok = await attachSolutionSummary({