@covibes/zeroshot 5.2.1 → 5.4.0

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 (98) hide show
  1. package/CHANGELOG.md +174 -189
  2. package/README.md +226 -195
  3. package/cli/commands/providers.js +149 -0
  4. package/cli/index.js +3145 -2366
  5. package/cli/lib/first-run.js +40 -3
  6. package/cli/message-formatters-normal.js +28 -6
  7. package/cluster-templates/base-templates/debug-workflow.json +24 -78
  8. package/cluster-templates/base-templates/full-workflow.json +99 -316
  9. package/cluster-templates/base-templates/single-worker.json +23 -15
  10. package/cluster-templates/base-templates/worker-validator.json +105 -36
  11. package/cluster-templates/conductor-bootstrap.json +9 -7
  12. package/lib/docker-config.js +14 -1
  13. package/lib/git-remote-utils.js +165 -0
  14. package/lib/id-detector.js +10 -7
  15. package/lib/provider-defaults.js +62 -0
  16. package/lib/provider-detection.js +59 -0
  17. package/lib/provider-names.js +57 -0
  18. package/lib/settings/claude-auth.js +78 -0
  19. package/lib/settings.js +298 -15
  20. package/lib/stream-json-parser.js +4 -238
  21. package/package.json +27 -6
  22. package/scripts/setup-merge-queue.sh +170 -0
  23. package/scripts/validate-templates.js +100 -0
  24. package/src/agent/agent-config.js +140 -63
  25. package/src/agent/agent-context-builder.js +336 -165
  26. package/src/agent/agent-hook-executor.js +337 -67
  27. package/src/agent/agent-lifecycle.js +386 -287
  28. package/src/agent/agent-stuck-detector.js +7 -7
  29. package/src/agent/agent-task-executor.js +944 -683
  30. package/src/agent/output-extraction.js +217 -0
  31. package/src/agent/output-reformatter.js +175 -0
  32. package/src/agent/schema-utils.js +146 -0
  33. package/src/agent-wrapper.js +112 -31
  34. package/src/agents/git-pusher-template.js +285 -0
  35. package/src/claude-task-runner.js +145 -44
  36. package/src/config-router.js +13 -13
  37. package/src/config-validator.js +1049 -563
  38. package/src/input-helpers.js +65 -0
  39. package/src/isolation-manager.js +499 -320
  40. package/src/issue-providers/README.md +305 -0
  41. package/src/issue-providers/azure-devops-provider.js +273 -0
  42. package/src/issue-providers/base-provider.js +232 -0
  43. package/src/issue-providers/github-provider.js +179 -0
  44. package/src/issue-providers/gitlab-provider.js +241 -0
  45. package/src/issue-providers/index.js +196 -0
  46. package/src/issue-providers/jira-provider.js +239 -0
  47. package/src/ledger.js +50 -11
  48. package/src/lib/safe-exec.js +88 -0
  49. package/src/orchestrator.js +1348 -757
  50. package/src/preflight.js +306 -149
  51. package/src/process-metrics.js +98 -56
  52. package/src/providers/anthropic/cli-builder.js +73 -0
  53. package/src/providers/anthropic/index.js +204 -0
  54. package/src/providers/anthropic/models.js +23 -0
  55. package/src/providers/anthropic/output-parser.js +177 -0
  56. package/src/providers/base-provider.js +251 -0
  57. package/src/providers/capabilities.js +60 -0
  58. package/src/providers/google/cli-builder.js +55 -0
  59. package/src/providers/google/index.js +116 -0
  60. package/src/providers/google/models.js +24 -0
  61. package/src/providers/google/output-parser.js +101 -0
  62. package/src/providers/index.js +91 -0
  63. package/src/providers/openai/cli-builder.js +133 -0
  64. package/src/providers/openai/index.js +136 -0
  65. package/src/providers/openai/models.js +21 -0
  66. package/src/providers/openai/output-parser.js +143 -0
  67. package/src/providers/opencode/cli-builder.js +42 -0
  68. package/src/providers/opencode/index.js +103 -0
  69. package/src/providers/opencode/models.js +55 -0
  70. package/src/providers/opencode/output-parser.js +122 -0
  71. package/src/schemas/sub-cluster.js +68 -39
  72. package/src/status-footer.js +94 -48
  73. package/src/sub-cluster-wrapper.js +92 -36
  74. package/src/task-runner.js +8 -6
  75. package/src/template-resolver.js +12 -9
  76. package/src/tui/data-poller.js +123 -99
  77. package/src/tui/formatters.js +4 -3
  78. package/src/tui/keybindings.js +259 -318
  79. package/src/tui/layout.js +20 -3
  80. package/src/tui/renderer.js +11 -21
  81. package/task-lib/attachable-watcher.js +150 -111
  82. package/task-lib/claude-recovery.js +156 -0
  83. package/task-lib/commands/episodes.js +105 -0
  84. package/task-lib/commands/list.js +3 -3
  85. package/task-lib/commands/logs.js +231 -189
  86. package/task-lib/commands/resume.js +3 -2
  87. package/task-lib/commands/run.js +12 -3
  88. package/task-lib/commands/schedules.js +111 -61
  89. package/task-lib/config.js +0 -2
  90. package/task-lib/runner.js +128 -50
  91. package/task-lib/scheduler.js +3 -3
  92. package/task-lib/store.js +464 -152
  93. package/task-lib/tui/formatters.js +94 -45
  94. package/task-lib/tui/renderer.js +13 -3
  95. package/task-lib/tui.js +73 -32
  96. package/task-lib/watcher.js +157 -100
  97. package/src/agents/git-pusher-agent.json +0 -20
  98. package/src/github.js +0 -103
package/src/preflight.js CHANGED
@@ -2,17 +2,25 @@
2
2
  * Preflight Validation - Check all dependencies before starting
3
3
  *
4
4
  * Validates:
5
- * - Claude CLI installed and authenticated
5
+ * - Selected provider CLI installed
6
6
  * - gh CLI installed and authenticated (if using issue numbers)
7
7
  * - Docker available (if using --docker)
8
8
  *
9
9
  * Provides CLEAR, ACTIONABLE error messages with recovery instructions.
10
10
  */
11
11
 
12
- const { execSync } = require('child_process');
12
+ const { execSync } = require('./lib/safe-exec'); // Enforces timeouts
13
13
  const path = require('path');
14
14
  const fs = require('fs');
15
15
  const os = require('os');
16
+ const {
17
+ isValidAnthropicKey,
18
+ ANTHROPIC_KEY_PREFIX,
19
+ resolveClaudeAuth,
20
+ } = require('../lib/settings/claude-auth.js');
21
+ const { loadSettings, getClaudeCommand } = require('../lib/settings.js');
22
+ const { normalizeProviderName } = require('../lib/provider-names');
23
+ const { detectGitContext } = require('../lib/git-remote-utils');
16
24
 
17
25
  /**
18
26
  * Validation result
@@ -42,13 +50,15 @@ function formatError(title, detail, recovery) {
42
50
  }
43
51
 
44
52
  /**
45
- * Check if a command exists
53
+ * Check if a command exists (cross-platform)
46
54
  * @param {string} cmd - Command to check
47
55
  * @returns {boolean}
48
56
  */
49
57
  function commandExists(cmd) {
50
58
  try {
51
- execSync(`which ${cmd}`, { encoding: 'utf8', stdio: 'pipe' });
59
+ // Windows uses 'where', Unix uses 'which'
60
+ const checkCmd = process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`;
61
+ execSync(checkCmd, { encoding: 'utf8', stdio: 'pipe' });
52
62
  return true;
53
63
  } catch {
54
64
  return false;
@@ -120,29 +130,55 @@ function checkMacOsKeychain() {
120
130
  */
121
131
  function checkClaudeAuth() {
122
132
  const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
133
+
134
+ // Helper to create consistent result objects
135
+ const authResult = (authenticated, error = null, method = null) => ({
136
+ authenticated,
137
+ error,
138
+ configDir,
139
+ ...(method && { method }),
140
+ });
141
+
142
+ // Check for Bedrock bearer token (highest priority)
143
+ if (process.env.AWS_BEARER_TOKEN_BEDROCK) {
144
+ if (!process.env.AWS_REGION) {
145
+ return authResult(false, 'AWS_BEARER_TOKEN_BEDROCK set but AWS_REGION is missing');
146
+ }
147
+ return authResult(true, null, 'bedrock_api_key');
148
+ }
149
+
150
+ // Check for ANTHROPIC_API_KEY environment variable
151
+ const apiKeyEnv = process.env.ANTHROPIC_API_KEY;
152
+ if (apiKeyEnv) {
153
+ if (!isValidAnthropicKey(apiKeyEnv)) {
154
+ return authResult(false, `ANTHROPIC_API_KEY must start with ${ANTHROPIC_KEY_PREFIX}`);
155
+ }
156
+ return authResult(true, null, 'env_api_key');
157
+ }
158
+
159
+ // Check for settings-based auth (anthropicApiKey or bedrockApiKey in settings)
160
+ const settings = loadSettings();
161
+ const settingsAuth = resolveClaudeAuth(settings);
162
+ if (settingsAuth.ANTHROPIC_API_KEY) {
163
+ return authResult(true, null, 'settings_api_key');
164
+ }
165
+ if (settingsAuth.AWS_BEARER_TOKEN_BEDROCK) {
166
+ if (!settingsAuth.AWS_REGION && !process.env.AWS_REGION) {
167
+ return authResult(false, 'Bedrock configured in settings but AWS_REGION is missing');
168
+ }
169
+ return authResult(true, null, 'settings_bedrock');
170
+ }
171
+
123
172
  const credentialsPath = path.join(configDir, '.credentials.json');
124
173
 
125
174
  // Check if credentials file exists
126
175
  if (!fs.existsSync(credentialsPath)) {
127
176
  // No credentials file - check macOS Keychain as fallback
128
177
  // Only use Keychain when using default config dir (not custom CLAUDE_CONFIG_DIR)
129
- const isDefaultConfigDir = !process.env.CLAUDE_CONFIG_DIR;
130
- if (isDefaultConfigDir) {
131
- const keychainResult = checkMacOsKeychain();
132
- if (keychainResult.authenticated) {
133
- return {
134
- authenticated: true,
135
- error: null,
136
- configDir,
137
- method: 'keychain',
138
- };
139
- }
178
+ if (!process.env.CLAUDE_CONFIG_DIR && checkMacOsKeychain().authenticated) {
179
+ return authResult(true, null, 'keychain');
140
180
  }
141
- return {
142
- authenticated: false,
143
- error: 'No credentials file found',
144
- configDir,
145
- };
181
+ return authResult(false, 'No credentials file found');
146
182
  }
147
183
 
148
184
  // Check if credentials file has content
@@ -152,42 +188,21 @@ function checkClaudeAuth() {
152
188
 
153
189
  // Check for OAuth token (primary auth method)
154
190
  if (creds.claudeAiOauth?.accessToken) {
155
- // Check if token is expired
156
191
  const expiresAt = creds.claudeAiOauth.expiresAt;
157
192
  if (expiresAt && new Date(expiresAt) < new Date()) {
158
- return {
159
- authenticated: false,
160
- error: 'OAuth token expired',
161
- configDir,
162
- };
193
+ return authResult(false, 'OAuth token expired');
163
194
  }
164
- return {
165
- authenticated: true,
166
- error: null,
167
- configDir,
168
- };
195
+ return authResult(true);
169
196
  }
170
197
 
171
198
  // Check for API key auth
172
199
  if (creds.apiKey) {
173
- return {
174
- authenticated: true,
175
- error: null,
176
- configDir,
177
- };
200
+ return authResult(true);
178
201
  }
179
202
 
180
- return {
181
- authenticated: false,
182
- error: 'No valid authentication found in credentials',
183
- configDir,
184
- };
203
+ return authResult(false, 'No valid authentication found in credentials');
185
204
  } catch (err) {
186
- return {
187
- authenticated: false,
188
- error: `Failed to parse credentials: ${err.message}`,
189
- configDir,
190
- };
205
+ return authResult(false, `Failed to parse credentials: ${err.message}`);
191
206
  }
192
207
  }
193
208
 
@@ -272,26 +287,16 @@ function checkDocker() {
272
287
  }
273
288
  }
274
289
 
275
- /**
276
- * Run all preflight checks
277
- * @param {Object} options - Preflight options
278
- * @param {boolean} options.requireGh - Whether gh CLI is required (true if using issue number)
279
- * @param {boolean} options.requireDocker - Whether Docker is required (true if using --docker)
280
- * @param {boolean} options.requireGit - Whether git repo is required (true if using --worktree)
281
- * @param {boolean} options.quiet - Suppress success messages
282
- * @param {string} options.claudeCommand - Custom Claude command (from settings)
283
- * @returns {ValidationResult}
284
- */
285
- function runPreflight(options = {}) {
290
+ function buildClaudeCommand(options) {
291
+ const { command, args } = getClaudeCommand();
292
+ return options.claudeCommand || [command, ...args].join(' ');
293
+ }
294
+
295
+ function validateClaudeProvider(options) {
286
296
  const errors = [];
287
297
  const warnings = [];
298
+ const claudeCommand = buildClaudeCommand(options);
288
299
 
289
- // Get configured Claude command (supports custom commands like 'ccr code')
290
- const { getClaudeCommand } = require('../lib/settings.js');
291
- const { command, args } = getClaudeCommand();
292
- const claudeCommand = options.claudeCommand || [command, ...args].join(' ');
293
-
294
- // 1. Check Claude CLI installation
295
300
  const claude = getClaudeVersion(claudeCommand);
296
301
  if (!claude.installed) {
297
302
  errors.push(
@@ -312,35 +317,15 @@ function runPreflight(options = {}) {
312
317
  ]
313
318
  )
314
319
  );
315
- } else {
316
- // 2. Check Claude CLI authentication
317
- const auth = checkClaudeAuth();
318
- if (!auth.authenticated) {
319
- errors.push(
320
- formatError(
321
- 'Claude CLI not authenticated',
322
- auth.error,
323
- [
324
- 'Run: claude login',
325
- 'Follow the browser prompts to authenticate',
326
- `Config directory: ${auth.configDir}`,
327
- ]
328
- )
320
+ } else if (claude.version) {
321
+ const [major, minor] = claude.version.split('.').map(Number);
322
+ if (major < 1 || (major === 1 && minor < 0)) {
323
+ warnings.push(
324
+ `⚠️ Claude CLI version ${claude.version} may be outdated. Consider upgrading.`
329
325
  );
330
326
  }
331
-
332
- // Check version (warn if old)
333
- if (claude.version) {
334
- const [major, minor] = claude.version.split('.').map(Number);
335
- if (major < 1 || (major === 1 && minor < 0)) {
336
- warnings.push(
337
- `⚠️ Claude CLI version ${claude.version} may be outdated. Consider upgrading.`
338
- );
339
- }
340
- }
341
327
  }
342
328
 
343
- // 3. Check if running as root (blocks --dangerously-skip-permissions)
344
329
  if (process.getuid && process.getuid() === 0) {
345
330
  errors.push(
346
331
  formatError(
@@ -356,78 +341,249 @@ function runPreflight(options = {}) {
356
341
  );
357
342
  }
358
343
 
359
- // 4. Check gh CLI (if required)
360
- if (options.requireGh) {
361
- const gh = checkGhAuth();
362
- if (!gh.installed) {
363
- errors.push(
364
- formatError(
365
- 'GitHub CLI (gh) not installed',
366
- 'Required for fetching issues by number',
367
- [
368
- 'Install: brew install gh (macOS) or apt install gh (Linux)',
369
- 'Or download from: https://cli.github.com/',
370
- ]
371
- )
372
- );
373
- } else if (!gh.authenticated) {
374
- errors.push(
375
- formatError(
376
- 'GitHub CLI (gh) not authenticated',
377
- gh.error,
378
- [
379
- 'Run: gh auth login',
380
- 'Select GitHub.com, HTTPS, and authenticate via browser',
381
- 'Then verify: gh auth status',
382
- ]
383
- )
384
- );
385
- }
344
+ return { errors, warnings };
345
+ }
346
+
347
+ function validateCliProvider(command, title, detail, recovery) {
348
+ const errors = [];
349
+ if (!commandExists(command)) {
350
+ errors.push(formatError(title, detail, recovery));
386
351
  }
387
352
 
388
- // 5. Check Docker (if required)
389
- if (options.requireDocker) {
390
- const docker = checkDocker();
391
- if (!docker.available) {
392
- errors.push(
393
- formatError(
394
- 'Docker not available',
395
- docker.error,
396
- docker.error.includes('daemon')
397
- ? ['Start Docker Desktop', 'Or run: sudo systemctl start docker (Linux)']
398
- : [
399
- 'Install Docker Desktop from: https://docker.com/products/docker-desktop',
400
- 'Then start Docker and verify: docker info',
401
- ]
402
- )
403
- );
353
+ return { errors, warnings: [] };
354
+ }
355
+
356
+ function validateProvider(providerName, options) {
357
+ const validatorByProvider = {
358
+ claude: () => validateClaudeProvider(options),
359
+ codex: () =>
360
+ validateCliProvider('codex', 'Codex CLI not available', 'Command "codex" not installed', [
361
+ 'Install Codex CLI: npm install -g @openai/codex',
362
+ 'Then run: codex --version',
363
+ ]),
364
+ gemini: () =>
365
+ validateCliProvider('gemini', 'Gemini CLI not available', 'Command "gemini" not installed', [
366
+ 'Install Gemini CLI: npm install -g @google/gemini-cli',
367
+ 'Then run: gemini --version',
368
+ ]),
369
+ opencode: () =>
370
+ validateCliProvider(
371
+ 'opencode',
372
+ 'Opencode CLI not available',
373
+ 'Command "opencode" not installed',
374
+ ['Install Opencode CLI: see https://opencode.ai', 'Then run: opencode --version']
375
+ ),
376
+ };
377
+
378
+ const validator = validatorByProvider[providerName];
379
+ if (!validator) {
380
+ return {
381
+ errors: [
382
+ formatError('Unknown provider', `Provider "${providerName}" is not supported`, [
383
+ 'Use claude, codex, gemini, or opencode',
384
+ ]),
385
+ ],
386
+ warnings: [],
387
+ };
388
+ }
389
+
390
+ return validator();
391
+ }
392
+
393
+ function validateGhRequirement() {
394
+ const errors = [];
395
+ const gh = checkGhAuth();
396
+ if (!gh.installed) {
397
+ errors.push(
398
+ formatError('GitHub CLI (gh) not installed', 'Required for fetching issues by number', [
399
+ 'Install: brew install gh (macOS) or apt install gh (Linux)',
400
+ 'Or download from: https://cli.github.com/',
401
+ ])
402
+ );
403
+ } else if (!gh.authenticated) {
404
+ errors.push(
405
+ formatError('GitHub CLI (gh) not authenticated', gh.error, [
406
+ 'Run: gh auth login',
407
+ 'Select GitHub.com, HTTPS, and authenticate via browser',
408
+ 'Then verify: gh auth status',
409
+ ])
410
+ );
411
+ }
412
+
413
+ return errors;
414
+ }
415
+
416
+ function validateDockerRequirement() {
417
+ const errors = [];
418
+ const docker = checkDocker();
419
+ if (!docker.available) {
420
+ errors.push(
421
+ formatError(
422
+ 'Docker not available',
423
+ docker.error,
424
+ docker.error.includes('daemon')
425
+ ? ['Start Docker Desktop', 'Or run: sudo systemctl start docker (Linux)']
426
+ : [
427
+ 'Install Docker Desktop from: https://docker.com/products/docker-desktop',
428
+ 'Then start Docker and verify: docker info',
429
+ ]
430
+ )
431
+ );
432
+ }
433
+
434
+ return errors;
435
+ }
436
+
437
+ function isGitRepository() {
438
+ try {
439
+ execSync('git rev-parse --git-dir', { stdio: 'pipe' });
440
+ return true;
441
+ } catch {
442
+ return false;
443
+ }
444
+ }
445
+
446
+ function validateGitRequirement() {
447
+ if (isGitRepository()) {
448
+ return [];
449
+ }
450
+
451
+ return [
452
+ formatError('Not in a git repository', 'Worktree isolation requires a git repository', [
453
+ 'Run from within a git repository',
454
+ 'Or use --docker instead of --worktree for non-git directories',
455
+ 'Initialize a repo with: git init',
456
+ ]),
457
+ ];
458
+ }
459
+
460
+ /**
461
+ * Run all preflight checks
462
+ * @param {Object} options - Preflight options
463
+ * @param {boolean} options.requireGh - Whether gh CLI is required (true if using issue number)
464
+ * @param {boolean} options.requireDocker - Whether Docker is required (true if using --docker)
465
+ * @param {boolean} options.requireGit - Whether git repo is required (true if using --worktree)
466
+ * @param {boolean} options.quiet - Suppress success messages
467
+ * @param {string} options.claudeCommand - Custom Claude command (from settings)
468
+ * @param {string} options.provider - Provider override
469
+ * @returns {ValidationResult}
470
+ */
471
+ function runPreflight(options = {}) {
472
+ const errors = [];
473
+ const warnings = [];
474
+
475
+ const settings = loadSettings();
476
+ const providerName = normalizeProviderName(
477
+ options.provider || settings.defaultProvider || 'claude'
478
+ );
479
+
480
+ const providerResult = validateProvider(providerName, options);
481
+ errors.push(...providerResult.errors);
482
+ warnings.push(...providerResult.warnings);
483
+
484
+ // 4. Check issue provider CLI (if required)
485
+ if (options.issueProvider) {
486
+ const { getProvider } = require('./issue-providers');
487
+ const ProviderClass = getProvider(options.issueProvider);
488
+
489
+ if (ProviderClass) {
490
+ const tool = ProviderClass.getRequiredTool();
491
+
492
+ // Check if tool is installed
493
+ if (!commandExists(tool.name)) {
494
+ errors.push(
495
+ formatError(
496
+ `${ProviderClass.displayName} CLI (${tool.name}) not installed`,
497
+ `Required for fetching ${ProviderClass.displayName} issues`,
498
+ [tool.installHint, `Then verify: ${tool.checkCmd}`]
499
+ )
500
+ );
501
+ } else {
502
+ // Check provider authentication (abstracted per provider)
503
+ // Use targetHost from URL input if provided, otherwise detect from git context
504
+ // This ensures we check auth for the actual target, not the current repo
505
+ const targetHost =
506
+ options.targetHost || detectGitContext(options.cwd || process.cwd())?.host;
507
+ const authResult = ProviderClass.checkAuth(targetHost);
508
+ if (!authResult.authenticated) {
509
+ errors.push(
510
+ formatError(
511
+ `${ProviderClass.displayName} CLI (${tool.name}) not authenticated`,
512
+ authResult.error,
513
+ authResult.recovery
514
+ )
515
+ );
516
+ }
517
+ }
404
518
  }
405
519
  }
406
520
 
407
- // 6. Check git repo (if required for worktree isolation)
408
- if (options.requireGit) {
409
- let isGitRepo = false;
521
+ // 5. Check PR/MR CLI tools (if --pr or --ship mode is active)
522
+ if (options.autoPr) {
523
+ const { getPlatformForPR, getPRToolForPlatform, getProvider } = require('./issue-providers');
524
+
525
+ let platform;
526
+ let prGitContext;
410
527
  try {
411
- execSync('git rev-parse --git-dir', { stdio: 'pipe' });
412
- isGitRepo = true;
413
- } catch {
414
- // Not a git repo
415
- }
416
- if (!isGitRepo) {
528
+ // Detect git platform (independent of issue provider)
529
+ platform = getPlatformForPR(options.cwd || process.cwd());
530
+ // Get git context for hostname (needed for multi-instance auth checks)
531
+ prGitContext = detectGitContext(options.cwd || process.cwd());
532
+ } catch (error) {
533
+ // If platform detection fails, show clear error
417
534
  errors.push(
418
- formatError(
419
- 'Not in a git repository',
420
- 'Worktree isolation requires a git repository',
421
- [
422
- 'Run from within a git repository',
423
- 'Or use --docker instead of --worktree for non-git directories',
424
- 'Initialize a repo with: git init',
425
- ]
426
- )
535
+ formatError('--pr mode requires a git repository', error.message, [
536
+ 'Ensure you are in a git repository with a remote URL from GitHub, GitLab, or Azure DevOps',
537
+ ])
427
538
  );
539
+ // Skip CLI tool check if platform unknown
540
+ }
541
+
542
+ if (platform) {
543
+ // Get PR tool info from the provider (unified source of truth)
544
+ const tool = getPRToolForPlatform(platform);
545
+ const ProviderClass = getProvider(platform);
546
+
547
+ if (tool && !commandExists(tool.name)) {
548
+ errors.push(
549
+ formatError(
550
+ `${tool.displayName} CLI (${tool.name}) not installed`,
551
+ `Required for --pr mode with ${tool.displayName} repositories`,
552
+ [tool.installHint, `Then verify: ${tool.checkCmd}`]
553
+ )
554
+ );
555
+ } else if (tool && ProviderClass) {
556
+ // Check provider authentication (abstracted per provider)
557
+ // Pass hostname for multi-instance providers (e.g., GitLab with self-hosted)
558
+ const authResult = ProviderClass.checkAuth(prGitContext?.host);
559
+ if (!authResult.authenticated) {
560
+ errors.push(
561
+ formatError(
562
+ `${tool.displayName} CLI (${tool.name}) not authenticated`,
563
+ authResult.error,
564
+ authResult.recovery
565
+ )
566
+ );
567
+ }
568
+ }
428
569
  }
429
570
  }
430
571
 
572
+ // Legacy gh check for backward compatibility
573
+ if (options.requireGh) {
574
+ errors.push(...validateGhRequirement());
575
+ }
576
+
577
+ // 6. Check Docker (if required)
578
+ if (options.requireDocker) {
579
+ errors.push(...validateDockerRequirement());
580
+ }
581
+
582
+ // 7. Check git repo (if required for worktree isolation)
583
+ if (options.requireGit) {
584
+ errors.push(...validateGitRequirement());
585
+ }
586
+
431
587
  return {
432
588
  valid: errors.length === 0,
433
589
  errors,
@@ -442,6 +598,7 @@ function runPreflight(options = {}) {
442
598
  * @param {boolean} options.requireDocker - Whether Docker is required
443
599
  * @param {boolean} options.requireGit - Whether git repo is required
444
600
  * @param {boolean} options.quiet - Suppress success messages
601
+ * @param {string} options.provider - Provider override
445
602
  */
446
603
  function requirePreflight(options = {}) {
447
604
  const result = runPreflight(options);