{"version":3,"file":"HttpServerManager-X21a8LMu.mjs","names":["currentDir","execFileAsync","HTTP_SERVE_COMMAND","path","getPlaywrightPort","resolveWorkspaceRoot","getPlaywrightHost","BROWSE_TOOL_WORKSPACE_ROOT_ENV","allowGlobalHttpReuse","killProcessTree","getKillGraceMs","getShutdownGraceMs","getClaimTtlMs","getStartupWaitMs","withoutAgentIdentity","path","path$1","currentDir$1","fileURLToPath","cachedVersion","getPackageVersion","candidates","candidate","existsSync","parsed","readFileSync","isCompatibleVersion","discovered","compareVersions","coreA","preA","a","coreB","preB","b","numsA","numsB","i","diff","partsA","partsB","pa","pb","na","nb","execFileAsync$1","promisify","execFile","getProcessCommandLine","pid","stdout","matchesDaemonCommandLine","commandLine","port","isVerifiedDaemonProcess","getProcessGroupId","processGroupId","resolveSelfProcessIdentity","processStartedAt","leadsOwnGroup","currentFile","currentDir","SERVICE_NAME","SERVICE_TYPE","HTTP_SERVE_COMMAND","CLAIM_TOKEN_ENV","DAEMON_ENV_NAMES","DAEMON_ENV_PREFIXES","CALLER_ENV_NAMES","buildDaemonEnv","allowed","name","value","env","upper","prefix","NODE_COMMAND","DIST_CLI_FILENAME","SRC_CLI_FILENAME","LOG_PREFIX","HttpServerPortError","message","options","HttpServerStartupError","HttpServerVersionMismatchError","HttpServerStopError","causes","execFileAsync","delay","resolve","ms","pathExists","targetPath","resolveGitCommonRoot","startPath","gitCommonDir","resolved","repoRoot","HttpServerManager","ProcessRegistryService","healthCheck","portRegistry","processRegistry","error","repositoryPath","baseQuery","scoped","gitCommonRoot","fromRoot","entry","processResponse","response","entries","result","registration","errors","healthResult","checkError","msg","distCliPath","srcCliPath","portArgs","packageRoot","fallbackDistCli","fallbackSrcCli","command","args","logFd","openSync","tmpdir","child","spawn","SKIP_DYNAMIC_TOOLS_ENV","claimToken","closeSync","spawnError","timer","err","deadline","timeoutMs","discoveredVersion","localVersion","VERSION_STRICT_ENV","claim","preferredPort","healthy","claimedPort","releaseClaim","discoveredOnPort","startupError","nextPort","attempt","collectedErrors","wrappedError","messages","staleEntry"],"sources":["../src/utils/packageVersion.ts","../src/utils/processIdentity.ts","../src/services/HttpServerManager.ts"],"sourcesContent":["/**\n * Runtime resolution of this package's version.\n *\n * The daemon is shared across worktrees, so a client must be able to tell whether\n * the daemon it discovered was built from the same code it is running. The version\n * is read from package.json at runtime rather than injected at build time so it\n * works identically from `src/` (bun) and `dist/` (node).\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst UNKNOWN_VERSION = 'unknown';\n\nconst currentDir = path.dirname(fileURLToPath(import.meta.url));\n\nlet cachedVersion: string | undefined;\n\n/**\n * Resolve this package's version, or `'unknown'` when package.json cannot be read.\n * Never throws: a missing version degrades to a version mismatch, not a crash.\n */\nexport function getPackageVersion(): string {\n  if (cachedVersion) {\n    return cachedVersion;\n  }\n\n  // dist/chunk.mjs -> ../package.json; src/utils/x.ts -> ../../package.json\n  const candidates = [\n    path.resolve(currentDir, '..', 'package.json'),\n    path.resolve(currentDir, '..', '..', 'package.json'),\n    path.resolve(currentDir, '..', '..', '..', 'package.json'),\n  ];\n\n  for (const candidate of candidates) {\n    if (!existsSync(candidate)) {\n      continue;\n    }\n\n    try {\n      const parsed = JSON.parse(readFileSync(candidate, 'utf-8')) as { name?: string; version?: string };\n      if (parsed.name === '@agimon-ai/po-browse-tool' && parsed.version) {\n        cachedVersion = parsed.version;\n        return cachedVersion;\n      }\n    } catch {\n      // Try the next candidate; an unreadable manifest is not fatal.\n    }\n  }\n\n  cachedVersion = UNKNOWN_VERSION;\n  return cachedVersion;\n}\n\n/** True when a discovered daemon reports a version this build can safely reuse. */\nexport function isCompatibleVersion(discovered: string | undefined): boolean {\n  return discovered !== undefined && discovered === getPackageVersion();\n}\n\n/**\n * Semver-style comparison including prerelease tags (`0.0.0-alpha.16`).\n * Returns a positive number when `a` is newer than `b`, negative when older, 0 when equal.\n * Unparseable input compares as equal so callers fall back to their mismatch path.\n */\nexport function compareVersions(a: string, b: string): number {\n  const [coreA = '', preA] = a.split('-', 2);\n  const [coreB = '', preB] = b.split('-', 2);\n  const numsA = coreA.split('.').map(Number);\n  const numsB = coreB.split('.').map(Number);\n  if ([...numsA, ...numsB].some(Number.isNaN)) return 0;\n  for (let i = 0; i < 3; i++) {\n    const diff = (numsA[i] ?? 0) - (numsB[i] ?? 0);\n    if (diff !== 0) return diff;\n  }\n  // A release outranks any prerelease of the same core version.\n  if (preA === undefined || preB === undefined) return preA === preB ? 0 : preA === undefined ? 1 : -1;\n  const partsA = preA.split('.');\n  const partsB = preB.split('.');\n  for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {\n    const pa = partsA[i];\n    const pb = partsB[i];\n    if (pa === undefined || pb === undefined) return pa === undefined ? -1 : 1;\n    const na = Number(pa);\n    const nb = Number(pb);\n    const diff = Number.isNaN(na) || Number.isNaN(nb) ? pa.localeCompare(pb) : na - nb;\n    if (diff !== 0) return diff;\n  }\n  return 0;\n}\n","/**\n * Process identity verification.\n *\n * A registry row outlives the process it describes: a crashed or SIGKILLed daemon\n * leaves its pid behind, and the OS is free to hand that pid to something else.\n * Signalling a pid read from the registry without first checking what it actually\n * is can therefore kill an unrelated process, so every kill path verifies the\n * command line first.\n */\n\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\n\nimport type { ProcessTerminationMode } from '@agimon-ai/foundation-process-registry';\n\nconst execFileAsync = promisify(execFile);\n\n/** The CLI sub-command every browse-tool HTTP daemon is started with. */\nconst HTTP_SERVE_COMMAND = 'http-serve';\n\n/** Registry fields that let a later release signal the right thing. */\nexport interface SelfProcessIdentity {\n  processStartedAt: string;\n  processGroupId?: number;\n  terminationMode: ProcessTerminationMode;\n}\n\n/**\n * Read a process's full command line, or `''` when the process is gone or\n * unreadable. Never throws — callers treat an empty result as \"unverified\".\n */\nexport async function getProcessCommandLine(pid: number): Promise<string> {\n  try {\n    const { stdout } = await execFileAsync('ps', ['-p', String(pid), '-o', 'command=']);\n    return stdout.trim();\n  } catch {\n    return '';\n  }\n}\n\n/**\n * Whether a command line belongs to a browse-tool HTTP daemon serving `port`.\n *\n * The daemon is always spawned as `node <…>/cli.mjs http-serve --port N …` or\n * `bun run <…>/cli.ts http-serve --port N …`, so the sub-command plus the port\n * flag identify it unambiguously and survive both runtimes.\n */\nexport function matchesDaemonCommandLine(commandLine: string, port: number): boolean {\n  if (!commandLine) {\n    return false;\n  }\n\n  return commandLine.includes(HTTP_SERVE_COMMAND) && new RegExp(`--port(?:=|\\\\s+)${port}(?:\\\\s|$)`).test(commandLine);\n}\n\n/** Resolve a pid's command line and check it against {@link matchesDaemonCommandLine}. */\nexport async function isVerifiedDaemonProcess(pid: number, port: number): Promise<boolean> {\n  return matchesDaemonCommandLine(await getProcessCommandLine(pid), port);\n}\n\n/** Read a process's group id, or `undefined` when it cannot be determined. */\nasync function getProcessGroupId(pid: number): Promise<number | undefined> {\n  try {\n    const { stdout } = await execFileAsync('ps', ['-p', String(pid), '-o', 'pgid=']);\n    const processGroupId = Number.parseInt(stdout.trim(), 10);\n    return Number.isInteger(processGroupId) && processGroupId > 0 ? processGroupId : undefined;\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * Describe this process for the registry so a release can take its children down\n * with it. Chrome is spawned by the daemon, so signalling the bare pid leaves the\n * browser and its helpers running.\n *\n * A process group is only claimed when this process leads its own group. The\n * daemon is spawned detached, so it does; one started by hand shares its shell's\n * group, and signalling that group would kill the shell too.\n */\nexport async function resolveSelfProcessIdentity(): Promise<SelfProcessIdentity> {\n  const processStartedAt = new Date(Date.now() - process.uptime() * 1000).toISOString();\n  const processGroupId = await getProcessGroupId(process.pid);\n  const leadsOwnGroup = processGroupId === process.pid;\n\n  return {\n    processStartedAt,\n    processGroupId: leadsOwnGroup ? processGroupId : undefined,\n    terminationMode: leadsOwnGroup ? 'process-group' : 'direct',\n  };\n}\n","/**\n * HttpServerManager\n *\n * Lifecycle management service for singleton HTTP server coordination.\n * Handles server discovery, startup, shutdown, and health monitoring.\n *\n * Uses repositoryPath-scoped service discovery via listAllocations to prevent\n * cross-repo pollution. All discovery is additionally scoped by environment\n * to prevent cross-environment interference.\n *\n * Environment variables:\n * - NODE_ENV: Determines the runtime environment (default: 'development').\n *   All registry queries are scoped to this environment value.\n * - PORT_REGISTRY_PATH: Custom path for the port registry file (optional).\n *   When unset, PortRegistryService uses its default location.\n * - PLAYWRIGHT_SKIP_DYNAMIC_TOOL_COMMANDS: Set to '1' on spawned child servers\n *   to skip dynamic tool command loading during startup.\n */\n\nimport { execFile, spawn } from 'node:child_process';\nimport { closeSync, existsSync, openSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { promisify } from 'node:util';\n\nimport type { PortRegistryRecord, PortRegistryService } from '@agimon-ai/foundation-port-registry';\nimport { ProcessRegistryService } from '@agimon-ai/foundation-process-registry';\n\nimport {\n  getClaimTtlMs,\n  getKillGraceMs,\n  getShutdownGraceMs,\n  getStartupWaitMs,\n  HEALTH_POLL_INTERVAL_MS,\n} from '../constants/timeouts.js';\nimport { getPlaywrightHost, getPlaywrightPort } from '../utils/networkConfig.js';\nimport { compareVersions, getPackageVersion, isCompatibleVersion } from '../utils/packageVersion.js';\nimport { isVerifiedDaemonProcess } from '../utils/processIdentity.js';\nimport { killProcessTree } from '../utils/processTree.js';\nimport { withoutAgentIdentity } from '../utils/telemetryContext.js';\nimport { allowGlobalHttpReuse, BROWSE_TOOL_WORKSPACE_ROOT_ENV, resolveWorkspaceRoot } from '../utils/workspaceRoot.js';\nimport type { HealthCheckResult, HttpServerHealthCheck } from './HttpServerHealthCheck.js';\n\nconst currentFile = fileURLToPath(import.meta.url);\nconst currentDir = path.dirname(currentFile);\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Env var set on spawned child servers to skip dynamic tool loading */\nconst SKIP_DYNAMIC_TOOLS_ENV = 'PLAYWRIGHT_SKIP_DYNAMIC_TOOL_COMMANDS';\n\n/** Value that enables the SKIP_DYNAMIC_TOOLS_ENV flag */\nconst SKIP_DYNAMIC_TOOLS_ENABLED = '1';\n\n/** Service name used for port registry entries */\nconst SERVICE_NAME = 'browse-tool-http';\n\n/** Service type used for port registry entries */\nconst SERVICE_TYPE = 'tool' as const;\n\n/** Service type used for process registry entries */\nconst PROCESS_SERVICE_TYPE = 'service' as const;\n\n/** CLI sub-command used to start the HTTP server */\nconst HTTP_SERVE_COMMAND = 'http-serve';\n\n/** Maximum number of spawn attempts before giving up */\nconst MAX_SPAWN_ATTEMPTS = 3;\n\n/** Milliseconds to wait for spawn error events before considering spawn successful */\nconst SPAWN_ERROR_GRACE_MS = 100;\n\n/** Env var carrying the bootstrap claim from the spawning parent to the daemon. */\nexport const CLAIM_TOKEN_ENV = 'BROWSE_TOOL_CLAIM_TOKEN';\n\n/** Env names the shared daemon inherits from whichever caller spawned it. */\nconst DAEMON_ENV_NAMES = new Set([\n  'PATH',\n  'HOME',\n  'USER',\n  'LOGNAME',\n  'SHELL',\n  'TMPDIR',\n  'TEMP',\n  'TMP',\n  'LANG',\n  'TZ',\n  'DISPLAY',\n  'WAYLAND_DISPLAY',\n  'XDG_RUNTIME_DIR',\n  'XDG_CONFIG_HOME',\n  'XDG_CACHE_HOME',\n  'XDG_DATA_HOME',\n  'NODE_ENV',\n  'NODE_EXTRA_CA_CERTS',\n  'PORT_REGISTRY_PATH',\n  'PROCESS_REGISTRY_PATH',\n  'CHROME_BIN',\n  'HTTP_PROXY',\n  'HTTPS_PROXY',\n  'NO_PROXY',\n  'ALL_PROXY',\n  '__HOOKS_SOURCE_DIR__',\n  // Windows\n  'APPDATA',\n  'LOCALAPPDATA',\n  'PROGRAMFILES',\n  'SYSTEMROOT',\n  'USERPROFILE',\n  'COMSPEC',\n]);\nconst DAEMON_ENV_PREFIXES = ['LC_', 'BROWSE_TOOL_', 'PLAYWRIGHT_', 'CLOAKBROWSER_', 'OTEL_', 'DOCKER_'];\n/** Per-caller identity; it reaches the daemon per request as headers. */\nconst CALLER_ENV_NAMES = new Set(['BROWSE_TOOL_OWNER', 'BROWSE_TOOL_PROFILE']);\n\n/**\n * The daemon outlives its spawner and serves every caller, so it must not\n * inherit the first caller's whole env (profile secrets, tokens). Keep only the\n * names it needs; agent identity is dropped as well and arrives per request.\n */\nexport function buildDaemonEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n  const allowed: NodeJS.ProcessEnv = {};\n  for (const [name, value] of Object.entries(env)) {\n    const upper = name.toUpperCase();\n    if (CALLER_ENV_NAMES.has(upper)) {\n      continue;\n    }\n    if (DAEMON_ENV_NAMES.has(upper) || DAEMON_ENV_PREFIXES.some((prefix) => upper.startsWith(prefix))) {\n      allowed[name] = value;\n    }\n  }\n  return withoutAgentIdentity(allowed);\n}\n\n/** Set to '1' to fail rather than reuse a daemon built from a different version. */\nconst VERSION_STRICT_ENV = 'BROWSE_TOOL_VERSION_STRICT';\n\n/** CLI argument flags */\nconst CLI_FLAG_PORT = '--port';\nconst CLI_FLAG_HOST = '--host';\n\n/** Default environment when NODE_ENV is not set */\nconst DEFAULT_ENVIRONMENT = 'development';\n\n/** Node runtime command */\nconst NODE_COMMAND = 'node';\n\n/** Bun runtime command */\nconst BUN_COMMAND = 'bun';\n\n/** Bun run subcommand */\nconst BUN_RUN_SUBCOMMAND = 'run';\n\n/** Dist CLI filename */\nconst DIST_CLI_FILENAME = 'cli.mjs';\n\n/** Source CLI filename */\nconst SRC_CLI_FILENAME = 'cli.ts';\n\n/** Dist directory name */\nconst DIST_DIR = 'dist';\n\n/** Source directory name */\nconst SRC_DIR = 'src';\n\n// Error codes and messages for structured status responses\nconst ERROR_CODE_STALE_ENTRY = 'HTTP_SERVER_STALE_ENTRY';\nconst ERROR_CODE_NOT_REGISTERED = 'HTTP_SERVER_NOT_REGISTERED';\nconst ERROR_CODE_VERSION_MISMATCH = 'HTTP_SERVER_VERSION_MISMATCH';\nconst ERROR_MSG_STALE = 'Server registered but not healthy';\nconst ERROR_MSG_NOT_REGISTERED = 'No HTTP server registered';\nconst ERROR_RECOVERY_STALE = 'Run ensureRunning() to start a fresh server or clean up stale entries.';\nconst ERROR_RECOVERY_NOT_REGISTERED = 'Call ensureRunning() to discover or spawn a server.';\n\n/** Sentinel string from PortRegistryService when no entry matches a release */\nconst REGISTRY_NO_MATCH_SENTINEL = 'No matching registry entry';\n\n/** Sentinel string from ProcessRegistryService when no entry matches a release */\nconst PROCESS_REGISTRY_NO_MATCH_SENTINEL = 'No matching process entry';\n\n/** Log prefix for all console output from this service */\nconst LOG_PREFIX = '[HttpServerManager]';\n\n// ============================================================================\n// Custom Error Classes\n// ============================================================================\n\n/**\n * Error thrown when no available port can be found for the HTTP server.\n */\nexport class HttpServerPortError extends Error {\n  readonly code = 'HTTP_SERVER_PORT_ERROR';\n  readonly recovery = 'Check if other services are occupying ports in the configured range.';\n\n  constructor(message: string, options?: ErrorOptions) {\n    super(message, options);\n    this.name = 'HttpServerPortError';\n  }\n}\n\n/**\n * Error thrown when the HTTP server fails to start after all retry attempts.\n */\nexport class HttpServerStartupError extends Error {\n  readonly code = 'HTTP_SERVER_STARTUP_ERROR';\n  readonly recovery = 'Check server logs, ensure CLI binary exists, and verify port availability.';\n\n  constructor(message: string, options?: ErrorOptions) {\n    super(message, options);\n    this.name = 'HttpServerStartupError';\n  }\n}\n\n/**\n * Error thrown when a discovered daemon was built from a different version and\n * cannot be restarted because it is still serving browsers.\n */\nexport class HttpServerVersionMismatchError extends Error {\n  readonly code = ERROR_CODE_VERSION_MISMATCH;\n  readonly recovery = 'Close the running browsers and re-run, or restart the daemon with `browse-tool stop`.';\n\n  constructor(message: string, options?: ErrorOptions) {\n    super(message, options);\n    this.name = 'HttpServerVersionMismatchError';\n  }\n}\n\n/**\n * Error thrown when stopping the HTTP server fails.\n * Carries an array of root-cause errors from individual entry stop failures.\n */\nexport class HttpServerStopError extends Error {\n  readonly code = 'HTTP_SERVER_STOP_ERROR';\n  readonly recovery = 'Manually kill the process and clean up the port registry.';\n  readonly causes: Error[];\n\n  /**\n   * @param message - Aggregated error description\n   * @param causes - Array of root-cause errors from per-entry failures\n   * @param options - Standard ErrorOptions for cause chaining\n   */\n  constructor(message: string, causes: Error[] = [], options?: ErrorOptions) {\n    super(message, options);\n    this.name = 'HttpServerStopError';\n    this.causes = causes;\n  }\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\nconst execFileAsync = promisify(execFile);\n\nconst delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nfunction pathExists(targetPath: string): boolean {\n  return existsSync(targetPath);\n}\n\n/**\n * Resolve the main worktree root for the given path.\n * In a linked worktree, `git rev-parse --git-common-dir` returns the path to\n * the main repo's `.git` directory; its parent is the main worktree root.\n * In the main worktree, it returns `.git`, so we fall back to the given path.\n *\n * @param startPath - Directory to start from\n * @returns The main worktree root, or `startPath` if not in a git repo / not a worktree\n */\nasync function resolveGitCommonRoot(startPath: string): Promise<string | null> {\n  try {\n    const { stdout } = await execFileAsync('git', ['rev-parse', '--git-common-dir'], { cwd: startPath });\n    const gitCommonDir = stdout.trim();\n    // In the main worktree this is just \".git\" (relative); in a linked worktree it's an absolute path\n    const resolved = path.isAbsolute(gitCommonDir) ? gitCommonDir : path.join(startPath, gitCommonDir);\n    const repoRoot = path.dirname(resolved);\n    return repoRoot !== startPath ? repoRoot : null;\n  } catch {\n    return null;\n  }\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * HTTP Server status returned by lifecycle operations.\n * Includes structured error fields when the server is not running.\n */\nexport interface HttpServerStatus {\n  /** Whether the server is running */\n  running: boolean;\n  /** Port number if running */\n  port?: number;\n  /** Process ID if running */\n  pid?: number;\n  /** Browser count if available */\n  browserCount?: number;\n  /** Whether the server was spawned (true) or reused (false) */\n  spawned?: boolean;\n  /** Human-readable error message if not running */\n  error?: string;\n  /** Machine-readable error code for programmatic branching */\n  errorCode?: string;\n  /** Recovery suggestion for the error */\n  errorRecovery?: string;\n  /** Root cause error message when wrapping an inner error */\n  errorCause?: string;\n}\n\nexport interface StopOptions {\n  /**\n   * When true, stop daemons registered under any repositoryPath rather than\n   * only the current worktree's.\n   */\n  allWorktrees?: boolean;\n}\n\nexport interface EnsureRunningOptions {\n  /**\n   * When true, only reuse or spawn the requested port.\n   * Do not silently fall back to another healthy registered instance.\n   */\n  exactPort?: boolean;\n}\n\n/** Reason a spawn attempt failed, used for structured control flow. */\ntype SpawnFailureReason =\n  | 'health_check_failed'\n  | 'health_check_error'\n  | 'service_name_mismatch'\n  | 'claim_lost'\n  | 'claim_unavailable'\n  | 'startup_timeout';\n\n/** Result of a single spawn attempt. */\ntype SpawnAttemptResult =\n  | { success: true; port: number; pid: number; browserCount?: number; spawned: boolean }\n  | { success: false; reason: SpawnFailureReason; port: number; error?: string };\n\n/** A healthy daemon found in the registry. */\ninterface DiscoveredService {\n  entry: PortRegistryRecord;\n  browserCount?: number;\n  version?: string;\n}\n\n/**\n * What to do with a discovered daemon whose version does not match this build.\n * `reuse` also covers the matching case.\n */\ntype VersionDecision = 'reuse' | 'replace';\n\n// ============================================================================\n// Service\n// ============================================================================\n\n/**\n * HTTP Server Manager Service\n *\n * Manages singleton HTTP server lifecycle with service discovery and health checks.\n * Discovery is scoped by both repositoryPath and environment to prevent cross-repo\n * and cross-environment interference.\n */\nexport class HttpServerManager {\n  private repositoryPath: string | undefined;\n  private readonly environment: string;\n  private readonly host: string;\n\n  /**\n   * @param healthCheck - Injected health check service for verifying server liveness\n   * @param portRegistry - Injected port registry service for service discovery and allocation\n   */\n  constructor(\n    private readonly healthCheck: HttpServerHealthCheck,\n    private readonly portRegistry: PortRegistryService,\n    private readonly processRegistry: ProcessRegistryService = new ProcessRegistryService(\n      process.env.PROCESS_REGISTRY_PATH,\n    ),\n  ) {\n    this.environment = process.env.NODE_ENV || DEFAULT_ENVIRONMENT;\n    this.host = getPlaywrightHost();\n  }\n\n  /**\n   * Lazily resolve and cache the workspace root path.\n   *\n   * @returns The resolved workspace root\n   */\n  private async getRepositoryPath(): Promise<string> {\n    if (!this.repositoryPath) {\n      this.repositoryPath = resolveWorkspaceRoot();\n    }\n    return this.repositoryPath;\n  }\n\n  /**\n   * Build a structured error status from a caught error, preserving\n   * error code, recovery, and cause fields from custom error classes.\n   *\n   * @param error - The caught error value\n   * @returns An HttpServerStatus with structured error details\n   */\n  private buildErrorStatus(error: unknown): HttpServerStatus {\n    const message = error instanceof Error ? error.message : String(error);\n    const errorCode = error instanceof Error && 'code' in error ? (error as { code: string }).code : undefined;\n    const errorRecovery =\n      error instanceof Error && 'recovery' in error ? (error as { recovery: string }).recovery : undefined;\n    const errorCause = error instanceof Error && error.cause instanceof Error ? error.cause.message : undefined;\n\n    return { running: false, error: message, errorCode, errorRecovery, errorCause };\n  }\n\n  /**\n   * List registry entries for this service using a scoped fallback chain:\n   * 1. Current repositoryPath (worktree or repo root)\n   * 2. Main worktree root (if running inside a linked git worktree)\n   * 3. Global only when explicitly enabled for compatibility\n   *\n   * This prevents cross-repo pollution while still supporting git worktrees.\n   *\n   * @returns Array of matching registry records\n   */\n  private async listServiceEntries(): Promise<PortRegistryRecord[]> {\n    const repositoryPath = await this.getRepositoryPath();\n    const baseQuery = {\n      serviceName: SERVICE_NAME,\n      serviceType: SERVICE_TYPE,\n      environment: this.environment,\n    };\n\n    // 1. Scoped to current path (worktree or main repo)\n    const scoped = await this.portRegistry.listAllocations({ ...baseQuery, repositoryPath });\n    if (scoped.length > 0) return scoped;\n\n    // 2. Main worktree root (handles linked worktrees)\n    const gitCommonRoot = await resolveGitCommonRoot(repositoryPath);\n    if (gitCommonRoot) {\n      const fromRoot = await this.portRegistry.listAllocations({ ...baseQuery, repositoryPath: gitCommonRoot });\n      if (fromRoot.length > 0) return fromRoot;\n    }\n\n    if (allowGlobalHttpReuse()) {\n      return this.portRegistry.listAllocations(baseQuery);\n    }\n\n    return [];\n  }\n\n  /**\n   * List registry entries for this service across every repositoryPath, scoped\n   * only by environment. Used by the explicit all-worktrees stop, which is the\n   * one operation that is meant to reach daemons this worktree cannot discover.\n   *\n   * @returns Array of matching registry records\n   */\n  private async listAllServiceEntries(): Promise<PortRegistryRecord[]> {\n    return this.portRegistry.listAllocations({\n      serviceName: SERVICE_NAME,\n      serviceType: SERVICE_TYPE,\n      environment: this.environment,\n    });\n  }\n\n  /**\n   * Release a registry entry for a specific port and PID, using the entry's own\n   * repositoryPath so the release matches the original registration.\n   *\n   * @param entry - The registry record to release\n   */\n  private async releaseEntry(entry: PortRegistryRecord): Promise<void> {\n    if (entry.pid) {\n      const processResponse = await this.processRegistry.releaseProcess({\n        repositoryPath: entry.repositoryPath,\n        serviceName: SERVICE_NAME,\n        serviceType: PROCESS_SERVICE_TYPE,\n        environment: entry.environment,\n        pid: entry.pid,\n        kill: false,\n        releasePort: false,\n      });\n\n      if (!processResponse.success && !processResponse.error?.includes(PROCESS_REGISTRY_NO_MATCH_SENTINEL)) {\n        console.error(`${LOG_PREFIX} Failed to release process entry for port ${entry.port}: ${processResponse.error}`);\n      }\n    }\n\n    const response = await this.portRegistry.releasePort({\n      repositoryPath: entry.repositoryPath,\n      serviceName: SERVICE_NAME,\n      serviceType: SERVICE_TYPE,\n      environment: entry.environment,\n      pid: entry.pid,\n    });\n\n    if (!response.success && !response.error?.includes(REGISTRY_NO_MATCH_SENTINEL)) {\n      console.error(`${LOG_PREFIX} Failed to release entry for port ${entry.port}: ${response.error}`);\n    }\n  }\n\n  /**\n   * Discover a healthy service instance scoped to the current repositoryPath and environment.\n   * Per-entry health check failures are treated as indeterminate and skipped.\n   *\n   * @returns The first healthy entry with browser count, or null if none found\n   */\n  private async discoverHealthyService(): Promise<DiscoveredService | null> {\n    const entries = await this.listServiceEntries();\n\n    for (const entry of entries) {\n      try {\n        const result = await this.healthCheck.check(entry.port);\n        if (result.healthy && result.serviceName === SERVICE_NAME) {\n          return { entry, browserCount: result.browserCount, version: result.serviceVersion };\n        }\n      } catch (error) {\n        console.error(\n          `${LOG_PREFIX} Health check threw for port ${entry.port}: ${error instanceof Error ? error.message : String(error)}`,\n        );\n      }\n    }\n\n    return null;\n  }\n\n  /**\n   * Check whether the requested port already hosts a healthy browse-tool HTTP service.\n   *\n   * @param port - The exact port to verify\n   * @returns Healthy service metadata for the requested port, or null if not healthy\n   */\n  private async discoverHealthyServiceOnPort(port: number): Promise<DiscoveredService | null> {\n    const registration = await this.findRegistrationByPort(port);\n    if (!registration) {\n      return null;\n    }\n\n    try {\n      const result = await this.healthCheck.check(port);\n      if (result.healthy && result.serviceName === SERVICE_NAME) {\n        return { entry: registration, browserCount: result.browserCount, version: result.serviceVersion };\n      }\n    } catch (error) {\n      console.error(\n        `${LOG_PREFIX} Health check threw for requested port ${port}: ${error instanceof Error ? error.message : String(error)}`,\n      );\n    }\n\n    return null;\n  }\n\n  /**\n   * Remove stale (unhealthy) registry entries and kill dead PIDs.\n   * Iterates all entries for this service scoped by environment.\n   * Processes each entry independently — per-entry failures are logged and skipped.\n   * Health check errors are treated as indeterminate (entry is left in place).\n   */\n  private async cleanupStaleEntries(): Promise<void> {\n    const entries = await this.listServiceEntries();\n    const errors: string[] = [];\n\n    for (const entry of entries) {\n      try {\n        let healthResult: HealthCheckResult;\n        try {\n          healthResult = await this.healthCheck.check(entry.port);\n        } catch (checkError) {\n          // Health check threw — treat as indeterminate, skip this entry to avoid\n          // removing a potentially healthy server during a transient failure.\n          console.error(\n            `${LOG_PREFIX} Health check threw for port ${entry.port}, skipping cleanup: ${checkError instanceof Error ? checkError.message : String(checkError)}`,\n          );\n          continue;\n        }\n\n        const isHealthy = healthResult.healthy && healthResult.serviceName === SERVICE_NAME;\n        if (!isHealthy) {\n          // The entry is already known unhealthy, so skip the graceful request\n          // and go straight to a verified kill of whatever the pid points at.\n          if (entry.pid) {\n            await this.killVerified(entry.pid, entry.port);\n          }\n          await this.releaseEntry(entry);\n        }\n      } catch (error) {\n        const msg = `Failed to clean up entry on port ${entry.port}: ${error instanceof Error ? error.message : String(error)}`;\n        console.error(`${LOG_PREFIX} ${msg}`);\n        errors.push(msg);\n      }\n    }\n\n    if (errors.length > 0) {\n      console.error(`${LOG_PREFIX} ${errors.length} entry cleanup failure(s) during stale entry removal`);\n    }\n  }\n\n  /**\n   * Find a registry entry by port across all repository paths (scoped by environment).\n   * Used after spawn to confirm the child process self-registered.\n   *\n   * @param port - The port number to search for\n   * @returns The matching registry record, or null if not found\n   */\n  private async findRegistrationByPort(port: number): Promise<PortRegistryRecord | null> {\n    const entries = await this.listServiceEntries();\n    return entries.find((entry) => entry.port === port) ?? null;\n  }\n\n  /**\n   * Build CLI command and args for spawning the HTTP server.\n   *\n   * @param port - The port to start the server on\n   * @returns Tuple of [command, args] for spawn\n   * @throws {HttpServerStartupError} If the CLI binary cannot be found\n   */\n  private async resolveCliCommand(port: number): Promise<[string, string[]]> {\n    const distCliPath = path.resolve(currentDir, DIST_CLI_FILENAME);\n    const srcCliPath = path.resolve(currentDir, '..', SRC_CLI_FILENAME);\n    const portArgs = [CLI_FLAG_PORT, port.toString(), CLI_FLAG_HOST, this.host];\n\n    if (pathExists(distCliPath)) {\n      return [NODE_COMMAND, [distCliPath, HTTP_SERVE_COMMAND, ...portArgs]];\n    }\n\n    if (pathExists(srcCliPath)) {\n      return [BUN_COMMAND, [BUN_RUN_SUBCOMMAND, srcCliPath, HTTP_SERVE_COMMAND, ...portArgs]];\n    }\n\n    const packageRoot = path.resolve(currentDir, '..', '..');\n    const fallbackDistCli = path.join(packageRoot, DIST_DIR, DIST_CLI_FILENAME);\n    const fallbackSrcCli = path.join(packageRoot, SRC_DIR, SRC_CLI_FILENAME);\n\n    if (pathExists(fallbackDistCli)) {\n      return [NODE_COMMAND, [fallbackDistCli, HTTP_SERVE_COMMAND, ...portArgs]];\n    }\n\n    if (pathExists(fallbackSrcCli)) {\n      return [BUN_COMMAND, [BUN_RUN_SUBCOMMAND, fallbackSrcCli, HTTP_SERVE_COMMAND, ...portArgs]];\n    }\n\n    throw new HttpServerStartupError(\n      `Cannot find CLI at ${distCliPath}, ${srcCliPath}, ${fallbackDistCli}, or ${fallbackSrcCli}`,\n    );\n  }\n\n  /**\n   * Start HTTP server as a detached background process.\n   * Resolves the CLI entry point (dist or src) and spawns it with the given port.\n   * Listens for spawn errors with a grace period to catch ENOENT/EACCES.\n   *\n   * @param port - The port to start the server on\n   * @returns The PID of the spawned process\n   * @throws {HttpServerStartupError} If the CLI binary cannot be found or spawn fails\n   */\n  private async startHttpServer(port: number, claimToken: string): Promise<number> {\n    const [command, args] = await this.resolveCliCommand(port);\n    const repositoryPath = await this.getRepositoryPath();\n\n    // Daemon output goes to a per-port log file so crashes and logged\n    // rejections stay diagnosable after the detached process loses its TTY.\n    const logFd = openSync(path.join(tmpdir(), `browse-tool-http-${port}.log`), 'w');\n    const child = spawn(command, args, {\n      detached: true,\n      stdio: ['ignore', logFd, logFd],\n      cwd: repositoryPath,\n      env: {\n        ...buildDaemonEnv(process.env),\n        NODE_ENV: this.environment,\n        [BROWSE_TOOL_WORKSPACE_ROOT_ENV]: repositoryPath,\n        [SKIP_DYNAMIC_TOOLS_ENV]: SKIP_DYNAMIC_TOOLS_ENABLED,\n        [CLAIM_TOKEN_ENV]: claimToken,\n      },\n    });\n    // The child holds its own copy of the descriptor.\n    closeSync(logFd);\n\n    // Race: wait for either an error event or a grace timeout.\n    // Keeps the listener active long enough to catch ENOENT/EACCES from the OS.\n    const spawnError = await new Promise<Error | null>((resolve) => {\n      const timer = setTimeout(() => {\n        child.removeAllListeners('error');\n        resolve(null);\n      }, SPAWN_ERROR_GRACE_MS);\n\n      child.once('error', (err) => {\n        clearTimeout(timer);\n        resolve(err);\n      });\n    });\n\n    if (spawnError) {\n      throw new HttpServerStartupError(`Failed to spawn HTTP server process: ${spawnError.message}`, {\n        cause: spawnError,\n      });\n    }\n\n    // The grace window above removed its listener; a later spawn error must log, not crash.\n    child.on('error', (error) => {\n      console.error(`${LOG_PREFIX} HTTP server process error:`, error);\n    });\n    child.unref();\n\n    const pid = child.pid;\n    if (!pid) {\n      throw new HttpServerStartupError('Failed to spawn HTTP server - no PID returned');\n    }\n\n    return pid;\n  }\n\n  /**\n   * Terminate a daemon process, but only after confirming the pid really is a\n   * browse-tool daemon on this port.\n   *\n   * Registry rows outlive crashed daemons and the OS recycles pids, so an\n   * unverified kill can take out an unrelated process. The whole process tree is\n   * signalled so the daemon's detached Chrome children die with it.\n   *\n   * @returns true when the process was verified and signalled\n   */\n  private async killVerified(pid: number, port: number): Promise<boolean> {\n    if (!(await isVerifiedDaemonProcess(pid, port))) {\n      console.error(`${LOG_PREFIX} Process ${pid} is not a browse-tool daemon on port ${port} — skipping kill`);\n      return false;\n    }\n\n    await killProcessTree(pid, getKillGraceMs());\n    return true;\n  }\n\n  /** Poll /health until the daemon on `port` answers as browse-tool, or the deadline passes. */\n  private async waitForHealthy(port: number, timeoutMs: number): Promise<HealthCheckResult | null> {\n    const deadline = Date.now() + timeoutMs;\n\n    while (Date.now() < deadline) {\n      try {\n        const result = await this.healthCheck.check(port);\n        if (result.healthy && result.serviceName === SERVICE_NAME) {\n          return result;\n        }\n      } catch {\n        // Not up yet — keep polling until the deadline.\n      }\n\n      await delay(HEALTH_POLL_INTERVAL_MS);\n    }\n\n    return null;\n  }\n\n  /** Poll until the daemon on `port` stops answering, i.e. it has finished shutting down. */\n  private async waitForUnhealthy(port: number, timeoutMs: number): Promise<boolean> {\n    const deadline = Date.now() + timeoutMs;\n\n    while (Date.now() < deadline) {\n      try {\n        const result = await this.healthCheck.check(port);\n        if (!result.healthy) {\n          return true;\n        }\n      } catch {\n        return true;\n      }\n\n      await delay(HEALTH_POLL_INTERVAL_MS);\n    }\n\n    return false;\n  }\n\n  /**\n   * Ask a daemon to shut itself down.\n   *\n   * The daemon owns its browsers, so letting it close them is the only clean\n   * teardown; signalling from outside races that cleanup and orphans Chrome.\n   *\n   * @returns true when the daemon accepted the request and stopped answering\n   */\n  private async requestGracefulShutdown(port: number): Promise<boolean> {\n    try {\n      const response = await fetch(`http://${this.host}:${port}/shutdown`, { method: 'POST' });\n      if (!response.ok) {\n        // 404 means a daemon predating this endpoint; caller falls back to signals.\n        return false;\n      }\n    } catch {\n      return false;\n    }\n\n    return this.waitForUnhealthy(port, getShutdownGraceMs());\n  }\n\n  /**\n   * Decide whether a discovered daemon can be reused given its version.\n   *\n   * A daemon serving browsers is never replaced: those browsers belong to another\n   * worktree's agent, and taking them away is worse than tolerating skew.\n   *\n   * @throws {HttpServerVersionMismatchError} When skew is not tolerated\n   */\n  private async resolveVersionDecision(discovered: DiscoveredService): Promise<VersionDecision> {\n    if (isCompatibleVersion(discovered.version)) {\n      return 'reuse';\n    }\n\n    const discoveredVersion = discovered.version ?? 'unknown';\n    const localVersion = getPackageVersion();\n    const port = discovered.entry.port;\n\n    if ((discovered.browserCount ?? 0) > 0) {\n      const message =\n        `Daemon on port ${port} runs version ${discoveredVersion}, this client is ${localVersion}, ` +\n        `and it still has ${discovered.browserCount} browser(s) open`;\n\n      if (process.env[VERSION_STRICT_ENV] === '1') {\n        throw new HttpServerVersionMismatchError(message);\n      }\n\n      console.error(`${LOG_PREFIX} ${message} — reusing it. Run \\`browse-tool stop\\` to restart on this version.`);\n      return 'reuse';\n    }\n\n    // Idle daemon: never let an older client tear down a newer one\n    // (pattern from playwriter relay-client.ts).\n    if (discovered.version !== undefined && compareVersions(discovered.version, localVersion) > 0) {\n      console.error(\n        `${LOG_PREFIX} Daemon on port ${port} runs newer version ${discoveredVersion} (this client is ${localVersion}); reusing it.`,\n      );\n      return 'reuse';\n    }\n\n    console.error(\n      `${LOG_PREFIX} Restarting idle daemon on port ${port}: version ${discoveredVersion} != ${localVersion}`,\n    );\n\n    if (!(await this.requestGracefulShutdown(port))) {\n      if (discovered.entry.pid) {\n        await this.killVerified(discovered.entry.pid, port);\n      }\n    }\n\n    await this.releaseEntry(discovered.entry);\n    return 'replace';\n  }\n\n  /**\n   * Attempt a single bootstrap cycle: claim the scope, spawn a daemon, and wait\n   * for it to confirm the claim.\n   *\n   * Claiming before spawning is what makes concurrent bootstraps safe — the\n   * registry hands the claim to exactly one caller, so the others wait for that\n   * daemon instead of racing to start their own.\n   *\n   * @param preferredPort - Port to start scanning from\n   * @returns Structured result indicating success or a typed failure reason\n   */\n  private async attemptSpawn(\n    preferredPort: number,\n    options: { exactPort?: boolean } = {},\n  ): Promise<SpawnAttemptResult> {\n    const repositoryPath = await this.getRepositoryPath();\n    const claim = await this.portRegistry.claimPort({\n      repositoryPath,\n      serviceName: SERVICE_NAME,\n      serviceType: SERVICE_TYPE,\n      environment: this.environment,\n      host: this.host,\n      pid: process.pid,\n      preferredPort,\n      portRange: options.exactPort ? { min: preferredPort, max: preferredPort } : undefined,\n      claimTtlMs: getClaimTtlMs(),\n    });\n\n    if (!claim.success || claim.port === undefined) {\n      throw new HttpServerPortError(`No available port found from ${preferredPort}: ${claim.error}`);\n    }\n\n    // Someone else is bootstrapping this scope: wait for their daemon rather\n    // than starting a second one.\n    if (claim.outcome === 'existing') {\n      const healthy = await this.waitForHealthy(claim.port, getStartupWaitMs());\n      if (healthy) {\n        const entry = await this.findRegistrationByPort(claim.port);\n        return {\n          success: true,\n          port: claim.port,\n          pid: entry?.pid ?? 0,\n          browserCount: healthy.browserCount,\n          spawned: false,\n        };\n      }\n\n      return {\n        success: false,\n        reason: claim.claimPending ? 'claim_unavailable' : 'health_check_failed',\n        port: claim.port,\n        error: `Another bootstrap holds port ${claim.port} but it never became healthy`,\n      };\n    }\n\n    const claimedPort = claim.port;\n    const claimToken = claim.claimToken ?? '';\n    const releaseClaim = async (): Promise<void> => {\n      await this.portRegistry.releaseClaim({\n        repositoryPath,\n        serviceName: SERVICE_NAME,\n        serviceType: SERVICE_TYPE,\n        environment: this.environment,\n        claimToken,\n      });\n    };\n\n    let pid: number;\n    try {\n      pid = await this.startHttpServer(claimedPort, claimToken);\n    } catch (error) {\n      await releaseClaim();\n      throw error;\n    }\n\n    const healthResult = await this.waitForHealthy(claimedPort, getStartupWaitMs());\n    if (!healthResult) {\n      await this.killVerified(pid, claimedPort);\n      await releaseClaim();\n      return {\n        success: false,\n        reason: 'startup_timeout',\n        port: claimedPort,\n        error: `Server did not become healthy within ${getStartupWaitMs()}ms`,\n      };\n    }\n\n    // The daemon confirms the claim itself, which is what turns the reservation\n    // into a real allocation. A missing confirmation means it lost the lease.\n    const registration = await this.findRegistrationByPort(claimedPort);\n    if (!registration || registration.pid !== pid) {\n      console.error(\n        `${LOG_PREFIX} Port ${claimedPort} is healthy but registered to ${registration?.pid ?? 'nobody'}, not the spawned pid ${pid}`,\n      );\n      return {\n        success: false,\n        reason: 'claim_lost',\n        port: claimedPort,\n        error: 'Spawned server did not confirm its claim',\n      };\n    }\n\n    console.error(`${LOG_PREFIX} Server confirmed on port ${claimedPort} (pid: ${registration.pid})`);\n\n    return { success: true, port: claimedPort, pid, browserCount: healthResult.browserCount, spawned: true };\n  }\n\n  /**\n   * Ensure HTTP server is running (discover or start).\n   *\n   * Phase 1: Discovery — reuse a healthy instance if its version matches this build.\n   * Phase 2: Cleanup — remove stale entries from dead processes.\n   * Phase 3: Bootstrap — claim the scope, then spawn a daemon that confirms the claim.\n   *          Claiming first means concurrent callers cannot spawn duplicate daemons.\n   *\n   * @param port - Preferred starting port (default: from PLAYWRIGHT_PORT env or config)\n   * @returns Server status with connection details or structured error information\n   */\n  async ensureRunning(port = getPlaywrightPort(), options: EnsureRunningOptions = {}): Promise<HttpServerStatus> {\n    try {\n      if (options.exactPort) {\n        const discoveredOnPort = await this.discoverHealthyServiceOnPort(port);\n        if (discoveredOnPort && (await this.resolveVersionDecision(discoveredOnPort)) === 'reuse') {\n          return {\n            running: true,\n            port: discoveredOnPort.entry.port,\n            pid: discoveredOnPort.entry.pid,\n            browserCount: discoveredOnPort.browserCount,\n            spawned: false,\n          };\n        }\n\n        await this.cleanupStaleEntries();\n\n        const result = await this.attemptSpawn(port, { exactPort: true });\n        if (result.success) {\n          return {\n            running: true,\n            port: result.port,\n            pid: result.pid,\n            browserCount: result.browserCount,\n            spawned: result.spawned,\n          };\n        }\n\n        const startupError = new HttpServerStartupError(\n          `Failed to start browse-tool HTTP server on requested port ${port}: ${result.reason}`,\n        );\n        return this.buildErrorStatus(startupError);\n      }\n\n      // Phase 1: Discover existing healthy server across all worktrees\n      const discovered = await this.discoverHealthyService();\n      if (discovered && (await this.resolveVersionDecision(discovered)) === 'reuse') {\n        return {\n          running: true,\n          port: discovered.entry.port,\n          pid: discovered.entry.pid,\n          browserCount: discovered.browserCount,\n          spawned: false,\n        };\n      }\n\n      // Phase 2: Clean up stale entries before spawning\n      await this.cleanupStaleEntries();\n\n      // Phase 3: Spawn loop\n      let nextPort = port;\n      for (let attempt = 0; attempt < MAX_SPAWN_ATTEMPTS; attempt += 1) {\n        const result = await this.attemptSpawn(nextPort);\n\n        if (result.success) {\n          return {\n            running: true,\n            port: result.port,\n            pid: result.pid,\n            browserCount: result.browserCount,\n            spawned: result.spawned,\n          };\n        }\n\n        console.error(`${LOG_PREFIX} Spawn attempt ${attempt + 1} failed: ${result.reason} on port ${result.port}`);\n        nextPort = result.port + 1;\n      }\n\n      const startupError = new HttpServerStartupError(\n        `Failed to start browse-tool HTTP server after ${MAX_SPAWN_ATTEMPTS} attempts`,\n      );\n      return this.buildErrorStatus(startupError);\n    } catch (error) {\n      return this.buildErrorStatus(error);\n    }\n  }\n\n  /**\n   * Terminate the daemon behind a registry entry, preferring a graceful shutdown.\n   *\n   * Asking the daemon to stop lets it close its own browsers; signalling is the\n   * fallback for daemons that are unresponsive or predate the shutdown endpoint,\n   * and it only ever signals a pid verified to be that daemon.\n   */\n  private async terminateEntry(entry: PortRegistryRecord): Promise<void> {\n    if (await this.requestGracefulShutdown(entry.port)) {\n      return;\n    }\n\n    if (entry.pid) {\n      await this.killVerified(entry.pid, entry.port);\n    }\n  }\n\n  /**\n   * Stop HTTP server instances and clean up their registry entries.\n   *\n   * Scoped to this worktree by default: daemons registered under another\n   * worktree usually belong to another agent's session, and stopping one takes\n   * its browsers away mid-run. `allWorktrees` opts into the wider sweep for the\n   * case where that is exactly what the caller wants.\n   *\n   * Processes each entry independently — per-entry failures are collected and reported\n   * with full cause chain preserved.\n   *\n   * @returns true if at least one server was found and stopped\n   * @throws {HttpServerStopError} If any entry fails to stop (aggregated with causes)\n   */\n  async stop(options: StopOptions = {}): Promise<boolean> {\n    const entries = options.allWorktrees ? await this.listAllServiceEntries() : await this.listServiceEntries();\n\n    if (entries.length === 0) {\n      return false;\n    }\n\n    const collectedErrors: Error[] = [];\n\n    for (const entry of entries) {\n      try {\n        await this.terminateEntry(entry);\n        await this.releaseEntry(entry);\n      } catch (error) {\n        const wrappedError =\n          error instanceof Error ? error : new Error(`Failed to stop entry on port ${entry.port}: ${String(error)}`);\n        console.error(`${LOG_PREFIX} ${wrappedError.message}`);\n        collectedErrors.push(wrappedError);\n      }\n    }\n\n    if (collectedErrors.length > 0) {\n      const messages = collectedErrors.map((err) => err.message);\n      throw new HttpServerStopError(\n        `Failed to stop ${collectedErrors.length} of ${entries.length} server(s): ${messages.join('; ')}`,\n        collectedErrors,\n        { cause: collectedErrors[0] },\n      );\n    }\n\n    return true;\n  }\n\n  /**\n   * Get current HTTP server status.\n   * Uses global discovery first, falls back to stale entry reporting.\n   *\n   * @returns Current server status with health and connection details\n   */\n  async getStatus(): Promise<HttpServerStatus> {\n    try {\n      const discovered = await this.discoverHealthyService();\n      if (discovered) {\n        return {\n          running: true,\n          port: discovered.entry.port,\n          pid: discovered.entry.pid,\n          browserCount: discovered.browserCount,\n        };\n      }\n\n      // Check for stale entries to report\n      const entries = await this.listServiceEntries();\n\n      if (entries.length > 0) {\n        const staleEntry = entries[0];\n        return {\n          running: false,\n          port: staleEntry.port,\n          pid: staleEntry.pid,\n          error: ERROR_MSG_STALE,\n          errorCode: ERROR_CODE_STALE_ENTRY,\n          errorRecovery: ERROR_RECOVERY_STALE,\n        };\n      }\n\n      return {\n        running: false,\n        error: ERROR_MSG_NOT_REGISTERED,\n        errorCode: ERROR_CODE_NOT_REGISTERED,\n        errorRecovery: ERROR_RECOVERY_NOT_REGISTERED,\n      };\n    } catch (error) {\n      return this.buildErrorStatus(error);\n    }\n  }\n}\n"],"mappings":"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"}