maistro 1.0.402 → 1.0.410

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 (49) hide show
  1. package/README.md +90 -53
  2. package/dist/apiKeyValidator.d.ts +58 -0
  3. package/dist/apiKeyValidator.d.ts.map +1 -0
  4. package/dist/apiKeyValidator.js +235 -0
  5. package/dist/apiKeyValidator.js.map +1 -0
  6. package/dist/app.d.ts +13 -2
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/app.js +191 -61
  9. package/dist/app.js.map +1 -1
  10. package/dist/config.d.ts +2 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js.map +1 -1
  13. package/dist/dependencyDetector.d.ts +3 -1
  14. package/dist/dependencyDetector.d.ts.map +1 -1
  15. package/dist/dependencyDetector.js +103 -4
  16. package/dist/dependencyDetector.js.map +1 -1
  17. package/dist/executor.d.ts.map +1 -1
  18. package/dist/executor.js +13 -14
  19. package/dist/executor.js.map +1 -1
  20. package/dist/git.d.ts +7 -0
  21. package/dist/git.d.ts.map +1 -1
  22. package/dist/git.js +34 -0
  23. package/dist/git.js.map +1 -1
  24. package/dist/logger.d.ts +7 -10
  25. package/dist/logger.d.ts.map +1 -1
  26. package/dist/logger.js +45 -24
  27. package/dist/logger.js.map +1 -1
  28. package/dist/logo.d.ts +20 -0
  29. package/dist/logo.d.ts.map +1 -0
  30. package/dist/logo.js +57 -0
  31. package/dist/logo.js.map +1 -0
  32. package/dist/orchestrator.d.ts +1 -1
  33. package/dist/orchestrator.d.ts.map +1 -1
  34. package/dist/orchestrator.js +38 -6
  35. package/dist/orchestrator.js.map +1 -1
  36. package/dist/planner.d.ts.map +1 -1
  37. package/dist/planner.js +0 -3
  38. package/dist/planner.js.map +1 -1
  39. package/dist/screen.d.ts +1 -1
  40. package/dist/screen.d.ts.map +1 -1
  41. package/dist/screen.js +3 -22
  42. package/dist/screen.js.map +1 -1
  43. package/dist/types.d.ts +17 -17
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/validator.d.ts +6 -34
  46. package/dist/validator.d.ts.map +1 -1
  47. package/dist/validator.js +256 -344
  48. package/dist/validator.js.map +1 -1
  49. package/package.json +2 -2
package/dist/app.js CHANGED
@@ -2,20 +2,22 @@ import { resolve, basename, extname, join } from 'node:path';
2
2
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { homedir } from 'node:os';
5
- import { exec } from 'node:child_process';
5
+ import { exec, spawn } from 'node:child_process';
6
6
  import { Orchestrator } from './orchestrator.js';
7
7
  import { InteractiveUI, formatTaskStatus, formatTaskStatusIcon, formatStatusName, formatBlockedStatus, progressBar } from './ui.js';
8
8
  import { refinePlan, generateReadme, discoverRequirements, buildProjectContext, formatContextForDecomposition, detectDependencies } from './planner.js';
9
9
  import { isTaskBlocked } from './taskQueue.js';
10
- import { isClaudeCodeInstalled, isClaudeCodeAuthenticated, testClaudeInteraction, launchClaudeLogin, } from './config.js';
10
+ import { isClaudeCodeInstalled, isClaudeCodeAuthenticated, testClaudeInteraction, launchClaudeLogin, loadConfig, saveConfig, } from './config.js';
11
11
  import { initLogger, getLogger } from './logger.js';
12
12
  import { VERSION } from './version.js';
13
+ import { buildHeaderWithLogo } from './logo.js';
13
14
  import { addImageFromFile, addImageFromUrl, addImageFromPaste, getImages, deleteImage, looksLikeImagePath, looksLikeImageUrl, formatImageSize, sanitizeImageId, generatePasteImageId, extractImageUrls, processDescriptionImageUrls, } from './imageManager.js';
14
15
  import { InputBox, getDisplayWidth, charIndexAtDisplayColumn } from './inputBox.js';
15
16
  import { startCaffeinate, stopCaffeinate, isCaffeinateAvailable } from './caffeinate.js';
16
17
  import { getPreventSleep, setPreventSleep, getConfigPath, detectTerminal, getShiftEnterEnabled, setShiftEnterEnabled, configureTerminalShiftEnter, removeTerminalShiftEnter, getNewlineHint, getCmdVPasteEnabled, setCmdVPasteEnabled, configureTerminalCmdVPaste, removeTerminalCmdVPaste, getCmdVPasteInstructions, } from './config.js';
17
18
  import { hasClipboardImage, saveClipboardImage, cleanupTempImage } from './clipboard.js';
18
- import { isGitRepo } from './git.js';
19
+ import { isGitRepo, ensureEnvInGitignore } from './git.js';
20
+ import { validateApiKeys } from './apiKeyValidator.js';
19
21
  import { UI, TipsManager } from './constants.js';
20
22
  import { getUpdateNotice, checkForUpdates, performUpdate, isAutoUpdatePromptEnabled, isAutoUpdateEnabled, } from './versionCheck.js';
21
23
  /**
@@ -226,8 +228,9 @@ export class MaistroApp {
226
228
  await this.handleUpdateCheck();
227
229
  }
228
230
  // Run doctor check on every startup (skip if MAISTRO_SKIP_DOCTOR is set)
231
+ // Pass fullCheck=false to skip interaction test if it has already passed once
229
232
  if (!process.env.MAISTRO_SKIP_DOCTOR) {
230
- const configured = await this.handleDoctor();
233
+ const configured = await this.handleDoctor(false);
231
234
  if (!configured) {
232
235
  getLogger()?.info('Exit: not configured');
233
236
  getLogger()?.session('end');
@@ -312,8 +315,8 @@ export class MaistroApp {
312
315
  console.log('\x1b[90mAuto-updating...\x1b[0m');
313
316
  const updateResult = await performUpdate();
314
317
  if (updateResult.success) {
315
- console.log(`\x1b[32m✓ Updated successfully!\x1b[0m`);
316
- console.log(`\x1b[90mPlease restart maistro to use the new version.\x1b[0m\n`);
318
+ this.restartAfterUpdate();
319
+ return; // Won't reach here, but for clarity
317
320
  }
318
321
  else {
319
322
  console.log(`\x1b[31m✗ Update failed: ${updateResult.error}\x1b[0m`);
@@ -333,8 +336,8 @@ export class MaistroApp {
333
336
  }
334
337
  // Prompt user to update
335
338
  const choice = await this.ui.quickPick([
339
+ { key: 'yes', label: 'Yes', description: 'Update and restart automatically' },
336
340
  { key: 'no', label: 'No', description: 'Continue with current version' },
337
- { key: 'yes', label: 'Yes', description: 'Run npm update -g maistro' },
338
341
  ], 'Update now?');
339
342
  if (choice !== 'yes') {
340
343
  console.log('\x1b[90mContinuing with current version...\x1b[0m\n');
@@ -343,18 +346,49 @@ export class MaistroApp {
343
346
  console.log('\n\x1b[90mUpdating...\x1b[0m');
344
347
  const updateResult = await performUpdate();
345
348
  if (updateResult.success) {
346
- console.log(`\x1b[32m✓ Updated successfully!\x1b[0m`);
347
- console.log(`\x1b[90mPlease restart maistro to use the new version.\x1b[0m\n`);
349
+ this.restartAfterUpdate();
350
+ return; // Won't reach here, but for clarity
348
351
  }
349
352
  else {
350
353
  console.log(`\x1b[31m✗ Update failed: ${updateResult.error}\x1b[0m`);
351
354
  console.log(`\x1b[90mContinuing with current version...\x1b[0m\n`);
352
355
  }
353
356
  }
357
+ /**
358
+ * Restart maistro after update by spawning a new process and exiting
359
+ */
360
+ restartAfterUpdate() {
361
+ console.log(`\x1b[32m✓ Updated successfully!\x1b[0m`);
362
+ console.log(`\x1b[90mRestarting maistro...\x1b[0m\n`);
363
+ // Get the original command arguments (skip node and script path)
364
+ const args = process.argv.slice(2);
365
+ // Spawn new maistro process with same arguments
366
+ // Use stdio: 'inherit' so the new process takes over the terminal
367
+ const child = spawn(process.argv[0], [process.argv[1], ...args], {
368
+ stdio: 'inherit',
369
+ detached: false,
370
+ env: {
371
+ ...process.env,
372
+ // Skip update check on restart to avoid infinite loop
373
+ MAISTRO_SKIP_UPDATE_CHECK: '1',
374
+ },
375
+ });
376
+ // Exit current process once child starts
377
+ child.on('spawn', () => {
378
+ process.exit(0);
379
+ });
380
+ // If spawn fails, exit with error
381
+ child.on('error', (err) => {
382
+ console.error(`\x1b[31m✗ Failed to restart: ${err.message}\x1b[0m`);
383
+ console.log(`\x1b[90mPlease restart maistro manually.\x1b[0m\n`);
384
+ process.exit(1);
385
+ });
386
+ }
354
387
  /**
355
388
  * Run doctor check - verify Claude Code CLI installation and authentication
389
+ * @param fullCheck - If true, always run interaction test (step 3). If false, skip if already passed once.
356
390
  */
357
- async handleDoctor() {
391
+ async handleDoctor(fullCheck = true) {
358
392
  console.log('\n\x1b[1m── Doctor ──\x1b[0m\n');
359
393
  // Step 1: Check Claude Code CLI
360
394
  console.log('\x1b[36m1.\x1b[0m Checking Claude Code CLI...');
@@ -380,6 +414,14 @@ export class MaistroApp {
380
414
  }
381
415
  console.log(' \x1b[32m✓ Claude Code authenticated\x1b[0m');
382
416
  // Step 3: Test Claude Code interaction (with tool use)
417
+ // Skip if: not a full check AND interaction test has passed before
418
+ const config = loadConfig();
419
+ const skipInteractionTest = !fullCheck && config.interactionTestPassed === true;
420
+ if (skipInteractionTest) {
421
+ console.log('\n\x1b[32m✓ All checks passed!\x1b[0m');
422
+ console.log('\x1b[90mMaistro uses Claude Code CLI with your subscription for all operations.\x1b[0m\n');
423
+ return true;
424
+ }
383
425
  console.log('\n\x1b[36m3.\x1b[0m Testing Claude Code interaction...');
384
426
  const interactionTest = await testClaudeInteraction();
385
427
  if (!interactionTest.success) {
@@ -406,6 +448,11 @@ export class MaistroApp {
406
448
  }
407
449
  return false;
408
450
  }
451
+ // Cache successful interaction test
452
+ saveConfig({
453
+ ...config,
454
+ interactionTestPassed: true,
455
+ });
409
456
  console.log(' \x1b[32m✓ Claude Code interaction working\x1b[0m');
410
457
  console.log('\n\x1b[32m✓ All checks passed!\x1b[0m');
411
458
  console.log('\x1b[90mMaistro uses Claude Code CLI with your subscription for all operations.\x1b[0m\n');
@@ -664,7 +711,7 @@ export class MaistroApp {
664
711
  });
665
712
  if (skipChoice === 's' || skipChoice === null) {
666
713
  // Even when skipping discovery, still check for dependencies
667
- await this.handleDependencyDetection();
714
+ await this.handleDependencyDetection(goal);
668
715
  return null; // Skip discovery
669
716
  }
670
717
  // Show analyzing screen with spinner
@@ -681,7 +728,7 @@ export class MaistroApp {
681
728
  console.log('\x1b[90mProceeding with direct planning...\x1b[0m\n');
682
729
  await new Promise(r => setTimeout(r, 1500));
683
730
  // Still check for dependencies even when discovery fails
684
- await this.handleDependencyDetection();
731
+ await this.handleDependencyDetection(goal);
685
732
  return null;
686
733
  }
687
734
  // Ask each question one at a time (full-screen for each)
@@ -698,7 +745,7 @@ export class MaistroApp {
698
745
  answers[question.id] = answer;
699
746
  }
700
747
  // Run dependency detection after discovery questions
701
- const depResult = await this.handleDependencyDetection();
748
+ const depResult = await this.handleDependencyDetection(goal);
702
749
  if (depResult === null) {
703
750
  // User cancelled during dependency collection
704
751
  return null;
@@ -894,15 +941,16 @@ export class MaistroApp {
894
941
  }
895
942
  /**
896
943
  * Detect and collect missing external dependencies
944
+ * @param goal - The user's goal text to detect mentioned dependencies
897
945
  * Returns the collected values or null if user cancelled
898
946
  */
899
- async handleDependencyDetection() {
947
+ async handleDependencyDetection(goal) {
900
948
  // Show scanning screen with spinner
901
949
  this.ui.clear();
902
950
  this.showHeader();
903
951
  console.log('\x1b[1m── Scanning for Dependencies ──\x1b[0m\n');
904
952
  const spinner = this.ui.spinner('Scanning project for external dependencies...');
905
- const result = await detectDependencies(this.projectPath);
953
+ const result = await detectDependencies(this.projectPath, goal);
906
954
  spinner.stop();
907
955
  if (result.dependencies.length === 0) {
908
956
  console.log('\x1b[90mNo external dependencies detected.\x1b[0m\n');
@@ -986,12 +1034,102 @@ export class MaistroApp {
986
1034
  collectedValues[dep.id] = value.trim();
987
1035
  }
988
1036
  }
989
- // Save collected values to .env file
1037
+ // Validate and save collected values
990
1038
  if (Object.keys(collectedValues).length > 0) {
991
- await this.saveDependenciesToEnv(collectedValues);
1039
+ const validationResult = await this.validateCollectedKeys(collectedValues, missing);
1040
+ if (validationResult === 'cancel') {
1041
+ return null;
1042
+ }
1043
+ // validationResult contains the final values (possibly updated after retries)
1044
+ await this.saveDependenciesToEnv(validationResult);
1045
+ return validationResult;
992
1046
  }
993
1047
  return collectedValues;
994
1048
  }
1049
+ /**
1050
+ * Validate collected API keys against their respective services
1051
+ * Returns the validated values (possibly updated after retries), or 'cancel' if user cancelled
1052
+ */
1053
+ async validateCollectedKeys(values, deps) {
1054
+ let currentValues = { ...values };
1055
+ let validationResults;
1056
+ // Validation loop - allows retrying failed keys
1057
+ while (true) {
1058
+ // Show validation spinner
1059
+ this.ui.clear();
1060
+ this.showHeader();
1061
+ console.log('\x1b[1m── Validating Credentials ──\x1b[0m\n');
1062
+ const spinner = this.ui.spinner('Validating API keys...');
1063
+ validationResults = await validateApiKeys(currentValues, { timeout: 5000 });
1064
+ spinner.stop();
1065
+ // Display results
1066
+ this.ui.clear();
1067
+ this.showHeader();
1068
+ console.log('\x1b[1m── Validation Results ──\x1b[0m\n');
1069
+ for (const [envVar, result] of Object.entries(validationResults.results)) {
1070
+ const dep = deps.find(d => d.id === envVar);
1071
+ const name = dep?.name || envVar;
1072
+ if (result.valid) {
1073
+ console.log(`\x1b[32m✓\x1b[0m ${name}`);
1074
+ if (result.skipped) {
1075
+ console.log(` \x1b[90m(${result.message})\x1b[0m`);
1076
+ }
1077
+ }
1078
+ else {
1079
+ console.log(`\x1b[31m✗\x1b[0m ${name}`);
1080
+ console.log(` \x1b[90m${result.error || result.message}\x1b[0m`);
1081
+ }
1082
+ }
1083
+ console.log('');
1084
+ // If all passed, continue
1085
+ if (validationResults.allPassed) {
1086
+ await new Promise(r => setTimeout(r, 1000));
1087
+ return currentValues;
1088
+ }
1089
+ // Some validations failed - offer choices
1090
+ const choice = await this.ui.quickPick([
1091
+ { key: 'r', label: 'Retry failed', description: 'Re-enter values for failed keys' },
1092
+ { key: 's', label: 'Save anyway', description: 'Save values despite validation failure' },
1093
+ { key: 'c', label: 'Cancel', description: 'Discard all values' },
1094
+ ], 'Some validations failed. What would you like to do?', undefined, { showInput: false });
1095
+ if (choice === 'c' || choice === null) {
1096
+ return 'cancel';
1097
+ }
1098
+ if (choice === 's') {
1099
+ return currentValues;
1100
+ }
1101
+ // choice === 'r' - retry failed keys
1102
+ const failedKeys = Object.entries(validationResults.results)
1103
+ .filter(([, result]) => !result.valid)
1104
+ .map(([key]) => key);
1105
+ for (const envVar of failedKeys) {
1106
+ const dep = deps.find(d => d.id === envVar);
1107
+ if (!dep)
1108
+ continue;
1109
+ // Build instructions string
1110
+ let instructions = '';
1111
+ if (dep.obtainInstructions) {
1112
+ instructions += `\n\n\x1b[90m${dep.obtainInstructions}\x1b[0m`;
1113
+ }
1114
+ if (dep.obtainUrl) {
1115
+ instructions += `\n\x1b[90m→ ${dep.obtainUrl}\x1b[0m`;
1116
+ }
1117
+ if (dep.valueFormat) {
1118
+ instructions += `\n\x1b[90mFormat: ${dep.valueFormat}\x1b[0m`;
1119
+ }
1120
+ const value = await this.fullScreenTextInput({
1121
+ title: 'Retry Failed Key',
1122
+ subtitle: `Re-enter value for \x1b[1m${dep.name}\x1b[0m${instructions}`,
1123
+ placeholder: dep.valueFormat || 'enter value...',
1124
+ hint: 'Enter to continue, Esc to skip',
1125
+ initialValue: currentValues[envVar],
1126
+ });
1127
+ if (value !== null && value.trim()) {
1128
+ currentValues[envVar] = value.trim();
1129
+ }
1130
+ }
1131
+ }
1132
+ }
995
1133
  /**
996
1134
  * Save collected dependency values to .env file
997
1135
  */
@@ -1029,6 +1167,11 @@ export class MaistroApp {
1029
1167
  }
1030
1168
  try {
1031
1169
  writeFileSync(envPath, envContent);
1170
+ // Ensure .env is in .gitignore to prevent accidental commit of secrets
1171
+ const gitignoreResult = await ensureEnvInGitignore(this.projectPath);
1172
+ if (gitignoreResult.success && gitignoreResult.output.includes('Added')) {
1173
+ console.log(`\x1b[32m✓ Added .env to .gitignore\x1b[0m`);
1174
+ }
1032
1175
  console.log(`\x1b[32m✓ Saved ${Object.keys(values).length} ${Object.keys(values).length === 1 ? 'value' : 'values'} to .env\x1b[0m\n`);
1033
1176
  }
1034
1177
  catch (err) {
@@ -1052,15 +1195,10 @@ export class MaistroApp {
1052
1195
  }
1053
1196
  showHeader() {
1054
1197
  const versionStr = `v${VERSION}`;
1055
- // Center the version string within the box (37 chars inner width)
1056
- const padding = Math.max(0, Math.floor((35 - versionStr.length) / 2));
1057
- const paddedVersion = ' '.repeat(padding) + versionStr + ' '.repeat(35 - padding - versionStr.length);
1058
- console.log('\n\x1b[1m\x1b[36m╭─────────────────────────────────────╮\x1b[0m');
1059
- console.log('\x1b[1m\x1b[36m│\x1b[0m \x1b[1mM A I S T R O\x1b[0m \x1b[1m\x1b[36m│\x1b[0m');
1060
- console.log('\x1b[1m\x1b[36m│\x1b[0m \x1b[90mClaude Code Orchestration Maestro\x1b[0m \x1b[1m\x1b[36m│\x1b[0m');
1061
- console.log(`\x1b[1m\x1b[36m│\x1b[0m \x1b[90m${paddedVersion}\x1b[0m \x1b[1m\x1b[36m│\x1b[0m`);
1062
- console.log('\x1b[1m\x1b[36m╰─────────────────────────────────────╯\x1b[0m\n');
1063
- console.log(`\x1b[90mProject: ${this.projectPath}\x1b[0m\n`);
1198
+ const headerLines = buildHeaderWithLogo(versionStr, this.projectPath);
1199
+ for (const line of headerLines) {
1200
+ console.log(line);
1201
+ }
1064
1202
  }
1065
1203
  /**
1066
1204
  * Render plan view content (header, progress, tasks, commands)
@@ -2286,23 +2424,28 @@ export class MaistroApp {
2286
2424
  console.log(` ${line}`);
2287
2425
  });
2288
2426
  console.log('');
2289
- // Acceptance criteria with pass/fail status
2290
- if (task.acceptanceCriteria && task.acceptanceCriteria.length > 0) {
2427
+ // Acceptance criteria with pass/fail status (includes code quality as implicit criterion)
2428
+ if (task.acceptanceCriteriaResults && task.acceptanceCriteriaResults.length > 0) {
2291
2429
  console.log(`\x1b[90mAcceptance Criteria:\x1b[0m`);
2292
- task.acceptanceCriteria.forEach((criterion, i) => {
2293
- // Check if we have validation results
2294
- const result = task.acceptanceCriteriaResults?.find(r => r.criterion === criterion);
2295
- let statusIcon = '\x1b[90m○\x1b[0m'; // Not yet validated
2296
- if (result) {
2297
- statusIcon = result.passed ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
2298
- }
2299
- console.log(` ${statusIcon} ${i + 1}. ${criterion}`);
2300
- if (result && !result.passed && result.explanation) {
2430
+ task.acceptanceCriteriaResults.forEach((result, i) => {
2431
+ const statusIcon = result.passed ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
2432
+ console.log(` ${statusIcon} ${i + 1}. ${result.criterion}`);
2433
+ if (!result.passed && result.explanation) {
2301
2434
  console.log(` \x1b[31m↳ ${result.explanation}\x1b[0m`);
2302
2435
  }
2303
2436
  });
2304
2437
  console.log('');
2305
2438
  }
2439
+ else if (task.acceptanceCriteria && task.acceptanceCriteria.length > 0) {
2440
+ // Show acceptance criteria without results (not yet validated)
2441
+ console.log(`\x1b[90mAcceptance Criteria:\x1b[0m`);
2442
+ task.acceptanceCriteria.forEach((criterion, i) => {
2443
+ console.log(` \x1b[90m○\x1b[0m ${i + 1}. ${criterion}`);
2444
+ });
2445
+ // Code quality is always checked as implicit criterion
2446
+ console.log(` \x1b[90m○\x1b[0m ${task.acceptanceCriteria.length + 1}. Code Quality: No debugging code, no copy/paste issues, no code smells`);
2447
+ console.log('');
2448
+ }
2306
2449
  // Result summary for completed tasks
2307
2450
  if (isCompleted && task.resultSummary) {
2308
2451
  console.log(`\x1b[32m── Result Summary ──\x1b[0m\n`);
@@ -2784,7 +2927,7 @@ export class MaistroApp {
2784
2927
  * Uses virtual screen approach: keeps state in memory, full clear+redraw on every change
2785
2928
  */
2786
2929
  async fullScreenTextInput(options) {
2787
- const { title, subtitle, placeholder = 'type your answer...', hint = 'Enter to continue, Esc to cancel' } = options;
2930
+ const { title, subtitle, placeholder = 'type your answer...', hint = 'Enter to continue, Esc to cancel', initialValue } = options;
2788
2931
  // Build header lines
2789
2932
  const { buildMaistroHeader, simpleTextInput } = await import('./screen.js');
2790
2933
  const headerLines = buildMaistroHeader(`v${VERSION}`, this.projectPath);
@@ -2794,6 +2937,7 @@ export class MaistroApp {
2794
2937
  subtitle,
2795
2938
  placeholder,
2796
2939
  hint,
2940
+ initialValue,
2797
2941
  });
2798
2942
  return result;
2799
2943
  }
@@ -3924,7 +4068,7 @@ ${detection.projectContext}`;
3924
4068
  });
3925
4069
  }
3926
4070
  /**
3927
- * Handle clear plan command - clears goal and all tasks, keeps logs
4071
+ * Handle clear plan command - clears goal, all tasks, and logs
3928
4072
  * Returns true if plan was cleared and should return to init flow
3929
4073
  */
3930
4074
  async handleClearPlan() {
@@ -3945,8 +4089,9 @@ ${detection.projectContext}`;
3945
4089
  const truncatedGoal = goal.length > 60 ? goal.slice(0, 57) + '...' : goal;
3946
4090
  console.log(` • Goal: \x1b[33m${truncatedGoal}\x1b[0m`);
3947
4091
  }
3948
- console.log(` • ${tasks.length} task${tasks.length !== 1 ? 's' : ''}\n`);
3949
- console.log('\x1b[90mLogs and images will be preserved.\x1b[0m\n');
4092
+ console.log(` • ${tasks.length} task${tasks.length !== 1 ? 's' : ''}`);
4093
+ console.log(` • All task logs\n`);
4094
+ console.log('\x1b[90mImages will be preserved.\x1b[0m\n');
3950
4095
  const choice = await this.ui.quickPick([
3951
4096
  { key: 'clear', label: 'Clear plan' },
3952
4097
  { key: 'cancel', label: 'Cancel' },
@@ -3954,7 +4099,7 @@ ${detection.projectContext}`;
3954
4099
  if (choice === 'clear') {
3955
4100
  const result = await orchestrator.clearPlan();
3956
4101
  if (result.success) {
3957
- console.log('\n\x1b[32m✓ Plan cleared\x1b[0m\n');
4102
+ console.log('\n\x1b[32m✓ Plan and logs cleared\x1b[0m\n');
3958
4103
  await this.ui.waitForKey('Press any key to continue...');
3959
4104
  return true;
3960
4105
  }
@@ -4625,9 +4770,6 @@ ${detection.projectContext}`;
4625
4770
  let editTaskFlag = false;
4626
4771
  let editTaskIndex = -1;
4627
4772
  let taskEditWarning = null;
4628
- // Track task completion for notifications
4629
- let lastCompletedTaskId = null;
4630
- let taskCompletionNotification = null;
4631
4773
  // Render function
4632
4774
  const render = async () => {
4633
4775
  needsRender = false;
@@ -4664,10 +4806,6 @@ ${detection.projectContext}`;
4664
4806
  hint = taskEditWarning;
4665
4807
  hintColor = '\x1b[33m';
4666
4808
  }
4667
- else if (taskCompletionNotification) {
4668
- hint = taskCompletionNotification;
4669
- hintColor = '\x1b[32m';
4670
- }
4671
4809
  else {
4672
4810
  hint = 'Esc pause · ↑↓ navigate · Enter details · e edit';
4673
4811
  hintColor = '\x1b[90m';
@@ -4837,19 +4975,11 @@ ${detection.projectContext}`;
4837
4975
  this.executionState.lastCompletedTask = task;
4838
4976
  }
4839
4977
  }
4840
- // Show notification if user is browsing other tasks
4841
- if (userNavigated && this.selectedTaskIndex !== tasks.findIndex(t => t.id === task.id)) {
4842
- const status = success ? '✓' : '✗';
4843
- taskCompletionNotification = `${status} Task completed: ${task.title.slice(0, 30)}${task.title.length > 30 ? '...' : ''}`;
4844
- lastCompletedTaskId = task.id;
4978
+ // Always move cursor to the completed task so user sees the summary
4979
+ const completedTaskIndex = tasks.findIndex(t => t.id === task.id);
4980
+ if (completedTaskIndex >= 0) {
4981
+ this.selectedTaskIndex = completedTaskIndex;
4845
4982
  needsRender = true;
4846
- // Clear notification after 3 seconds
4847
- setTimeout(() => {
4848
- if (lastCompletedTaskId === task.id) {
4849
- taskCompletionNotification = null;
4850
- needsRender = true;
4851
- }
4852
- }, 3000);
4853
4983
  }
4854
4984
  },
4855
4985
  onOutput: (line) => {