@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
package/src/solve.mjs CHANGED
@@ -5,7 +5,6 @@ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
5
5
  const earlyArgs = process.argv.slice(2);
6
6
  const { handleSolveEarlyExit } = await import('./solve.bootstrap.lib.mjs');
7
7
  await handleSolveEarlyExit(earlyArgs);
8
-
9
8
  const use = (globalThis.use = await ensureUseM());
10
9
  const { $: __rawDollar$ } = await use('command-stream');
11
10
  const { configureGitHubRateLimitLogging, wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
@@ -63,18 +62,16 @@ const { startWorkSession, endWorkSession, SESSION_TYPES } = await import('./solv
63
62
  const { attachFinalLogIfMissing } = await import('./attach-logs-guarantee.lib.mjs'); // Issue #1952
64
63
  const { collectAndCommitDevelopmentLogArtifacts, fetchIssueType, isDevelopmentLogEnabled, isIssueTypeAwarePromptEnabled } = await import('./development-log.lib.mjs');
65
64
  const { createDevelopmentLogFinalizer } = await import('./development-log.finalize.lib.mjs');
66
- // Issue #1625: centralized markers + tracked comment posting for solve.mjs's
67
- // own usage-limit notifications (so they're excluded from the
68
- // "did the AI post anything?" check in --auto-attach-solution-summary).
65
+ // Issue #1625: centralized markers + tracked comment posting for solve.mjs's own usage-limit notifications (so they're excluded from the "did the AI post anything?" check in --auto-attach-solution-summary).
69
66
  const { postTrackedComment, USAGE_LIMIT_REACHED_MARKER } = await import('./tool-comments.lib.mjs');
70
67
  const { prepareFeedbackAndTimestamps, checkUncommittedChanges, checkForkActions } = await import('./solve.preparation.lib.mjs');
71
68
  const { validateAndExitOnInvalidClaudeSubAgentModel, validateAndExitOnInvalidModel } = await import('./models/index.mjs');
72
69
  const { autoAcceptInviteForRepo } = await import('./solve.accept-invite.lib.mjs');
73
70
  const { handleAutoForkOption, handleMaintainerForkAccess } = await import('./solve.fork-detection.lib.mjs');
71
+ const { resolveUncommittedChangesTool } = await import('./solve.tool-uncommitted.lib.mjs');
74
72
  const logFile = await initializeLogFile(null);
75
73
  const versionInfo = await getVersionInfo();
76
74
  const rawCommand = await logSolveStartup(versionInfo);
77
-
78
75
  let finalResourceSnapshotRecorded = false;
79
76
  const safeExit = async (code = 0, reason = 'Process completed', options = {}) => {
80
77
  if (!finalResourceSnapshotRecorded) {
@@ -83,7 +80,6 @@ const safeExit = async (code = 0, reason = 'Process completed', options = {}) =>
83
80
  }
84
81
  return await baseSafeExit(code, reason, options);
85
82
  };
86
-
87
83
  let argv;
88
84
  try {
89
85
  argv = await parseArguments(yargs, hideBin);
@@ -95,7 +91,6 @@ try {
95
91
  await safeExit(1, 'Invalid command-line arguments');
96
92
  }
97
93
  global.verboseMode = argv.verbose;
98
-
99
94
  setupVerboseLogInterceptor(); // Issue #1466: capture [VERBOSE] output in log files
100
95
  setupStdioLogInterceptor(); // Issue #1549: capture ALL terminal output in log file
101
96
  configureGitHubRateLimitLogging({
@@ -103,33 +98,8 @@ configureGitHubRateLimitLogging({
103
98
  log,
104
99
  });
105
100
  await recordResourceSnapshot({ phase: RESOURCE_PHASE_SOLVE_START, log, diskPath: '/', label: 'solve start', logExecutionContext: true }); // #2001: detect+report container context
106
-
107
- // Early logs go to cwd; custom log dir takes effect after argv is parsed
108
- // Conditionally import tool-specific functions after argv is parsed
109
- // If --use-agent-commander is enabled, use agent-commander's checkForUncommittedChanges
110
- let checkForUncommittedChanges;
111
- let agentCommanderLib = null;
112
- if (argv.useAgentCommander) {
113
- agentCommanderLib = await import('./agent-commander.lib.mjs');
114
- checkForUncommittedChanges = agentCommanderLib.checkForUncommittedChanges;
115
- } else if (argv.tool === 'opencode') {
116
- const opencodeLib = await import('./opencode.lib.mjs');
117
- checkForUncommittedChanges = opencodeLib.checkForUncommittedChanges;
118
- } else if (argv.tool === 'gemini') {
119
- const geminiLib = await import('./gemini.lib.mjs');
120
- checkForUncommittedChanges = geminiLib.checkForUncommittedChanges;
121
- } else if (argv.tool === 'codex') {
122
- const codexLib = await import('./codex.lib.mjs');
123
- checkForUncommittedChanges = codexLib.checkForUncommittedChanges;
124
- } else if (argv.tool === 'agent') {
125
- const agentLib = await import('./agent.lib.mjs');
126
- checkForUncommittedChanges = agentLib.checkForUncommittedChanges;
127
- } else if (argv.tool === 'qwen') {
128
- const qwenLib = await import('./qwen.lib.mjs');
129
- checkForUncommittedChanges = qwenLib.checkForUncommittedChanges;
130
- } else {
131
- checkForUncommittedChanges = claudeLib.checkForUncommittedChanges;
132
- }
101
+ // Early logs go to cwd; custom log dir takes effect after argv is parsed.
102
+ let { checkForUncommittedChanges, agentCommanderLib } = await resolveUncommittedChangesTool({ argv, claudeLib });
133
103
  const shouldAttachLogs = argv.attachLogs || argv['attach-logs'];
134
104
  await showAttachLogsWarning(shouldAttachLogs);
135
105
  const absoluteLogPath = path.resolve(logFile);
@@ -161,10 +131,7 @@ const cleanupWrapper = async () => {
161
131
  const interruptWrapper = createInterruptWrapper({ cleanupContext, checkForUncommittedChanges, shouldAttachLogs, attachLogToGitHub, getLogFile, sanitizeLogContent, $, log });
162
132
  initializeExitHandler(getAbsoluteLogPath, log, cleanupWrapper, interruptWrapper, ({ code, reason, failureActionSection }) => notifyIssueAboutPrePullRequestFailure({ code, reason, failureActionSection, argv, globalState: global, $, log, getLogFile, shouldAttachLogs, attachLogToGitHub, sanitizeLogContent, rawCommand }));
163
133
  installGlobalExitHandlers({ handleProcessErrors: false }); // #2117: solve's richer process-error handlers below must not race a duplicate pair.
164
- // Issue #1823: Configure the working-session guard. When the experimental
165
- // --do-not-shutdown-in-the-middle-of-working-session flag is set (hive passes it to every
166
- // worker), an interrupt received during an AI working session is deferred: solve lets the AI
167
- // finish, auto-commits, then shuts down gracefully instead of aborting the AI tool mid-run.
134
+ // Issue #1823: Configure the working-session guard. When the experimental --do-not-shutdown-in-the-middle-of-working-session flag is set (hive passes it to every worker), an interrupt received during an AI working session is deferred: solve lets the AI finish, auto-commits, then shuts down gracefully instead of aborting the AI tool mid-run.
168
135
  configureWorkingSession({ enabled: argv['do-not-shutdown-in-the-middle-of-working-session'] === true, log });
169
136
  const markFailureNotificationPosted = targetType => {
170
137
  global.preExitFailureNotificationPosted = true;
@@ -174,7 +141,6 @@ const markFailureNotificationPosted = targetType => {
174
141
  global.prePullRequestFailureNotificationPosted = true;
175
142
  }
176
143
  };
177
-
178
144
  // Now handle argument validation that was moved from early checks
179
145
  let issueUrl = argv['issue-url'] || argv._[0];
180
146
  if (!issueUrl) {
@@ -192,8 +158,7 @@ const { isIssueUrl, isPrUrl, normalizedUrl, owner, repo, number: urlNumber } = u
192
158
  issueUrl = normalizedUrl || issueUrl;
193
159
  global.owner = owner;
194
160
  global.repo = repo;
195
- // Issue #1752: record the source issue as soon as the URL is validated so the pre-exit
196
- // notifier can still comment on it if a check fails before normal issue-mode setup below.
161
+ // Issue #1752: record the source issue as soon as the URL is validated so the pre-exit notifier can still comment on it if a check fails before normal issue-mode setup below.
197
162
  if (isIssueUrl) {
198
163
  global.issueNumber = urlNumber;
199
164
  }
@@ -235,13 +200,11 @@ if (!(await validateUrlRequirement(issueUrl))) {
235
200
  if (!(await validateContinueOnlyOnFeedback(argv, isPrUrl, isIssueUrl))) {
236
201
  await safeExit(1, 'Feedback validation failed');
237
202
  }
238
-
239
203
  // Validate model name EARLY - always runs regardless of --skip-tool-connection-check
240
204
  const tool = argv.tool || 'claude';
241
205
  await validateAndExitOnInvalidModel(argv.model, tool, safeExit);
242
206
  if (argv.fallbackModel) await validateAndExitOnInvalidModel(argv.fallbackModel, tool, safeExit);
243
207
  argv.originalModel ||= argv.model;
244
-
245
208
  // Validate --plan-model if provided (Issue #1223)
246
209
  if (argv.planModel) {
247
210
  if (tool !== 'claude') {
@@ -251,7 +214,6 @@ if (argv.planModel) {
251
214
  await validateAndExitOnInvalidModel(argv.planModel, tool, safeExit);
252
215
  }
253
216
  if (argv.subAgentModel) await validateAndExitOnInvalidClaudeSubAgentModel(argv.subAgentModel, tool, safeExit);
254
-
255
217
  // Perform all system checks (skip tool connection check in dry-run or when --skip-tool-connection-check; model validation always runs)
256
218
  const prepareOnly = argv.dryRun || argv.onlyPrepareCommand;
257
219
  const skipToolConnectionCheck = prepareOnly || argv.skipToolConnectionCheck || argv.toolConnectionCheck === false;
@@ -277,8 +239,7 @@ if (argv.verbose) {
277
239
  await log(` Is PR URL: ${!!isPrUrl}`, { verbose: true });
278
240
  }
279
241
  const claudePath = argv.executeToolWithBun ? 'bunx claude' : process.env.CLAUDE_PATH || 'claude';
280
- // Note: owner, repo, and urlNumber are extracted from validateGitHubUrl() above
281
- // Accept pending invitation BEFORE any access checks (auto-fork, permissions, entity validation)
242
+ // Note: owner, repo, and urlNumber are extracted from validateGitHubUrl() above Accept pending invitation BEFORE any access checks (auto-fork, permissions, entity validation)
282
243
  if (argv.autoAcceptInvite) {
283
244
  await autoAcceptInviteForRepo(owner, repo, log, argv.verbose);
284
245
  }
@@ -290,27 +251,22 @@ const hasWriteAccess = await checkRepositoryWritePermission(owner, repo, {
290
251
  useFork: argv.fork,
291
252
  issueUrl: issueUrl,
292
253
  });
293
-
294
254
  if (!hasWriteAccess) {
295
255
  await log('');
296
256
  await log('❌ Cannot proceed without repository write access or --fork option', { level: 'error' });
297
257
  await safeExit(1, 'Permission check failed');
298
258
  }
299
-
300
259
  // Issue #1552: Validate entity existence AFTER permissions (cascade: user/org → repo → issue/PR)
301
260
  const entityCheck = await (await import('./github-entity-validation.lib.mjs')).validateGitHubEntityExistence({ owner, repo, number: urlNumber, type: isIssueUrl ? 'issue' : isPrUrl ? 'pull' : undefined, baseBranch: argv.baseBranch, verbose: argv.verbose, autoAcceptInvite: !!argv.autoAcceptInvite });
302
261
  if (!entityCheck.valid) {
303
262
  await log(`\n❌ ${entityCheck.error}\n`, { level: 'error' });
304
263
  await safeExit(1, `GitHub entity not found (${entityCheck.level})`);
305
264
  }
306
-
307
- // Detect repository visibility once and reuse for downstream decisions
308
- // (auto-cleanup default + Issue #1716 private-repo fork bypass)
265
+ // Detect repository visibility once and reuse for downstream decisions (auto-cleanup default + Issue #1716 private-repo fork bypass)
309
266
  const { detectRepositoryVisibility } = githubLib;
310
267
  const { isPublic: isRepoPublic } = await detectRepositoryVisibility(owner, repo);
311
268
  if (argv.autoCleanup === undefined) {
312
- // For public repos: keep temp directories (default false)
313
- // For private repos: clean up temp directories (default true)
269
+ // For public repos: keep temp directories (default false) For private repos: clean up temp directories (default true)
314
270
  argv.autoCleanup = !isRepoPublic;
315
271
  if (argv.verbose) {
316
272
  await log(` Auto-cleanup default: ${argv.autoCleanup} (repository is ${isRepoPublic ? 'public' : 'private'})`, {
@@ -318,11 +274,7 @@ if (argv.autoCleanup === undefined) {
318
274
  });
319
275
  }
320
276
  }
321
- // Issue #1716: When the upstream repository is private and the user has direct
322
- // write access, fork-based workflows should be skipped — even if the existing
323
- // PR was originally created from a fork. Forks of private repositories often
324
- // become inaccessible (renamed, deleted, parent re-private'd) and there's no
325
- // reason to use them when we can push branches and PRs to the upstream repo.
277
+ // Issue #1716: When the upstream repository is private and the user has direct write access, fork-based workflows should be skipped — even if the existing PR was originally created from a fork. Forks of private repositories often become inaccessible (renamed, deleted, parent re-private'd) and there's no reason to use them when we can push branches and PRs to the upstream repo.
326
278
  const skipForkForPrivateUpstream = !isRepoPublic && !argv.fork && hasWriteAccess;
327
279
  // Determine mode and get issue details
328
280
  let issueNumber;
@@ -376,7 +328,6 @@ if (autoContinueResult.isContinueMode) {
376
328
  await log(' Will clone fork repository for continue mode', { verbose: true });
377
329
  }
378
330
  }
379
-
380
331
  // Check if maintainer can push to the fork when --allow-to-push-to-contributors-pull-requests-as-maintainer is enabled
381
332
  if (forkOwner && argv.allowToPushToContributorsPullRequestsAsMaintainer && argv.autoFork) {
382
333
  await handleMaintainerForkAccess({ owner, repo, prNumber });
@@ -451,7 +402,6 @@ if (isPrUrl) {
451
402
  await log(' Will clone fork repository for continue mode', { verbose: true });
452
403
  }
453
404
  }
454
-
455
405
  // Check if maintainer can push to the fork when --allow-to-push-to-contributors-pull-requests-as-maintainer is enabled
456
406
  if (forkOwner && argv.allowToPushToContributorsPullRequestsAsMaintainer && argv.autoFork) {
457
407
  await handleMaintainerForkAccess({ owner, repo, prNumber });
@@ -503,8 +453,7 @@ const finalizeDevelopmentLog = createDevelopmentLogFinalizer({
503
453
  getParams: () => ({ enabled: isDevelopmentLogEnabled(argv), repositoryPath: tempDir, logFile: getLogFile(), issueNumber, prNumber, tool: argv.tool || 'claude', sessionId, branchName, rawCommand, $, log }), // prettier-ignore
504
454
  });
505
455
  try {
506
- // Set up repository and clone using the new module
507
- // If --working-directory points to existing repo, needsClone is false and we skip cloning
456
+ // Set up repository and clone using the new module If --working-directory points to existing repo, needsClone is false and we skip cloning
508
457
  const { forkedRepo } = await setupRepositoryAndClone({
509
458
  argv,
510
459
  owner,
@@ -519,12 +468,9 @@ try {
519
468
  $,
520
469
  needsClone,
521
470
  });
522
-
523
471
  cleanupContext.diskDiagnostics = { beforeBytes: await recordAfterCloneSize({ tempDir, log }) };
524
472
  await recordResourceSnapshot({ phase: RESOURCE_PHASE_AFTER_CLONE, log, diskPath: '/', label: 'after repository clone' });
525
-
526
- // Verify default branch and status using the new module
527
- // Pass argv, owner, repo, issueUrl for empty repository auto-initialization (--auto-init-repository)
473
+ // Verify default branch and status using the new module Pass argv, owner, repo, issueUrl for empty repository auto-initialization (--auto-init-repository)
528
474
  const defaultBranch = await verifyDefaultBranchAndStatus({
529
475
  tempDir,
530
476
  log,
@@ -552,7 +498,6 @@ try {
552
498
  prNumber,
553
499
  });
554
500
  cleanupContext.branchName = branchName;
555
-
556
501
  // Auto-merge default branch to pull request branch if enabled
557
502
  let autoMergeFeedbackLines = [];
558
503
  if (isContinueMode && argv['auto-merge-default-branch-to-pull-request-branch']) {
@@ -586,16 +531,13 @@ try {
586
531
  autoMergeFeedbackLines.push('');
587
532
  }
588
533
  }
589
-
590
534
  // Initialize PR variables early
591
535
  let prUrl = null;
592
-
593
536
  // In continue mode, we already have the PR details
594
537
  if (isContinueMode) {
595
538
  prUrl = issueUrl; // The input URL is the PR URL
596
539
  // prNumber is already set from earlier when we parsed the PR
597
540
  }
598
-
599
541
  // Handle auto PR creation using the new module
600
542
  const autoPrResult = await handleAutoPrCreation({
601
543
  argv,
@@ -615,7 +557,6 @@ try {
615
557
  path,
616
558
  fs,
617
559
  });
618
-
619
560
  let claudeCommitHash = null;
620
561
  if (autoPrResult) {
621
562
  prUrl = autoPrResult.prUrl;
@@ -627,17 +568,12 @@ try {
627
568
  }
628
569
  }
629
570
  if (prNumber) cleanupContext.prNumber = prNumber;
630
-
631
- // CRITICAL: Validate that we have a PR number when required
632
- // This prevents continuing without a PR when one was supposed to be created
571
+ // CRITICAL: Validate that we have a PR number when required This prevents continuing without a PR when one was supposed to be created
633
572
  if ((isContinueMode || argv.autoPullRequestCreation) && !prNumber) {
634
573
  await handleNoPrAvailableError({ isContinueMode, tempDir, issueNumber, issueUrl, owner, repo, log, formatAligned });
635
574
  }
636
-
637
575
  const enforceRequestedBaseBranch = () => ensurePullRequestBaseBranch({ owner, repo, prNumber, argv, log, formatAligned, $ });
638
-
639
576
  await enforceRequestedBaseBranch();
640
-
641
577
  if (isContinueMode) {
642
578
  await log(`\n${formatAligned('🔄', 'Continue mode:', 'ACTIVE')}`);
643
579
  await log(formatAligned('', 'Using existing PR:', `#${prNumber}`, 2));
@@ -646,10 +582,7 @@ try {
646
582
  await log(`\n${formatAligned('⏭️', 'Auto PR creation:', 'DISABLED')}`);
647
583
  await log(formatAligned('', 'Workflow:', 'AI will create the PR', 2));
648
584
  }
649
-
650
- // Start work session using the new module
651
- // Determine session type based on command line flags
652
- // See: https://github.com/link-assistant/hive-mind/issues/1152
585
+ // Start work session using the new module Determine session type based on command line flags See: https://github.com/link-assistant/hive-mind/issues/1152
653
586
  let sessionType = SESSION_TYPES.NEW;
654
587
  if (argv.sessionType) {
655
588
  // Session type was explicitly set (e.g., by auto-resume/auto-restart spawning a new process)
@@ -667,7 +600,6 @@ try {
667
600
  $,
668
601
  sessionType,
669
602
  });
670
-
671
603
  // Prepare feedback and timestamps using the new module
672
604
  const { feedbackLines: preparedFeedbackLines, referenceTime } = await prepareFeedbackAndTimestamps({
673
605
  prNumber,
@@ -685,10 +617,8 @@ try {
685
617
  tempDir,
686
618
  $,
687
619
  });
688
-
689
620
  // Initialize feedback lines
690
621
  let feedbackLines = null;
691
-
692
622
  // Add auto-merge feedback lines if any
693
623
  if (autoMergeFeedbackLines && autoMergeFeedbackLines.length > 0) {
694
624
  if (!feedbackLines) {
@@ -696,7 +626,6 @@ try {
696
626
  }
697
627
  feedbackLines.push(...autoMergeFeedbackLines);
698
628
  }
699
-
700
629
  // Merge feedback lines
701
630
  if (preparedFeedbackLines && preparedFeedbackLines.length > 0) {
702
631
  if (!feedbackLines) {
@@ -704,7 +633,6 @@ try {
704
633
  }
705
634
  feedbackLines.push(...preparedFeedbackLines);
706
635
  }
707
-
708
636
  // Check for uncommitted changes and merge with feedback
709
637
  const uncommittedFeedbackLines = await checkUncommittedChanges({
710
638
  tempDir,
@@ -718,7 +646,6 @@ try {
718
646
  }
719
647
  feedbackLines.push(...uncommittedFeedbackLines);
720
648
  }
721
-
722
649
  // Check for fork actions
723
650
  const forkActionsUrl = await checkForkActions({
724
651
  argv,
@@ -728,22 +655,16 @@ try {
728
655
  formatAligned,
729
656
  $,
730
657
  });
731
-
732
658
  // Execute tool command with all prompts and settings
733
659
  let toolResult;
734
-
735
- // Issue #1823: Mark the start of the AI working session. While this is active and the
736
- // --do-not-shutdown-in-the-middle-of-working-session flag is set, an interrupt (CTRL+C/SIGTERM)
737
- // is deferred until the AI tool finishes its turn (see exit-handler.lib.mjs + working-session.lib.mjs).
660
+ // Issue #1823: Mark the start of the AI working session. While this is active and the --do-not-shutdown-in-the-middle-of-working-session flag is set, an interrupt (CTRL+C/SIGTERM) is deferred until the AI tool finishes its turn (see exit-handler.lib.mjs + working-session.lib.mjs).
738
661
  beginWorkingSession();
739
-
740
662
  // If --use-agent-commander is enabled, use agent-commander for all tools
741
663
  if (argv.useAgentCommander) {
742
664
  // Ensure agent-commander is available
743
665
  if (!agentCommanderLib) {
744
666
  agentCommanderLib = await import('./agent-commander.lib.mjs');
745
667
  }
746
-
747
668
  const isAvailable = await agentCommanderLib.isAgentCommanderAvailable();
748
669
  if (!isAvailable) {
749
670
  await log('\n[agent-commander] agent-commander is not installed.', { level: 'error' });
@@ -751,9 +672,7 @@ try {
751
672
  await log(' Or remove the --use-agent-commander flag to use embedded tool logic.', { level: 'error' });
752
673
  await safeExit(1, 'agent-commander not available');
753
674
  }
754
-
755
675
  await log(`\n[agent-commander] Using agent-commander for ${argv.tool || 'claude'} execution`);
756
-
757
676
  toolResult = await agentCommanderLib.executeWithAgentCommander({
758
677
  issueUrl,
759
678
  issueNumber,
@@ -786,7 +705,6 @@ try {
786
705
  qwen: { lib: './qwen.lib.mjs', execFn: 'executeQwen', envVar: 'QWEN_PATH', defaultBin: 'qwen', pathKey: 'qwenPath' },
787
706
  }[argv.tool];
788
707
  const toolLib = await import(toolDispatch.lib);
789
-
790
708
  toolResult = await toolLib[toolDispatch.execFn]({
791
709
  issueUrl,
792
710
  issueNumber,
@@ -839,17 +757,13 @@ try {
839
757
  });
840
758
  toolResult = claudeResult;
841
759
  }
842
-
843
760
  try {
844
761
  await recordAfterAgentSize({ tempDir, beforeBytes: cleanupContext.diskDiagnostics?.beforeBytes ?? null, log });
845
762
  } catch (diskError) {
846
763
  await log(`⚠️ Disk-size measurement failed: ${cleanErrorMessage(diskError)}`, { level: 'warning', verbose: true });
847
764
  }
848
765
  await recordResourceSnapshot({ phase: RESOURCE_PHASE_AFTER_AGENT, log, diskPath: '/', label: 'after AI execution' });
849
-
850
- // Issue #1823: Mark the end of the AI working session. If a graceful-shutdown interrupt arrived
851
- // during the session (deferred by the working-session guard), honor it now: auto-commit any
852
- // uncommitted changes and exit gracefully — only AFTER the AI tool has fully finished its turn.
766
+ // Issue #1823: Mark the end of the AI working session. If a graceful-shutdown interrupt arrived during the session (deferred by the working-session guard), honor it now: auto-commit any uncommitted changes and exit gracefully — only AFTER the AI tool has fully finished its turn.
853
767
  const workingSessionState = endWorkingSession();
854
768
  if (workingSessionState.shutdownRequested) {
855
769
  const shutdownExitCode = workingSessionState.shutdownSignal === 'SIGINT' ? 130 : 143;
@@ -860,11 +774,9 @@ try {
860
774
  } catch (interruptError) {
861
775
  await log(`⚠️ Auto-commit on graceful shutdown failed: ${cleanErrorMessage(interruptError)}`, { level: 'warning' });
862
776
  }
863
- // Graceful shutdown is NOT a failure: skip the pre-exit failure notifier so no spurious
864
- // "solver failed" comment is posted (issue #1823: no errors on graceful shutdown).
777
+ // Graceful shutdown is NOT a failure: skip the pre-exit failure notifier so no spurious "solver failed" comment is posted (issue #1823: no errors on graceful shutdown).
865
778
  await safeExit(shutdownExitCode, 'Graceful shutdown after AI working session', { skipPreExit: true });
866
779
  }
867
-
868
780
  if (toolResult.preparedOnly) await safeExit(0, 'Command prepared', { skipPreExit: true });
869
781
  const { success } = toolResult;
870
782
  sessionId = toolResult.sessionId;
@@ -876,7 +788,6 @@ try {
876
788
  let resultModelUsage = toolResult.resultModelUsage || null;
877
789
  let streamTokenUsage = toolResult.streamTokenUsage || null;
878
790
  let subAgentCalls = toolResult.subAgentCalls || null; // Issue #1590
879
-
880
791
  const applyRestartResult = result => {
881
792
  if (!result) return;
882
793
  sessionId = result.sessionId || sessionId;
@@ -886,14 +797,12 @@ try {
886
797
  };
887
798
  limitReached = toolResult.limitReached;
888
799
  cleanupContext.limitReached = limitReached;
889
-
890
800
  if (sessionId && (argv.resumeOnAutoRestart || argv['resume-on-auto-restart'])) {
891
801
  global.previousSessionId = sessionId;
892
802
  if (argv.verbose) {
893
803
  await log(`Session ID stored for auto-restart resume: ${sessionId}`, { verbose: true });
894
804
  }
895
805
  }
896
-
897
806
  // Capture limit reset time and timezone globally for downstream handlers (auto-continue, cleanup decisions)
898
807
  if (toolResult && toolResult.limitResetTime) {
899
808
  global.limitResetTime = toolResult.limitResetTime;
@@ -901,20 +810,16 @@ try {
901
810
  if (toolResult && toolResult.limitTimezone) {
902
811
  global.limitTimezone = toolResult.limitTimezone;
903
812
  }
904
-
905
813
  // Handle limit reached scenario
906
814
  if (limitReached) {
907
- // Check for both auto-resume (maintains context) and auto-restart (fresh start)
908
- // See: https://github.com/link-assistant/hive-mind/issues/1152
815
+ // Check for both auto-resume (maintains context) and auto-restart (fresh start) See: https://github.com/link-assistant/hive-mind/issues/1152
909
816
  const shouldAutoResumeOnReset = argv.autoResumeOnLimitReset;
910
817
  const shouldAutoRestartOnReset = argv.autoRestartOnLimitReset;
911
818
  const shouldAutoContinueOnReset = shouldAutoResumeOnReset || shouldAutoRestartOnReset;
912
-
913
819
  // If limit was reached but neither auto-resume nor auto-restart is enabled, fail immediately
914
820
  if (!shouldAutoContinueOnReset) {
915
821
  await log('\n❌ USAGE LIMIT REACHED!');
916
822
  await log(' The AI tool has reached its usage limit.');
917
-
918
823
  // Always show manual resume command in console so users can resume after limit resets
919
824
  if (sessionId) {
920
825
  const resetTime = global.limitResetTime;
@@ -923,8 +828,7 @@ try {
923
828
  await log(`📁 Working directory: ${tempDir}`);
924
829
  await log(`📌 Session ID: ${sessionId}`);
925
830
  if (resetTime) {
926
- // Format reset time with relative time and UTC for better user understanding
927
- // See: https://github.com/link-assistant/hive-mind/issues/1152
831
+ // Format reset time with relative time and UTC for better user understanding See: https://github.com/link-assistant/hive-mind/issues/1152
928
832
  const formattedResetTime = formatResetTimeWithRelative(resetTime, timezone);
929
833
  await log(`⏰ Limit resets at: ${formattedResetTime}`);
930
834
  }
@@ -944,7 +848,6 @@ try {
944
848
  await log('');
945
849
  }
946
850
  }
947
-
948
851
  // If --attach-logs is enabled and we have a PR, attach logs with usage limit details
949
852
  if (shouldAttachLogs && sessionId && prNumber) {
950
853
  await log('\n📄 Attaching logs to Pull Request...');
@@ -972,7 +875,6 @@ try {
972
875
  tool: argv.tool || 'claude',
973
876
  resultModelUsage, // Issue #1454: accurate multi-model display
974
877
  });
975
-
976
878
  if (logUploadSuccess) {
977
879
  markFailureNotificationPosted('pr');
978
880
  await log(' ✅ Logs uploaded successfully');
@@ -988,15 +890,12 @@ try {
988
890
  // Fallback: Post simple failure comment (no CLI commands in GitHub comments, only mention option)
989
891
  try {
990
892
  const resetTime = global.limitResetTime;
991
- // Issue #942: do not embed CLI commands in GitHub comments. Users
992
- // interact via the Telegram bot, not the CLI. The full resume
993
- // commands (interactive/autonomous/solve) live in the attached logs.
893
+ // Issue #942: do not embed CLI commands in GitHub comments. Users interact via the Telegram bot, not the CLI. The full resume commands (interactive/autonomous/solve) live in the attached logs.
994
894
  const resumeSection = sessionId ? `Session ID: \`${sessionId}\`\n\nUse the \`--auto-resume-on-limit-reset\` or \`--auto-restart-on-limit-reset\` option to automatically resume when the limit resets.` : 'Use the `--auto-resume-on-limit-reset` or `--auto-restart-on-limit-reset` option to automatically resume when the limit resets.';
995
895
  // Format the reset time with relative time and UTC conversion if available
996
896
  const timezone = global.limitTimezone || null;
997
897
  const formattedResetTime = resetTime ? formatResetTimeWithRelative(resetTime, timezone) : null;
998
898
  const failureComment = formattedResetTime ? `❌ **${USAGE_LIMIT_REACHED_MARKER}**\n\nThe AI tool has reached its usage limit. The limit will reset at: **${formattedResetTime}**\n\n${resumeSection}` : `❌ **${USAGE_LIMIT_REACHED_MARKER}**\n\nThe AI tool has reached its usage limit. Please wait for the limit to reset.\n\n${resumeSection}`;
999
-
1000
899
  const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: failureComment });
1001
900
  if (posted.ok) {
1002
901
  markFailureNotificationPosted('pr');
@@ -1006,11 +905,9 @@ try {
1006
905
  await log(` Warning: Could not post failure comment: ${cleanErrorMessage(error)}`, { verbose: true });
1007
906
  }
1008
907
  }
1009
-
1010
908
  await safeExit(1, 'Usage limit reached - use --auto-resume-on-limit-reset or --auto-restart-on-limit-reset to wait for reset');
1011
909
  } else {
1012
- // auto-resume-on-limit-reset or auto-restart-on-limit-reset is enabled - attach logs and/or post waiting comment
1013
- // Determine the mode type for comment formatting
910
+ // auto-resume-on-limit-reset or auto-restart-on-limit-reset is enabled - attach logs and/or post waiting comment Determine the mode type for comment formatting
1014
911
  const limitContinueMode = shouldAutoRestartOnReset ? 'restart' : 'resume';
1015
912
  if (prNumber && global.limitResetTime) {
1016
913
  // If --attach-logs is enabled, upload logs with usage limit details
@@ -1035,8 +932,7 @@ try {
1035
932
  toolName: getToolDisplayName(argv.tool),
1036
933
  resumeCommand,
1037
934
  sessionId,
1038
- // Tell attachLogToGitHub that auto-resume is enabled to suppress CLI commands in the comment
1039
- // See: https://github.com/link-assistant/hive-mind/issues/1152
935
+ // Tell attachLogToGitHub that auto-resume is enabled to suppress CLI commands in the comment See: https://github.com/link-assistant/hive-mind/issues/1152
1040
936
  isAutoResumeEnabled: true,
1041
937
  autoResumeMode: limitContinueMode,
1042
938
  argv,
@@ -1044,7 +940,6 @@ try {
1044
940
  tool: argv.tool || 'claude',
1045
941
  resultModelUsage, // Issue #1454: accurate multi-model display
1046
942
  });
1047
-
1048
943
  if (logUploadSuccess) {
1049
944
  await log(' ✅ Logs uploaded successfully');
1050
945
  } else {
@@ -1062,7 +957,6 @@ try {
1062
957
  const validation = await import('./solve.validation.lib.mjs');
1063
958
  const { calculateWaitTime } = validation;
1064
959
  const waitMs = calculateWaitTime(global.limitResetTime, global.limitTimezone || null);
1065
-
1066
960
  const formatWaitTime = ms => {
1067
961
  const seconds = Math.floor(ms / 1000);
1068
962
  const minutes = Math.floor(seconds / 60);
@@ -1073,16 +967,12 @@ try {
1073
967
  const h = hours % 24;
1074
968
  return `${days}:${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
1075
969
  };
1076
-
1077
- // For waiting comments, don't show CLI commands since auto-continue will handle it automatically
1078
- // See: https://github.com/link-assistant/hive-mind/issues/1152
970
+ // For waiting comments, don't show CLI commands since auto-continue will handle it automatically See: https://github.com/link-assistant/hive-mind/issues/1152
1079
971
  const continueModeName = limitContinueMode === 'restart' ? 'auto-restart' : 'auto-resume';
1080
972
  const continueDescription = limitContinueMode === 'restart' ? 'The session will automatically restart (fresh start) when the limit resets.' : 'The session will automatically resume (with context preserved) when the limit resets.';
1081
- // Format reset time with relative time and UTC for better user understanding
1082
- // See: https://github.com/link-assistant/hive-mind/issues/1236
973
+ // Format reset time with relative time and UTC for better user understanding See: https://github.com/link-assistant/hive-mind/issues/1236
1083
974
  const waitingResetTimeFormatted = formatResetTimeWithRelative(global.limitResetTime, global.limitTimezone || null) || global.limitResetTime;
1084
975
  const waitingComment = `⏳ **${USAGE_LIMIT_REACHED_MARKER} - Waiting to ${limitContinueMode === 'restart' ? 'Restart' : 'Continue'}**\n\nThe AI tool has reached its usage limit. ${continueModeName} is enabled.\n\n**Reset time:** ${waitingResetTimeFormatted}\n**Wait time:** ${formatWaitTime(waitMs)} (days:hours:minutes:seconds)\n\n${continueDescription}\n\nSession ID: \`${sessionId}\``;
1085
-
1086
976
  const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: waitingComment });
1087
977
  if (posted.ok) {
1088
978
  await log(` Posted waiting comment to PR${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
@@ -1094,14 +984,10 @@ try {
1094
984
  }
1095
985
  }
1096
986
  }
1097
-
1098
987
  // Skip failure exit if limit reached with auto-resume (continues to showSessionSummary/autoContinueWhenLimitResets)
1099
988
  const shouldSkipFailureExitForAutoLimitContinue = limitReached && argv.autoResumeOnLimitReset;
1100
989
  if (!success && !shouldSkipFailureExitForAutoLimitContinue) {
1101
- // Issue #942: show all three resume options on failure for richer guidance.
1102
- // 1. Interactive claude - opens Claude Code interactively (claude only)
1103
- // 2. Autonomous claude - one-shot claude --resume w/ --dangerously-skip-permissions -p (claude only)
1104
- // 3. Solve resume - re-enters solve.mjs with --resume, preserving tool/model/dir
990
+ // Issue #942: show all three resume options on failure for richer guidance. 1. Interactive claude - opens Claude Code interactively (claude only) 2. Autonomous claude - one-shot claude --resume w/ --dangerously-skip-permissions -p (claude only) 3. Solve resume - re-enters solve.mjs with --resume, preserving tool/model/dir
1105
991
  const toolForFailure = argv.tool || 'claude';
1106
992
  // Issue #1845: surface the core error instead of just "<TOOL> execution failed" (terminal + comment).
1107
993
  const toolFailureMessage = formatToolExecutionFailure({ tool: toolForFailure, toolResult });
@@ -1118,7 +1004,6 @@ try {
1118
1004
  }
1119
1005
  await log('');
1120
1006
  }
1121
-
1122
1007
  // Preserve work before remote diagnostics; issue #2101 ended during log upload.
1123
1008
  try {
1124
1009
  const { criticalErrorRecovery } = await import('./config.lib.mjs');
@@ -1129,14 +1014,12 @@ try {
1129
1014
  } catch (preserveError) {
1130
1015
  await log(` ⚠️ Could not auto-commit before failure exit: ${preserveError.message}`, { verbose: true });
1131
1016
  }
1132
-
1133
1017
  // Attach failure logs before exiting (Issues #1212, #1462: fall back to issue if no PR)
1134
1018
  const hasPR = global.createdPR && global.createdPR.number;
1135
1019
  const hasIssue = global.issueNumber;
1136
1020
  const logTargetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
1137
1021
  const logTargetNumber = hasPR ? global.createdPR.number : hasIssue ? global.issueNumber : null;
1138
1022
  const logTargetLabel = hasPR ? 'Pull Request' : `original issue #${logTargetNumber}`;
1139
-
1140
1023
  if (shouldAttachLogs && logTargetType && logTargetNumber) {
1141
1024
  await log(`\n📄 Attaching failure logs to ${logTargetLabel}...`);
1142
1025
  try {
@@ -1166,7 +1049,6 @@ try {
1166
1049
  tool: argv.tool || 'claude',
1167
1050
  resultModelUsage, // Issue #1454: accurate multi-model display
1168
1051
  });
1169
-
1170
1052
  if (logUploadSuccess) {
1171
1053
  markFailureNotificationPosted(logTargetType);
1172
1054
  await log(` 📎 Failure logs posted to ${logTargetLabel}`);
@@ -1179,10 +1061,8 @@ try {
1179
1061
  await log(` ⚠️ Error uploading failure logs: ${uploadError.message}`);
1180
1062
  }
1181
1063
  }
1182
-
1183
1064
  await safeExit(1, toolFailureMessage);
1184
1065
  }
1185
-
1186
1066
  // Clean up .playwright-mcp/ to prevent browser artifacts from triggering auto-restart (Issue #1124)
1187
1067
  if (argv.playwrightMcpAutoCleanup !== false) {
1188
1068
  const playwrightMcpDir = path.join(tempDir, '.playwright-mcp');
@@ -1202,30 +1082,19 @@ try {
1202
1082
  } else {
1203
1083
  await log('ℹ️ Playwright MCP auto-cleanup disabled via --no-playwright-mcp-auto-cleanup', { verbose: true });
1204
1084
  }
1205
-
1206
- // When limit is reached, force auto-commit of any uncommitted changes to preserve work.
1207
- // Issue #1834 (PR #1835 feedback): "on all critical errors we auto commit uncommitted changes by
1208
- // default." A failed/errored session is a critical error, so auto-commit (and push) to preserve any
1209
- // work the agent left on disk. On by default; disable via HIVE_MIND_AUTO_COMMIT_ON_CRITICAL_ERROR=false.
1085
+ // When limit is reached, force auto-commit of any uncommitted changes to preserve work. Issue #1834 (PR #1835 feedback): "on all critical errors we auto commit uncommitted changes by default." A failed/errored session is a critical error, so auto-commit (and push) to preserve any work the agent left on disk. On by default; disable via HIVE_MIND_AUTO_COMMIT_ON_CRITICAL_ERROR=false.
1210
1086
  const { criticalErrorRecovery } = await import('./config.lib.mjs');
1211
1087
  const criticalError = success === false || errorDuringExecution === true;
1212
1088
  const shouldAutoCommit = argv['auto-commit-uncommitted-changes'] || limitReached || (criticalError && criticalErrorRecovery.autoCommitUncommittedChanges);
1213
1089
  const autoRestartEnabled = argv['autoRestartOnUncommittedChanges'] !== false;
1214
1090
  const shouldRestart = await checkForUncommittedChanges(tempDir, owner, repo, branchName, $, log, shouldAutoCommit, autoRestartEnabled);
1215
-
1216
- // Issue #1516: cleanupClaudeFile() moved to after completion signals (before endWorkSession)
1217
-
1218
- // Show summary of session and log file
1091
+ // Issue #1516: cleanupClaudeFile() moved to after completion signals (before endWorkSession) Show summary of session and log file
1219
1092
  await showSessionSummary(sessionId, limitReached, argv, issueUrl, tempDir, shouldAttachLogs);
1220
-
1221
- // Issue #1571: Defense-in-depth guard — skip post-processing if auto-continue is handling it
1222
- // (prevents "Solution Draft Log" / "Ready to merge" comments before "Auto Resume")
1093
+ // Issue #1571: Defense-in-depth guard — skip post-processing if auto-continue is handling it (prevents "Solution Draft Log" / "Ready to merge" comments before "Auto Resume")
1223
1094
  if (limitReached && (argv.autoResumeOnLimitReset || argv.autoRestartOnLimitReset) && global.limitResetTime) {
1224
1095
  await safeExit(0, 'Auto-continue child process will handle post-processing');
1225
1096
  }
1226
-
1227
1097
  await enforceRequestedBaseBranch();
1228
-
1229
1098
  // 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
1099
  await finalizeDevelopmentLog();
1231
1100
  // Issue #1263 / #1728 / #2115: shared summary attachment and usage display.
@@ -1241,25 +1110,20 @@ try {
1241
1110
  pricingInfo,
1242
1111
  sessionUsage: { sessionId, tempDir, resultModelUsage, streamTokenUsage, subAgentCalls },
1243
1112
  });
1244
-
1245
1113
  // Search for newly created pull requests and comments
1246
1114
  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);
1247
1115
  const logsAlreadyUploaded = verifyResult?.logUploadSuccess || false;
1248
-
1249
1116
  // Issue #1162: Auto-restart when PR title/description still has placeholder content
1250
1117
  if (argv.autoRestartOnNonUpdatedPullRequestDescription && (verifyResult?.prTitleHasPlaceholder || verifyResult?.prBodyHasPlaceholder)) {
1251
1118
  const { buildPRNotUpdatedHint } = results;
1252
1119
  const hintLines = buildPRNotUpdatedHint(verifyResult.prTitleHasPlaceholder, verifyResult.prBodyHasPlaceholder);
1253
-
1254
1120
  await log('');
1255
1121
  await log('🔄 AUTO-RESTART: PR title/description not updated by agent');
1256
1122
  hintLines.forEach(async line => await log(` ${line}`));
1257
1123
  await log(' Restarting tool to give agent another chance to update...');
1258
1124
  await log('');
1259
-
1260
1125
  // Import executeToolIteration for re-execution
1261
1126
  const { executeToolIteration } = await import('./solve.restart-shared.lib.mjs');
1262
-
1263
1127
  // Re-execute tool with hint as feedback lines
1264
1128
  const restartResult = await executeToolIteration({
1265
1129
  issueUrl,
@@ -1278,16 +1142,12 @@ try {
1278
1142
  autoRestartOnNonUpdatedPullRequestDescription: false,
1279
1143
  },
1280
1144
  });
1281
-
1282
1145
  // Update session data from restart
1283
1146
  applyRestartResult(restartResult);
1284
-
1285
1147
  // Clean up CLAUDE.md/.gitkeep again after restart
1286
1148
  await cleanupClaudeFile(tempDir, branchName, null, argv);
1287
-
1288
1149
  // Re-verify results after restart (without auto-restart flag to prevent recursion)
1289
1150
  const reVerifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, { ...argv, autoRestartOnNonUpdatedPullRequestDescription: false }, shouldAttachLogs, false, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution, sessionType, resultModelUsage, streamTokenUsage, subAgentCalls);
1290
-
1291
1151
  if (reVerifyResult?.prTitleHasPlaceholder || reVerifyResult?.prBodyHasPlaceholder) {
1292
1152
  await log('⚠️ PR title/description still not updated after restart');
1293
1153
  }
@@ -1296,7 +1156,6 @@ try {
1296
1156
  applyRestartResult(await runEscalation({ issueUrl, owner, repo, issueNumber, prNumber, branchName, tempDir, workspaceTmpDir, argv, cleanupClaudeFile, resultSummary }));
1297
1157
  applyRestartResult(await runAutoEnsureRequirements({ issueUrl, owner, repo, issueNumber, prNumber, branchName, tempDir, argv, cleanupClaudeFile }));
1298
1158
  applyRestartResult(await runKeepWorkingUntilDone({ issueUrl, owner, repo, issueNumber, prNumber, branchName, tempDir, workspaceTmpDir, argv, cleanupClaudeFile, resultSummary }));
1299
-
1300
1159
  // Start watch mode if enabled OR if we need to handle uncommitted changes
1301
1160
  if (argv.verbose) {
1302
1161
  await log('');
@@ -1309,7 +1168,6 @@ try {
1309
1168
  await log(` branchName: ${branchName}`, { verbose: true });
1310
1169
  await log(` isContinueMode: ${isContinueMode}`, { verbose: true });
1311
1170
  }
1312
-
1313
1171
  // If uncommitted changes detected and auto-commit is disabled, enter temporary watch mode
1314
1172
  const temporaryWatchMode = shouldRestart && !argv.watch;
1315
1173
  if (temporaryWatchMode) {
@@ -1320,7 +1178,6 @@ try {
1320
1178
  await log(' Will exit automatically after changes are committed or discarded');
1321
1179
  await log('');
1322
1180
  }
1323
-
1324
1181
  const watchResult = await startWatchMode({
1325
1182
  issueUrl,
1326
1183
  owner,
@@ -1338,7 +1195,6 @@ try {
1338
1195
  temporaryWatch: temporaryWatchMode, // Flag to indicate temporary watch mode
1339
1196
  },
1340
1197
  });
1341
-
1342
1198
  if (watchResult && watchResult.latestSessionId) {
1343
1199
  sessionId = watchResult.latestSessionId;
1344
1200
  anthropicTotalCostUSD = watchResult.latestAnthropicCost;
@@ -1351,19 +1207,14 @@ try {
1351
1207
  }
1352
1208
  }
1353
1209
  }
1354
-
1355
1210
  await enforceRequestedBaseBranch();
1356
-
1357
1211
  // Track whether logs were successfully attached (used by endWorkSession)
1358
1212
  let logsAttached = false;
1359
-
1360
- // After watch mode completes (either user watch or temporary)
1361
- // Push any committed changes if this was a temporary watch mode
1213
+ // After watch mode completes (either user watch or temporary) Push any committed changes if this was a temporary watch mode
1362
1214
  if (temporaryWatchMode) {
1363
1215
  await log('');
1364
1216
  await log('📤 Pushing committed changes to GitHub...');
1365
1217
  await log('');
1366
-
1367
1218
  try {
1368
1219
  const pushResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
1369
1220
  if (pushResult.code === 0) {
@@ -1383,13 +1234,8 @@ try {
1383
1234
  await log(' Please push manually:', { level: 'error' });
1384
1235
  await log(` cd ${tempDir} && git push origin ${branchName}`, { level: 'error' });
1385
1236
  }
1386
-
1387
1237
  await verifyPullRequestIssueLinkAfterAutoRestart({ prNumber, issueNumber, owner, repo, argv, cleanErrorMessage });
1388
-
1389
- // Attach updated logs to PR after auto-restart completes
1390
- // Issue #1154: Skip if logs were already uploaded by verifyResults() to prevent duplicates
1391
- // Issue #1290: Always upload if auto-restart ran but last iteration's logs weren't uploaded
1392
- // This ensures final logs are uploaded even when the last iteration failed
1238
+ // Attach updated logs to PR after auto-restart completes Issue #1154: Skip if logs were already uploaded by verifyResults() to prevent duplicates Issue #1290: Always upload if auto-restart ran but last iteration's logs weren't uploaded This ensures final logs are uploaded even when the last iteration failed
1393
1239
  const autoRestartRanButNotUploaded = watchResult?.autoRestartIterationsRan && !watchResult?.lastIterationLogUploaded;
1394
1240
  if (shouldAttachLogs && prNumber && (!logsAlreadyUploaded || autoRestartRanButNotUploaded)) {
1395
1241
  await log('📎 Uploading working session logs to Pull Request...');
@@ -1412,7 +1258,6 @@ try {
1412
1258
  tool: argv.tool || 'claude',
1413
1259
  resultModelUsage, // Issue #1454: accurate multi-model display
1414
1260
  });
1415
-
1416
1261
  if (logUploadSuccess) {
1417
1262
  await log('✅ Working session logs uploaded successfully');
1418
1263
  logsAttached = true;
@@ -1427,10 +1272,7 @@ try {
1427
1272
  logsAttached = true;
1428
1273
  }
1429
1274
  }
1430
-
1431
- // Start auto-restart-until-mergeable mode if enabled
1432
- // This runs after the normal watch mode completes (if any)
1433
- // --auto-merge implies --auto-restart-until-mergeable
1275
+ // Start auto-restart-until-mergeable mode if enabled This runs after the normal watch mode completes (if any) --auto-merge implies --auto-restart-until-mergeable
1434
1276
  if (argv.autoMerge || argv.autoRestartUntilMergeable) {
1435
1277
  const autoMergeResult = await startAutoRestartUntilMergeable({
1436
1278
  issueUrl,
@@ -1443,7 +1285,6 @@ try {
1443
1285
  tempDir,
1444
1286
  argv,
1445
1287
  });
1446
-
1447
1288
  // Update session data with latest from auto-merge mode for accurate pricing
1448
1289
  if (autoMergeResult && autoMergeResult.latestSessionId) {
1449
1290
  sessionId = autoMergeResult.latestSessionId;
@@ -1458,13 +1299,10 @@ try {
1458
1299
  }
1459
1300
  }
1460
1301
  }
1461
-
1462
1302
  // Issue #1952: Final --attach-logs safety net + logsAttached reconciliation. See attach-logs-guarantee.lib.mjs.
1463
1303
  logsAttached = (await attachFinalLogIfMissing({ shouldAttachLogs, prNumber, owner, repo, $, log, sanitizeLogContent, getLogFile, attachLogToGitHub, argv, sessionId, tempDir, anthropicTotalCostUSD, resultModelUsage })) || logsAttached;
1464
-
1465
1304
  // Issue #1516: Cleanup after all signals (was before verifyResults, caused premature commits)
1466
1305
  await cleanupClaudeFile(tempDir, branchName, claudeCommitHash, argv);
1467
-
1468
1306
  await finalizeDevelopmentLog(); // Issue #1596/#2048: idempotent no-op on the success path (already committed before readiness signal); still preserves late/error work.
1469
1307
  await endWorkSession({ isContinueMode, prNumber, argv, log, formatAligned, $, logsAttached });
1470
1308
  } catch (error) {