startup
Verify GitHub authentication, validate state files, and check configuration status before starting operations.
Instructions
Run startup checks including GitHub auth verification, state file validation, and configuration status.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- Main tool handler: runStartup() — runs startup checks (setup, auth, daily fetch, dashboard launch, issue list detection) and returns structured StartupOutput.
export async function runStartup(): Promise<StartupOutput> { const version = getCLIVersion(); const stateManager = getStateManager(); // 1. Check setup — auto-detect if incomplete let autoDetected = false; if (!stateManager.isSetupComplete()) { const detectedUsername = await detectGitHubUsername(); if (detectedUsername) { try { stateManager.initializeWithDefaults(detectedUsername); autoDetected = true; } catch (err) { console.error( `[STARTUP] Auto-detected username "${detectedUsername}" but failed to save config:`, errorMessage(err), ); return { version, setupComplete: false }; } } else { return { version, setupComplete: false }; } } // 2. Check auth — use the async variant so the `gh auth token` CLI fallback // fires for users who ran `gh auth login` but never exported $GITHUB_TOKEN. // The sync `getGitHubToken()` reads only the env var, matching the `preAction` // token check that the CLI's `localOnly: true` flag on `startup` deliberately // skips — the mismatch produced a spurious `authError` for valid users. const token = await getGitHubTokenAsync(); if (!token) { return { version, setupComplete: true, authError: 'GitHub authentication required. Install GitHub CLI (https://cli.github.com/) and run "gh auth login", or set GITHUB_TOKEN.', }; } // 3. Run daily check const daily = await executeDailyCheck(token); // 4. Launch interactive SPA dashboard. // // Launched unconditionally once setup and auth pass. A prior heuristic skipped // launch whenever `totalActivePRs === 0`, assuming that meant a genuine first // run and deferring to the CLI's welcome flow. That gate also swallowed the // dashboard in three legitimate cases: a misconfigured/stale `githubUsername` // (Search API returns zero), transient GitHub API flakes (state still holds // merged PRs but the live count is zero), and users genuinely between PRs. // The dashboard's own empty-state UI renders "no PRs" cleanly, so always // surfacing it is the right default. let dashboardUrl: string | undefined; let dashboardStatus: 'opened' | 'refreshed' | 'running' | undefined; let dashboardError: string | undefined; try { const spaResult = await launchDashboardServer(); if (spaResult) { dashboardUrl = spaResult.url; if (spaResult.alreadyRunning) { const refreshed = await triggerDashboardRefresh(spaResult.port); dashboardStatus = refreshed ? 'refreshed' : 'running'; } else { dashboardStatus = 'opened'; } // `open`/`xdg-open`/`start` focus an existing tab matching the URL // instead of duplicating it, so this is safe whether the server was // just started or was already running. Closes #830 properly — a user // can close the dashboard tab while the daemon keeps running, leaving // subsequent /oss runs with no visible dashboard if we didn't re-open. openInBrowser(spaResult.url); } else { dashboardError = 'Dashboard SPA assets not found. Build with: cd packages/dashboard && pnpm run build'; console.error(`[STARTUP] ${dashboardError}`); } } catch (error) { dashboardError = `SPA dashboard launch failed: ${errorMessage(error)}`; console.error(`[STARTUP] ${dashboardError}`); } // Append dashboard status to brief summary if (dashboardStatus === 'opened') { daily.briefSummary += ' | Dashboard opened in browser'; } else if (dashboardStatus === 'refreshed') { daily.briefSummary += ' | Dashboard refreshed'; } else if (dashboardStatus === 'running') { daily.briefSummary += ' | Dashboard running'; } // 5. Detect issue list const issueList = detectIssueList(); return { version, setupComplete: true, autoDetected, daily, dashboardUrl, dashboardError, issueList, }; } - StartupOutput interface defining the schema for the startup command's return value (version, setupComplete, authError, daily, dashboardUrl, dashboardError, issueList).
export interface StartupOutput { version: string; setupComplete: boolean; /** True when username was auto-detected on first run (zero-config). */ autoDetected?: boolean; authError?: string; daily?: DailyOutput; /** URL of the interactive SPA dashboard server, when running (e.g., "http://localhost:3000") */ dashboardUrl?: string; /** * Set when the dashboard launch or refresh failed (assets missing, port * conflict, spawn error, etc.). The dashboard is always attempted, so JSON * consumers — which previously saw only a missing `dashboardUrl` — now have * a structured signal to surface or recover from the failure. */ dashboardError?: string; issueList?: IssueListInfo; } - IssueListInfo interface used in StartupOutput (path, source, availableCount, completedCount, skippedIssuesPath).
export interface IssueListInfo { path: string; source: 'configured' | 'auto-detected'; availableCount: number; completedCount: number; skippedIssuesPath?: string; } - packages/mcp-server/src/tools.ts:338-348 (registration)MCP tool registration for 'startup' — registers the tool with empty input schema, description, and wrapTool(runStartup) as the handler.
// 15. startup — Run startup checks server.registerTool( 'startup', { description: 'Run startup checks including GitHub auth verification, state file validation, and configuration status.', inputSchema: {}, annotations: { readOnlyHint: false, destructiveHint: false }, }, wrapTool(runStartup), ); - detectIssueList() helper — detects issue list from state config, legacy config.md, or known paths, and counts items.
export function detectIssueList(): IssueListInfo | undefined { let issueListPath = ''; let source: IssueListInfo['source'] = 'auto-detected'; // 1. Check state.json config (primary) try { const stateManager = getStateManager(); const configuredPath = stateManager.getState().config.issueListPath; if (configuredPath && fs.existsSync(configuredPath)) { issueListPath = configuredPath; source = 'configured'; } } catch (error) { // State manager may not be initialized yet — fall through to legacy config.md warn('startup', `Could not read issueListPath from state: ${errorMessage(error)}`); } // 2. Fallback: config.md (legacy — will be removed in future) if (!issueListPath) { const configPath = '.claude/oss-autopilot/config.md'; if (fs.existsSync(configPath)) { try { const configContent = fs.readFileSync(configPath, 'utf8'); const configuredPath = parseIssueListPathFromConfig(configContent); if (configuredPath && fs.existsSync(configuredPath)) { issueListPath = configuredPath; source = 'configured'; } } catch (error) { console.error('[STARTUP] Failed to read config:', errorMessage(error)); } } } // 3. Probe known paths if (!issueListPath) { const probes = ['open-source/potential-issue-list.md', 'oss/issue-list.md', 'issues.md']; for (const probe of probes) { if (fs.existsSync(probe)) { issueListPath = probe; source = 'auto-detected'; break; } } } if (!issueListPath) return undefined; // 4. Count available/completed items let availableCount = 0; let completedCount = 0; try { const content = fs.readFileSync(issueListPath, 'utf8'); ({ availableCount, completedCount } = countIssueListItems(content)); } catch (error) { console.error(`[STARTUP] Failed to read issue list at ${issueListPath}:`, errorMessage(error)); } // 5. Detect skipped issues file let skippedIssuesPath: string | undefined; // Check config first try { const stateManager = getStateManager(); const configuredSkipPath = stateManager.getState().config.skippedIssuesPath; if (configuredSkipPath && fs.existsSync(configuredSkipPath)) { skippedIssuesPath = configuredSkipPath; } } catch (err) { // State access can fail on a degraded state file (corrupt JSON, EACCES). // Default-path probe below still runs; warn so the underlying cause is visible (#994). // Matches the sibling warn at line 64 for `issueListPath` read failures. warn('startup', `Could not read skippedIssuesPath from state: ${errorMessage(err)}`); } // Probe default path: same directory as issue list, named skipped-issues.md if (!skippedIssuesPath && issueListPath) { const defaultSkipPath = path.join(path.dirname(issueListPath), 'skipped-issues.md'); if (fs.existsSync(defaultSkipPath)) { skippedIssuesPath = defaultSkipPath; } } return { path: issueListPath, source, availableCount, completedCount, skippedIssuesPath }; }