source dump of claude code
at main 316 lines 12 kB view raw
1/** 2 * Teleported /ultrareview execution. Creates a CCR session with the current repo, 3 * sends the review prompt as the initial message, and registers a 4 * RemoteAgentTask so the polling loop pipes results back into the local 5 * session via task-notification. Mirrors the /ultraplan → CCR flow. 6 * 7 * TODO(#22051): pass useBundleMode once landed so local-only / uncommitted 8 * repo state is captured. The GitHub-clone path (current) only works for 9 * pushed branches on repos with the Claude GitHub app installed. 10 */ 11 12import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js' 13import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' 14import { 15 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 16 logEvent, 17} from '../../services/analytics/index.js' 18import { fetchUltrareviewQuota } from '../../services/api/ultrareviewQuota.js' 19import { fetchUtilization } from '../../services/api/usage.js' 20import type { ToolUseContext } from '../../Tool.js' 21import { 22 checkRemoteAgentEligibility, 23 formatPreconditionError, 24 getRemoteTaskSessionUrl, 25 registerRemoteAgentTask, 26} from '../../tasks/RemoteAgentTask/RemoteAgentTask.js' 27import { isEnterpriseSubscriber, isTeamSubscriber } from '../../utils/auth.js' 28import { detectCurrentRepositoryWithHost } from '../../utils/detectRepository.js' 29import { execFileNoThrow } from '../../utils/execFileNoThrow.js' 30import { getDefaultBranch, gitExe } from '../../utils/git.js' 31import { teleportToRemote } from '../../utils/teleport.js' 32 33// One-time session flag: once the user confirms overage billing via the 34// dialog, all subsequent /ultrareview invocations in this session proceed 35// without re-prompting. 36let sessionOverageConfirmed = false 37 38export function confirmOverage(): void { 39 sessionOverageConfirmed = true 40} 41 42export type OverageGate = 43 | { kind: 'proceed'; billingNote: string } 44 | { kind: 'not-enabled' } 45 | { kind: 'low-balance'; available: number } 46 | { kind: 'needs-confirm' } 47 48/** 49 * Determine whether the user can launch an ultrareview and under what 50 * billing terms. Fetches quota and utilization in parallel. 51 */ 52export async function checkOverageGate(): Promise<OverageGate> { 53 // Team and Enterprise plans include ultrareview — no free-review quota 54 // or Extra Usage dialog. The quota endpoint is scoped to consumer plans 55 // (pro/max); hitting it on team/ent would surface a confusing dialog. 56 if (isTeamSubscriber() || isEnterpriseSubscriber()) { 57 return { kind: 'proceed', billingNote: '' } 58 } 59 60 const [quota, utilization] = await Promise.all([ 61 fetchUltrareviewQuota(), 62 fetchUtilization().catch(() => null), 63 ]) 64 65 // No quota info (non-subscriber or endpoint down) — let it through, 66 // server-side billing will handle it. 67 if (!quota) { 68 return { kind: 'proceed', billingNote: '' } 69 } 70 71 if (quota.reviews_remaining > 0) { 72 return { 73 kind: 'proceed', 74 billingNote: ` This is free ultrareview ${quota.reviews_used + 1} of ${quota.reviews_limit}.`, 75 } 76 } 77 78 // Utilization fetch failed (transient network error, timeout, etc.) — 79 // let it through, same rationale as the quota fallback above. 80 if (!utilization) { 81 return { kind: 'proceed', billingNote: '' } 82 } 83 84 // Free reviews exhausted — check Extra Usage setup. 85 const extraUsage = utilization.extra_usage 86 if (!extraUsage?.is_enabled) { 87 logEvent('tengu_review_overage_not_enabled', {}) 88 return { kind: 'not-enabled' } 89 } 90 91 // Check available balance (null monthly_limit = unlimited). 92 const monthlyLimit = extraUsage.monthly_limit 93 const usedCredits = extraUsage.used_credits ?? 0 94 const available = 95 monthlyLimit === null || monthlyLimit === undefined 96 ? Infinity 97 : monthlyLimit - usedCredits 98 99 if (available < 10) { 100 logEvent('tengu_review_overage_low_balance', { available }) 101 return { kind: 'low-balance', available } 102 } 103 104 if (!sessionOverageConfirmed) { 105 logEvent('tengu_review_overage_dialog_shown', {}) 106 return { kind: 'needs-confirm' } 107 } 108 109 return { 110 kind: 'proceed', 111 billingNote: ' This review bills as Extra Usage.', 112 } 113} 114 115/** 116 * Launch a teleported review session. Returns ContentBlockParam[] describing 117 * the launch outcome for injection into the local conversation (model is then 118 * queried with this content, so it can narrate the launch to the user). 119 * 120 * Returns ContentBlockParam[] with user-facing error messages on recoverable 121 * failures (missing merge-base, empty diff, bundle too large), or null on 122 * other failures so the caller falls through to the local-review prompt. 123 * Reason is captured in analytics. 124 * 125 * Caller must run checkOverageGate() BEFORE calling this function 126 * (ultrareviewCommand.tsx handles the dialog). 127 */ 128export async function launchRemoteReview( 129 args: string, 130 context: ToolUseContext, 131 billingNote?: string, 132): Promise<ContentBlockParam[] | null> { 133 const eligibility = await checkRemoteAgentEligibility() 134 // Synthetic DEFAULT_CODE_REVIEW_ENVIRONMENT_ID works without per-org CCR 135 // setup, so no_remote_environment isn't a blocker. Server-side quota 136 // consume at session creation routes billing: first N zero-rate, then 137 // anthropic:cccr org-service-key (overage-only). 138 if (!eligibility.eligible) { 139 const blockers = eligibility.errors.filter( 140 e => e.type !== 'no_remote_environment', 141 ) 142 if (blockers.length > 0) { 143 logEvent('tengu_review_remote_precondition_failed', { 144 precondition_errors: blockers 145 .map(e => e.type) 146 .join( 147 ',', 148 ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 149 }) 150 const reasons = blockers.map(formatPreconditionError).join('\n') 151 return [ 152 { 153 type: 'text', 154 text: `Ultrareview cannot launch:\n${reasons}`, 155 }, 156 ] 157 } 158 } 159 160 const resolvedBillingNote = billingNote ?? '' 161 162 const prNumber = args.trim() 163 const isPrNumber = /^\d+$/.test(prNumber) 164 // Synthetic code_review env. Go taggedid.FromUUID(TagEnvironment, 165 // UUID{...,0x02}) encodes with version prefix '01' — NOT Python's 166 // legacy tagged_id() format. Verified in prod. 167 const CODE_REVIEW_ENV_ID = 'env_011111111111111111111113' 168 // Lite-review bypasses bughunter.go entirely, so it doesn't see the 169 // webhook's bug_hunter_config (different GB project). These env vars are 170 // the only tuning surface — without them, run_hunt.sh's bash defaults 171 // apply (60min, 120s agent timeout), and 120s kills verifiers mid-run 172 // which causes infinite respawn. 173 // 174 // total_wallclock must stay below RemoteAgentTask's 30min poll timeout 175 // with headroom for finalization (~3min synthesis). Per-field guards 176 // match autoDream.ts — GB cache can return stale wrong-type values. 177 const raw = getFeatureValue_CACHED_MAY_BE_STALE<Record< 178 string, 179 unknown 180 > | null>('tengu_review_bughunter_config', null) 181 const posInt = (v: unknown, fallback: number, max?: number): number => { 182 if (typeof v !== 'number' || !Number.isFinite(v)) return fallback 183 const n = Math.floor(v) 184 if (n <= 0) return fallback 185 return max !== undefined && n > max ? fallback : n 186 } 187 // Upper bounds: 27min on wallclock leaves ~3min for finalization under 188 // RemoteAgentTask's 30min poll timeout. If GB is set above that, the 189 // hang we're fixing comes back — fall to the safe default instead. 190 const commonEnvVars = { 191 BUGHUNTER_DRY_RUN: '1', 192 BUGHUNTER_FLEET_SIZE: String(posInt(raw?.fleet_size, 5, 20)), 193 BUGHUNTER_MAX_DURATION: String(posInt(raw?.max_duration_minutes, 10, 25)), 194 BUGHUNTER_AGENT_TIMEOUT: String( 195 posInt(raw?.agent_timeout_seconds, 600, 1800), 196 ), 197 BUGHUNTER_TOTAL_WALLCLOCK: String( 198 posInt(raw?.total_wallclock_minutes, 22, 27), 199 ), 200 ...(process.env.BUGHUNTER_DEV_BUNDLE_B64 && { 201 BUGHUNTER_DEV_BUNDLE_B64: process.env.BUGHUNTER_DEV_BUNDLE_B64, 202 }), 203 } 204 205 let session 206 let command 207 let target 208 if (isPrNumber) { 209 // PR mode: refs/pull/N/head via github.com. Orchestrator --pr N. 210 const repo = await detectCurrentRepositoryWithHost() 211 if (!repo || repo.host !== 'github.com') { 212 logEvent('tengu_review_remote_precondition_failed', {}) 213 return null 214 } 215 session = await teleportToRemote({ 216 initialMessage: null, 217 description: `ultrareview: ${repo.owner}/${repo.name}#${prNumber}`, 218 signal: context.abortController.signal, 219 branchName: `refs/pull/${prNumber}/head`, 220 environmentId: CODE_REVIEW_ENV_ID, 221 environmentVariables: { 222 BUGHUNTER_PR_NUMBER: prNumber, 223 BUGHUNTER_REPOSITORY: `${repo.owner}/${repo.name}`, 224 ...commonEnvVars, 225 }, 226 }) 227 command = `/ultrareview ${prNumber}` 228 target = `${repo.owner}/${repo.name}#${prNumber}` 229 } else { 230 // Branch mode: bundle the working tree, orchestrator diffs against 231 // the fork point. No PR, no existing comments, no dedup. 232 const baseBranch = (await getDefaultBranch()) || 'main' 233 // Env-manager's `git remote remove origin` after bundle-clone 234 // deletes refs/remotes/origin/* — the base branch name won't resolve 235 // in the container. Pass the merge-base SHA instead: it's reachable 236 // from HEAD's history so `git diff <sha>` works without a named ref. 237 const { stdout: mbOut, code: mbCode } = await execFileNoThrow( 238 gitExe(), 239 ['merge-base', baseBranch, 'HEAD'], 240 { preserveOutputOnError: false }, 241 ) 242 const mergeBaseSha = mbOut.trim() 243 if (mbCode !== 0 || !mergeBaseSha) { 244 logEvent('tengu_review_remote_precondition_failed', {}) 245 return [ 246 { 247 type: 'text', 248 text: `Could not find merge-base with ${baseBranch}. Make sure you're in a git repo with a ${baseBranch} branch.`, 249 }, 250 ] 251 } 252 253 // Bail early on empty diffs instead of launching a container that 254 // will just echo "no changes". 255 const { stdout: diffStat, code: diffCode } = await execFileNoThrow( 256 gitExe(), 257 ['diff', '--shortstat', mergeBaseSha], 258 { preserveOutputOnError: false }, 259 ) 260 if (diffCode === 0 && !diffStat.trim()) { 261 logEvent('tengu_review_remote_precondition_failed', {}) 262 return [ 263 { 264 type: 'text', 265 text: `No changes against the ${baseBranch} fork point. Make some commits or stage files first.`, 266 }, 267 ] 268 } 269 270 session = await teleportToRemote({ 271 initialMessage: null, 272 description: `ultrareview: ${baseBranch}`, 273 signal: context.abortController.signal, 274 useBundle: true, 275 environmentId: CODE_REVIEW_ENV_ID, 276 environmentVariables: { 277 BUGHUNTER_BASE_BRANCH: mergeBaseSha, 278 ...commonEnvVars, 279 }, 280 }) 281 if (!session) { 282 logEvent('tengu_review_remote_teleport_failed', {}) 283 return [ 284 { 285 type: 'text', 286 text: 'Repo is too large. Push a PR and use `/ultrareview <PR#>` instead.', 287 }, 288 ] 289 } 290 command = '/ultrareview' 291 target = baseBranch 292 } 293 294 if (!session) { 295 logEvent('tengu_review_remote_teleport_failed', {}) 296 return null 297 } 298 registerRemoteAgentTask({ 299 remoteTaskType: 'ultrareview', 300 session, 301 command, 302 context, 303 isRemoteReview: true, 304 }) 305 logEvent('tengu_review_remote_launched', {}) 306 const sessionUrl = getRemoteTaskSessionUrl(session.id) 307 // Concise — the tool-output block is visible to the user, so the model 308 // shouldn't echo the same info. Just enough for Claude to acknowledge the 309 // launch without restating the target/URL (both already printed above). 310 return [ 311 { 312 type: 'text', 313 text: `Ultrareview launched for ${target} (~10–20 min, runs in the cloud). Track: ${sessionUrl}${resolvedBillingNote} Findings arrive via task-notification. Briefly acknowledge the launch to the user without repeating the target or URL — both are already visible in the tool output above.`, 314 }, 315 ] 316}