UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

22,682 lines • 892 kB
#!/usr/bin/env node
import { createRequire as __pilensCreateRequire } from "node:module"; const require = __pilensCreateRequire(import.meta.url);
import {
  DEAD_CODE_DELTA_MAX_SHOWN,
  deadCodeIssueCount,
  deadCodeIssueKey,
  deadCodeIssues,
  formatDeadCodeDelta,
  stableFindingKey
} from "../chunk-TT22N74J.js";
import {
  clearFormatterCache
} from "../chunk-F3AU2TMM.js";
import {
  findNearestProjectRoot,
  getStartupScanMaxEntries,
  isStartupScanVerdictFresh,
  resolveStartupScanContext
} from "../chunk-5RXBNSW2.js";
import {
  loadCallGraph
} from "../chunk-NKOUFPQF.js";
import {
  RUNNERS,
  formatRunDurationMs,
  isExcludedTestTarget,
  isRunnerErrorResult
} from "../chunk-IVP5KAB7.js";
import {
  GovulncheckClient
} from "../chunk-NXPS2L6U.js";
import {
  GitleaksClient,
  classifyAndFilterFindings
} from "../chunk-BRJKOBZP.js";
import "../chunk-6P62QVEA.js";
import {
  isHardFailureSummary
} from "../chunk-7U2IDNXN.js";
import {
  resetManagedToolRefreshSession
} from "../chunk-UXVNL3D7.js";
import {
  MAX_ADVISORY_AFFECTED_FILES,
  acknowledgeTestFindings,
  acknowledgeTurnEndFindings,
  advisoryPathKey,
  consumeSessionStartGuidance,
  consumeTestFindings,
  consumeTurnEndFindings,
  gateFindingsByPathFreshness,
  parseScannedAtMs,
  peekTestFindings,
  peekTurnEndFindings,
  snapshotAdvisoryProvenance,
  validateAdvisoryProvenance
} from "../chunk-Y4T27COV.js";
import {
  CacheManager,
  DELTA_UNUSED_PROMOTION_NOTE,
  MCP_TURN_STATE_OWNER_ID,
  acquireWarmWordIndex,
  analyzeFile,
  applyFindingPolicy,
  applyLspFindingPolicy,
  applyPushedFindingPolicy,
  applyRulePolicy,
  canHandle,
  cascadeCarrySuffix,
  cascadeSettleWaitMs,
  convertLspDiagnostics,
  createDispatchContext,
  createMcpHost,
  demoteInferredProjectDiagnostics,
  demoteInferredProjectSweepResults,
  drainPendingRunnerFindings,
  dropStaleRunnerFindings,
  evaluateAstGrepRules,
  evaluateRules,
  filterFindingsByDisposition,
  formatCacheAgeLabel,
  formatDiagnostics,
  getAvailableRunners,
  getDiagnosticTracker,
  getLang,
  getLatencyReports,
  getToolPlan,
  inlineBlockerIdentities,
  loadProjectRulePolicyMap,
  loadSg,
  logCascade,
  markUnreconciledFindings,
  normalizeRuleId,
  pendingRunnerFindingsSize,
  requeueRunnerFindings,
  resetCascadeTierSessionState,
  resetDispatchBaselines,
  resetObservedRunnerLatency,
  resetPendingRunnerFindings,
  resetPsScriptAnalyzerAvailability,
  runProviders
} from "../chunk-TLGFBM3F.js";
import {
  resetLazyInstallAttempts
} from "../chunk-C2AUYXQH.js";
import {
  isTestRoleCollateral
} from "../chunk-I3R7GKDR.js";
import "../chunk-FC4RP4I2.js";
import {
  TrivyClient
} from "../chunk-DFVMIKMQ.js";
import "../chunk-TO6WL3GM.js";
import "../chunk-7F72PXY2.js";
import {
  resetRustAvailability
} from "../chunk-RPD5K5NC.js";
import {
  findingsResult,
  hasUsableResult
} from "../chunk-ZMOL6RQC.js";
import "../chunk-I7SPGOWI.js";
import "../chunk-AS44CFQ4.js";
import {
  getSgCommand,
  resetDispatchAvailabilityState,
  resolveManagedToolClient
} from "../chunk-7FBF7NPR.js";
import "../chunk-HHYAELLJ.js";
import "../chunk-V5PXFSZK.js";
import "../chunk-YLF3PPT5.js";
import "../chunk-JOLITGCC.js";
import "../chunk-E2WFBRHM.js";
import {
  resetGoAvailability
} from "../chunk-6VCD4VKZ.js";
import "../chunk-F3P7VQ7H.js";
import {
  SYMBOL_KIND_NAMES,
  captureReviewGraphStructuralIr,
  formatReviewGraphRevisionDriftNote,
  publishReviewGraphFileIr,
  reviewGraphIrContentHash
} from "../chunk-LSMJ6KBE.js";
import {
  buildSymbolId
} from "../chunk-3MED75HT.js";
import {
  resolveGitCheckout,
  resolveLinkedWorktreeOwner
} from "../chunk-3I5QFT3N.js";
import {
  DEPENDENCY_DRIFT_MAX_DELIVERIES,
  MAX_LATE_AUX_REARMS,
  PAST_EOF_STALE_MARKER,
  RUNTIME_CONFIG,
  ReadGuard,
  STALE_LINE_MARKER,
  SWEEP_IDLE_SAFETY_MARGIN_MS,
  Text,
  TreeSitterSymbolExtractor,
  WriteOrderingGuard,
  anchorsForDiagnostic,
  applyAuxiliarySuppressions,
  applyDispositions,
  applyDispositionsMultiFile,
  applyWeakDispositions,
  applyWorkspaceEdit,
  armDeferredLspWork,
  attemptTsserverSyncDiagnostics,
  beginScope,
  buildScopeKey,
  canRearmPendingAuxiliary,
  classifyDiagnosticTier,
  clearAllSessions,
  countRetainedSuppressedRows,
  createFileTime,
  createLazyImport,
  createLiveBusEmitter,
  createWorkspaceDiagnosticsCacheContext,
  demotePastEofDiagnostics,
  drainPendingAuxCapEvictedCount,
  drainPendingAuxiliaryCoverage,
  drainRenderedDependencyDriftFilePaths,
  exceedsLspSyncLimits,
  findAuxiliaryProfileForSource,
  getActiveSessionId,
  getDisposition,
  getFileDiagnosticSummaries,
  getFullScanWallClockMs,
  getLSPService,
  getLspBudgetIdleTimeoutMs,
  getMutationBridge,
  getWidgetBlockingFilesForSweep,
  groupFilesByPrimaryServer,
  hasAnyDispositionMarks,
  hasStrictDispositionMarks,
  hashDiagnosticContent,
  incrementWidgetDependencyDriftDelivery,
  isAuxBacklogPublished,
  isBlocking,
  isPendingAuxiliaryPastRearmTtl,
  isWarmAttached,
  isWorkspaceSweepActive,
  loadLspService,
  logBusEvent,
  markWidgetFileBlockersStale,
  newLspMutationCorrelationId,
  nextOrderTurn,
  pendingAuxiliaryCoverageSize,
  rearmPendingAuxiliaryCoverage,
  reconcileCorrelatedScanDiagnostics,
  reconcileScanDiagnostics,
  reconcileStaleWidgetDependencyBlockers,
  reconcileStaleWidgetFiles,
  recordLspMutationOutcome,
  recordRunner,
  recordStaleBusFailure,
  refuseStaleWorkspaceEdit,
  registerDeferredLspWork,
  resetLSPService,
  resetLspMutationNoBridgeDbgLatch,
  resetObservedMutationNet,
  resetOpaqueMutationState,
  resetPendingAuxiliaryCoverage,
  resetWorkspaceDiagnosticsCacheSession,
  resolveImportToFiles,
  resolveLiveBusEmitter,
  resyncDocumentOnPastEof,
  retagAuxiliaryDiagnostics,
  retireScope,
  retireWidgetDependencyDriftBlockers,
  runPerServerGroups,
  runWhenWorkspaceSweepIdle,
  scopeCell,
  setSessionLanguages,
  shouldShortenLspIdleTimeout,
  summarizeWorkspaceEdit,
  sweepInlineBlockerFreshness,
  symbolExtractionGrammar,
  touchCompletedConfirmationPolicy,
  touchCoverageGap,
  tryWarmAttachedDiagnostics,
  widgetDiagnosticUri,
  workspaceEditTextPaths,
  writeOrderToken
} from "../chunk-TVN5N6GY.js";
import {
  emitBounded,
  resetBoundedTelemetry
} from "../chunk-ARFCXG7S.js";
import {
  hashText,
  normalizeMessage,
  primePersistedMutationAttribution,
  resetMutationAttribution,
  stableFindingId
} from "../chunk-OAXNDZJ7.js";
import {
  mapWithConcurrency
} from "../chunk-XIMYZ3JA.js";
import {
  classifyCascadeWaitTier,
  customServerSpecsOf,
  describeRootFallback,
  explainServersForFile,
  getServersForFileWithConfig,
  initLSPConfig,
  isSameOrWithin,
  loadLSPConfig,
  lspConfigOf,
  primaryServerId,
  registerLSPConfig,
  resetDirectLspCommandAvailability,
  resetLSPCaseSensitivityState,
  resolveLspCwdForFile,
  shouldInitializeSessionRoot
} from "../chunk-B3I2UUT4.js";
import {
  hasAmbientPythonEnvironment
} from "../chunk-DIULIWKU.js";
import "../chunk-VDBPVIKD.js";
import "../chunk-5W3R6LFT.js";
import {
  EXT_TO_LANG,
  classifyTreeSitterWasmError,
  getSharedTreeSitterClient,
  getTreeSitterRuntimeStatus,
  isTreeSitterWasmAborted,
  logTreeSitter,
  logTreeSitterCacheStats,
  queriesForLanguage,
  queryLoader,
  reportBundledResourceDirHealth,
  resolveTreeSitterLanguage
} from "../chunk-LW6XKDZQ.js";
import "../chunk-Z54ZSOBZ.js";
import {
  SCAN_LANGUAGE_PRIORITY,
  extensionsForLanguage,
  extensionsForLanguageToken,
  resolveLanguage
} from "../chunk-TGPXLY2J.js";
import {
  dump,
  resolvePackagePath
} from "../chunk-QOZXJ4NA.js";
import "../chunk-V7R425XG.js";
import "../chunk-PAK6IDME.js";
import {
  HOOK_WALL_BUDGET_MS,
  bounded,
  combineAbortSignals,
  withDeadline
} from "../chunk-GUOGVTCQ.js";
import {
  PROJECT_SNAPSHOT_VERSION,
  getProjectSnapshotLegacyPath,
  getProjectSnapshotPath,
  hydrateRuntimeFromProjectSnapshot,
  hydrateRuntimeFromProjectSnapshotIfIdle,
  isProjectSnapshotFresh,
  isProjectSnapshotMetaStale,
  loadProjectSnapshot,
  loadProjectSnapshotExportsAndRules,
  loadProjectSnapshotWithoutWordIndex,
  readJsonCache,
  readProjectSnapshotMeta,
  saveRuntimeProjectSnapshot
} from "../chunk-AJVLWNFN.js";
import {
  centralityFromReverseDeps,
  getWordIndexBuildStatus,
  logWordIndex,
  searchWordIndex,
  triggerBackgroundWordIndexBuild
} from "../chunk-YGVYKNSN.js";
import {
  getProjectDiagnosticsScannerMaxFiles
} from "../chunk-LNAKEO63.js";
import {
  PathKeyedMap
} from "../chunk-Z3CCVDUB.js";
import {
  WARM_TURN_END_SCHEMA_VERSION,
  createWarmIpcLineReader,
  createWarmIpcRequestQueue,
  freshnessFromMtime,
  ipcPathForCwd,
  readTurnEndStatus,
  resolveRunnerPath,
  toRunnerDisplayPath
} from "../chunk-PWFHBTSF.js";
import "../chunk-2BEMIWEW.js";
import {
  commitDurableStore,
  resetResolvedPathCache,
  resetZizmorTokenAvailability
} from "../chunk-OQGFCZJ4.js";
import {
  resolveToolCwd
} from "../chunk-RUOG5D2C.js";
import {
  _resetPackageManagerCache,
  findGlobalBinary,
  pmBinary,
  resolveNodePackageManager,
  runScriptArgs
} from "../chunk-U3VGR4VU.js";
import {
  isToolInstallAllowedByTrust,
  projectTrustDenialReason
} from "../chunk-NTUJEGDZ.js";
import {
  probeToolAsync
} from "../chunk-EOKRQ3SA.js";
import {
  classifyProbeFailure,
  createAvailabilityLatch,
  describeInstallAttempt,
  logAvailabilityDecision,
  resetInstallRetryLatches,
  startHostStallSampler
} from "../chunk-WONERJTP.js";
import {
  canRunStartupHeavyScans,
  detectProjectLanguageProfile,
  getDefaultStartupTools
} from "../chunk-22AVIGXS.js";
import "../chunk-GVXA6CCE.js";
import {
  collectSourceFilesWithBudgetAsync,
  getSlowFsVerdict,
  isSlowFs,
  slowFsDegradationNotice
} from "../chunk-6QIUX4EP.js";
import "../chunk-VHQLPIRJ.js";
import {
  displayProjectDataPath,
  drainProjectDataDirMigrations,
  getAmbientAbortSignal,
  getGlobalPiLensDir,
  getKnipIgnorePatterns,
  getProjectDataDir,
  getProjectIgnoreGlobs,
  getProjectIgnoreMatcher,
  getResourceFootprint,
  getSubagentIdentity,
  isExcludedDirName,
  isRecordableProjectPath,
  isSubagentSession,
  isTestFile,
  loadPiLensGlobalConfig,
  realIsPidAlive,
  resetProjectDataDirSessionState,
  resetSafeSpawnWindowsCommandCache,
  resetSpawnTimeoutCooldowns,
  safeSpawnAsync,
  subagentLightModeNotice,
  sweepAtomicWriteStages,
  sweepDeadEphemeralDataDirs,
  updateHeartbeat,
  withGenerationLockSync
} from "../chunk-VN2AXLEX.js";
import {
  createSingleFlight
} from "../chunk-BW5KFIQC.js";
import {
  createDeadline,
  yieldIfOverBudget
} from "../chunk-QYF5S6P6.js";
import {
  detectFileKind
} from "../chunk-5ZEJHV35.js";
import {
  compareKeys,
  detectFileRole,
  loadPiLensProjectConfig,
  lspSectionOf,
  provenanceFor,
  provenanceView,
  resolvePiLensConfig,
  summarizeConfigResolution
} from "../chunk-DQ2NZ7C4.js";
import {
  LSP_NAMESPACE_KEY,
  getPiLensGlobalConfigPath,
  resetWorkspaceTopology
} from "../chunk-VNMT7C64.js";
import {
  DEFAULT_MAX_OUTPUT_BYTES,
  createBoundedOutputSink,
  truncatedByOutputCap,
  writeFileAtomic
} from "../chunk-2EECBNC5.js";
import {
  LENS_TOOL_NAMES,
  TOOL_REGISTRY,
  compareOrdinal,
  escapeRegExp,
  getDegradationLedgerGeneration,
  getDegradationSummary,
  incrementDegradationCount,
  recordDegradationOnce,
  renderDegradationLines,
  resetDegradationLedger,
  resolveLensToolEnabled,
  resolveLensToolName,
  toolRegistryEntryForMcp
} from "../chunk-N3YQJI6O.js";
import {
  createSubsystemLogger,
  flushExtensionLog,
  logExtension
} from "../chunk-O6TQT6RI.js";
import {
  claimPhaseOncePerSession,
  logLatency
} from "../chunk-5THXD6X5.js";
import {
  beginTurnContext,
  createNdjsonLogger,
  getGlobalPiLensLogDir,
  isTestMode,
  runLogCleanup,
  setTurnContextSession
} from "../chunk-UK3CMEAL.js";
import "../chunk-LJDODBBZ.js";
import {
  FactStore
} from "../chunk-NMABWBZN.js";
import {
  BoundedFifoMap,
  BoundedLruCache
} from "../chunk-ABBOA7UD.js";
import {
  homeRelativePath,
  isAtOrAboveHomeDir,
  isUnderDir,
  minimatch,
  normalizeEphemeralMapKey,
  normalizeFilePath,
  normalizeMapKey,
  pathsEqual,
  realpathOrResolve,
  toProjectRelativePath,
  uriToPath
} from "../chunk-Q5U6FMDI.js";
import "../chunk-NXL4FFQQ.js";

// dist/mcp/server.js
import * as fs25 from "node:fs";
import * as net from "node:net";
import * as path38 from "node:path";
import { fileURLToPath as fileURLToPath3 } from "node:url";

// dist/tools/render-compact.js
import * as fs from "node:fs";
import * as path from "node:path";
import { randomUUID } from "node:crypto";
var MAX_RESULT_BYTES = 40 * 1024;
var COMPLETE_MCP_RESULT_INPUT_BUDGET_BYTES = 8 * 1024 * 1024;
var FOOTER_MAX_DIGITS = String(Number.MAX_SAFE_INTEGER).length;
var FOOTER_DIAG_LINE_MAX_CHARS = 200;
var FOOTER_DIAG_SECTION_MAX_BYTES = 1024;
var RESULT_FOOTER_RESERVE_BYTES = Buffer.byteLength(`

result error
${"x".repeat(FOOTER_DIAG_SECTION_MAX_BYTES)}
usage tokens=${"9".repeat(FOOTER_MAX_DIGITS)} elapsed-ms=${"9".repeat(FOOTER_MAX_DIGITS)} bytes=${"9".repeat(FOOTER_MAX_DIGITS)} truncated=false`, "utf8");
var RESULT_PAYLOAD_BUDGET_BYTES = MAX_RESULT_BYTES - RESULT_FOOTER_RESERVE_BYTES;
function renderHeadTail(text, maxBytes, markerFor) {
  const render3 = (kept) => {
    const head = Math.floor(kept / 2);
    const tail = kept - head;
    return `${text.slice(0, head)}${markerFor(head, tail)}${text.slice(text.length - tail)}`;
  };
  let low = 0;
  let high = text.length;
  while (low < high) {
    const kept = Math.ceil((low + high) / 2);
    if (Buffer.byteLength(render3(kept), "utf8") <= maxBytes)
      low = kept;
    else
      high = kept - 1;
  }
  return { text: render3(low), keptCharacters: low };
}
function boundToolText(text, maxBytes = MAX_RESULT_BYTES) {
  const totalBytes = Buffer.byteLength(text, "utf8");
  if (totalBytes <= maxBytes) {
    return { text, truncated: false, omittedCharacters: 0 };
  }
  const fullOutputPath = path.join(getGlobalPiLensLogDir(), `tool-result-${Date.now()}-${randomUUID()}.log`);
  fs.mkdirSync(path.dirname(fullOutputPath), { recursive: true });
  if (totalBytes > COMPLETE_MCP_RESULT_INPUT_BUDGET_BYTES) {
    const omittedBytes = totalBytes - COMPLETE_MCP_RESULT_INPUT_BUDGET_BYTES;
    recordDegradationOnce({
      kind: "mcp-complete-result-budget-exceeded",
      subject: "complete-result",
      reason: `${totalBytes} input bytes exceeded ${COMPLETE_MCP_RESULT_INPUT_BUDGET_BYTES}-byte budget`,
      metadata: {
        totalBytes,
        budgetBytes: COMPLETE_MCP_RESULT_INPUT_BUDGET_BYTES
      }
    });
    const logText = renderHeadTail(text, COMPLETE_MCP_RESULT_INPUT_BUDGET_BYTES, (head, tail) => {
      const keptBytes = Buffer.byteLength(`${text.slice(0, head)}${text.slice(text.length - tail)}`, "utf8");
      return `

[incomplete: ${totalBytes - keptBytes} bytes omitted, budget ${COMPLETE_MCP_RESULT_INPUT_BUDGET_BYTES}]

[Full output: ${fullOutputPath}]

`;
    });
    fs.writeFileSync(fullOutputPath, logText.text, "utf8");
    const output2 = renderHeadTail(logText.text, maxBytes, () => `

[incomplete: ${omittedBytes} bytes omitted, budget ${COMPLETE_MCP_RESULT_INPUT_BUDGET_BYTES}]

[Full output: ${fullOutputPath}]

`);
    return {
      text: output2.text,
      truncated: true,
      omittedCharacters: text.length - logText.keptCharacters,
      fullOutputPath
    };
  }
  fs.writeFileSync(fullOutputPath, text, "utf8");
  const output = renderHeadTail(text, maxBytes, (head, tail) => `

[${text.length - head - tail} characters omitted. Full output: ${fullOutputPath}]

`);
  return {
    text: output.text,
    truncated: true,
    omittedCharacters: text.length - output.keptCharacters,
    fullOutputPath
  };
}
var CONTRACT_FOOTER_TAIL_RE = /(?:^|\n)result (?:ok|error)\n(?:diag severity=[^\n]*\n)*usage tokens=\d+ elapsed-ms=\d+ bytes=(\d+) truncated=(true|false)$/;
function renderToolResultContract(result, delivery) {
  const normalized = {
    ...result,
    isError: result.isError === true
  };
  const content = result.content ?? [];
  const textBlocks = content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text);
  if (textBlocks.length === 0)
    return normalized;
  const text = textBlocks.join("\n");
  if (CONTRACT_FOOTER_TAIL_RE.test(text))
    return normalized;
  const details = normalized.details;
  const diagLines = [];
  let diagSectionBytes = 0;
  if (Array.isArray(details?.diagnostics)) {
    for (const value of details.diagnostics) {
      if (!value || typeof value !== "object")
        continue;
      const severity = value.severity;
      if (typeof severity !== "string")
        continue;
      const line = `diag severity=${severity.slice(0, FOOTER_DIAG_LINE_MAX_CHARS)}`;
      const lineBytes = Buffer.byteLength(line, "utf8") + 1;
      if (diagSectionBytes + lineBytes > FOOTER_DIAG_SECTION_MAX_BYTES)
        break;
      diagLines.push(line);
      diagSectionBytes += lineBytes;
    }
  }
  const tokens = normalized.usage?.tokens ?? Math.ceil(Buffer.byteLength(text, "utf8") / 4);
  const elapsedMs = normalized.usage?.elapsedMs ?? 0;
  const deliveredBytes = delivery?.bytes ?? Buffer.byteLength(text, "utf8");
  const truncated = delivery?.truncated === true;
  const contractLines = [
    `result ${normalized.isError ? "error" : "ok"}`,
    ...diagLines,
    `usage tokens=${tokens} elapsed-ms=${elapsedMs} bytes=${deliveredBytes} truncated=${truncated ? "true" : "false"}`
  ];
  let lastTextIndex = -1;
  for (let index = content.length - 1; index >= 0; index--) {
    const block = content[index];
    if (block?.type === "text" && typeof block.text === "string") {
      lastTextIndex = index;
      break;
    }
  }
  if (lastTextIndex < 0)
    return normalized;
  return {
    ...normalized,
    content: content.map((block, index) => index === lastTextIndex && block.type === "text" ? { ...block, text: `${block.text}

${contractLines.join("\n")}` } : block)
  };
}
function boundResultPayload(result) {
  if (!result.content) {
    return { result, deliveredBytes: 0, truncated: false };
  }
  const joined = fullTextOf(result);
  const bound = boundToolText(joined, RESULT_PAYLOAD_BUDGET_BYTES);
  const firstText = result.content.findIndex((block) => block.type === "text" && typeof block.text === "string");
  let retainedText = false;
  const content = result.content.filter((block, index) => block.type !== "text" || typeof block.text !== "string" || index === firstText).map((block) => {
    if (block.type === "text" && typeof block.text === "string" && !retainedText) {
      retainedText = true;
      return { ...block, text: bound.text };
    }
    return block;
  });
  const deliveredBytes = Buffer.byteLength(fullTextOf({ content }), "utf8");
  return {
    result: { ...result, content },
    deliveredBytes,
    truncated: bound.truncated
  };
}
function renderToolText(summary, structured, compact = false) {
  const rawText = structured === void 0 ? summary : `${summary}

\`\`\`json
${JSON.stringify(structured, compact ? void 0 : null, compact ? void 0 : 2)}
\`\`\``;
  return {
    content: [{ type: "text", text: rawText }],
    details: structured
  };
}
function stripResultDetails(result) {
  const { details: _details, ...rest } = result;
  return rest;
}
function finalizeToolResultWithDelivery(result) {
  const normalized = { ...result, isError: result.isError === true };
  const existingText = fullTextOf(normalized);
  const existingFooter = CONTRACT_FOOTER_TAIL_RE.exec(existingText);
  if (existingFooter && Buffer.byteLength(existingText, "utf8") <= MAX_RESULT_BYTES) {
    return {
      result: normalized,
      deliveredBytes: Number(existingFooter[1]),
      truncated: existingFooter[2] === "true"
    };
  }
  const bound = boundResultPayload(normalized);
  const text = fullTextOf(bound.result);
  const keptFooter = CONTRACT_FOOTER_TAIL_RE.exec(text);
  if (keptFooter) {
    return {
      result: bound.result,
      deliveredBytes: Number(keptFooter[1]),
      truncated: keptFooter[2] === "true"
    };
  }
  const stamped = renderToolResultContract(bound.result, {
    bytes: bound.deliveredBytes,
    truncated: bound.truncated
  });
  return {
    result: stamped,
    deliveredBytes: bound.deliveredBytes,
    truncated: bound.truncated
  };
}
function finalizeToolResult(result) {
  return finalizeToolResultWithDelivery(result).result;
}
var PI_LENS_BLUE_FG = "\x1B[1m\x1B[38;2;96;165;250m";
var RESET_FG = "\x1B[39m\x1B[22m";
function fullTextOf(result) {
  return (result.content ?? []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("\n");
}
function selectCompactText(result, args, expanded, summarize) {
  const text = fullTextOf(result);
  if (expanded) {
    return {
      text: text || "(no output)",
      style: result.isError ? "error" : "output"
    };
  }
  const lineCount3 = text ? text.split("\n").length : 0;
  let summary;
  try {
    summary = summarize({
      details: result.details,
      args,
      isError: result.isError === true,
      text,
      lineCount: lineCount3
    });
  } catch {
    summary = text.split("\n")[0] ?? "";
  }
  return { text: summary, style: result.isError ? "error" : "brand" };
}
function paintCompact(style, text, theme) {
  if (style === "brand") {
    return `${PI_LENS_BLUE_FG}${text}${RESET_FG}`;
  }
  const color = style === "error" ? "error" : "toolOutput";
  return theme.fg(color, text);
}
function compactRenderResult(summarize) {
  return (result, options, theme, context) => {
    const component = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
    const { text, style } = selectCompactText(result, context.args ?? {}, options.expanded === true, summarize);
    component.setText(paintCompact(style, text, theme));
    return component;
  };
}

// dist/clients/ast-grep-client.js
import * as fs4 from "node:fs";
import * as os2 from "node:os";
import * as path4 from "node:path";

// dist/clients/ast-grep-rule-manager.js
import * as fs2 from "node:fs";
import * as path2 from "node:path";
function candidateAstGrepRulesPaths(ruleDir) {
  return [
    path2.join(ruleDir, "ast-grep-rules", "rules"),
    path2.join(ruleDir, "rules"),
    ruleDir
  ];
}
function checkAstGrepRulesHealth(ruleDir) {
  const possiblePaths = candidateAstGrepRulesPaths(ruleDir);
  let rulesPath;
  for (const candidate of possiblePaths) {
    if (fs2.existsSync(candidate)) {
      rulesPath = candidate;
      break;
    }
  }
  if (!rulesPath) {
    try {
      fs2.readdirSync(ruleDir);
    } catch (error) {
      const fsErrorCode = error?.code;
      if (fsErrorCode && fsErrorCode !== "ENOENT") {
        return { status: "unreadable", fsErrorCode };
      }
    }
    return { status: "absent" };
  }
  let entries;
  try {
    entries = fs2.readdirSync(rulesPath).filter((f) => f.endsWith(".yml"));
  } catch (error) {
    const fsErrorCode = error?.code ?? "UNKNOWN";
    return { status: "unreadable", fsErrorCode };
  }
  return entries.length > 0 ? { status: "healthy", entryCount: entries.length } : { status: "empty" };
}
var AstGrepRuleManager = class {
  ruleDir;
  log;
  ruleDescriptions = null;
  constructor(ruleDir, log) {
    this.ruleDir = ruleDir;
    this.log = log;
  }
  loadRuleDescriptions() {
    if (this.ruleDescriptions !== null)
      return this.ruleDescriptions;
    const descriptions = /* @__PURE__ */ new Map();
    const possiblePaths = candidateAstGrepRulesPaths(this.ruleDir);
    const rulesPath = possiblePaths.find((p) => fs2.existsSync(p));
    if (!rulesPath) {
      this.log(`Rule descriptions: no rules directory found in ${possiblePaths.join(", ")}`);
      this.ruleDescriptions = descriptions;
      return descriptions;
    }
    try {
      const files = fs2.readdirSync(rulesPath).filter((f) => f.endsWith(".yml"));
      this.log(`Loaded ${files.length} rule descriptions from ${rulesPath}`);
      for (const file of files) {
        const filePath = path2.join(rulesPath, file);
        const content = fs2.readFileSync(filePath, "utf-8");
        const rule = this.parseRuleYaml(content);
        if (rule) {
          descriptions.set(rule.id, rule);
        }
      }
    } catch (err) {
      this.log(`Failed to load rule descriptions: ${err.message}`);
    }
    this.ruleDescriptions = descriptions;
    return descriptions;
  }
  parseRuleYaml(content) {
    const result = {};
    const idMatch = content.match(/^id:\s*(.+)$/m);
    if (idMatch)
      result.id = idMatch[1].trim();
    const msgMatch = content.match(/^message:\s*"([^"]+)"/m) || content.match(/^message:\s*'([^']+)'/m) || content.match(/^message:\s*(.+)$/m);
    if (msgMatch)
      result.message = (msgMatch[3] || msgMatch[2] || msgMatch[1]).trim();
    const noteMatch = content.match(/^note:\s*\|([\s\S]*?)(?=^\w|\n\n|\nrule:)/m);
    if (noteMatch) {
      result.note = noteMatch[1].split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).join(" ");
    }
    const sevMatch = content.match(/^severity:\s*(.+)$/m);
    if (sevMatch)
      result.severity = this.mapSeverity(sevMatch[1].trim());
    const gradeMatch = content.match(/Grade\s+(\d+\.\d+)/i);
    if (gradeMatch)
      result.grade = parseFloat(gradeMatch[1]);
    const fixMatch = content.match(/^fix:\s*\|?([\s\S]*?)(?=^\w|^rule:|Z)/m);
    if (fixMatch) {
      result.fix = fixMatch[1].split(/\r?\n/).map((line) => line.replace(/^\s*\|?\s*/, "")).filter((line) => line.length > 0).join("\n");
    }
    if (result.id && result.message) {
      return result;
    }
    return null;
  }
  mapSeverity(severity) {
    const lower = severity.toLowerCase();
    if (lower === "error")
      return "error";
    if (lower === "warning")
      return "warning";
    if (lower === "info")
      return "info";
    return "hint";
  }
};

// dist/clients/sg-runner.js
import * as fs3 from "node:fs";
import * as os from "node:os";
import * as path3 from "node:path";
function buildBashRunArgs(cmd, allArgs) {
  return ["-c", '"$0" "$@"', cmd, ...allArgs];
}
function sgExcludeArgsForProject(rootDir) {
  return getProjectIgnoreGlobs(rootDir).flatMap((glob) => [
    "--globs",
    `!${glob}`
  ]);
}
var DEFAULT_EXEC_TIMEOUT_MS = 3e4;
var PROBE_TIMEOUT_MS = 5e3;
var MAX_SG_OUTPUT_BYTES = 8 * 1024 * 1024;
function formatMetaVarCaptures(mv) {
  if (!mv)
    return void 0;
  const parts = [];
  for (const [name, node] of Object.entries(mv.single)) {
    if (node.text)
      parts.push(`$${name}=${node.text}`);
  }
  for (const [name, nodes] of Object.entries(mv.multi)) {
    if (nodes.length > 0) {
      const joined = nodes.map((n) => n.text).join("");
      if (joined)
        parts.push(`$$$${name}=${joined}`);
    }
  }
  for (const [name, value] of Object.entries(mv.transformed)) {
    if (value)
      parts.push(`@${name}=${value}`);
  }
  if (parts.length === 0)
    return void 0;
  return `  ${parts.join("  ")}`;
}
function tryParseSgMatches(stdout) {
  try {
    const parsed = JSON.parse(stdout);
    if (Array.isArray(parsed))
      return parsed;
    if (parsed !== null && typeof parsed === "object")
      return [parsed];
    return null;
  } catch {
    return null;
  }
}
function tryParseNonZeroSgMatches(result) {
  if (!result.stdout.trim())
    return null;
  return tryParseSgMatches(result.stdout.trim());
}
var SgRunner = class {
  log;
  sgPath = null;
  sgArgsPrefix = [];
  /**
   * Availability memo, backed by the shared transient-aware latch (#1476).
   *
   * Before this, one failed sweep over every candidate — timeouts included —
   * latched `false` for the life of the process AND ran a full auto-install
   * first. A host stall during warm-up therefore both disabled ast-grep until
   * restart and paid for an install nobody needed.
   */
  availabilityLatch = createAvailabilityLatch();
  /**
   * At-most-one sweep in flight, via the shared primitive (#1753). One
   * instance owns one question, so the key is a constant.
   */
  ensureFlight = createSingleFlight();
  /**
   * Whether a DIRECT candidate — one that would have been ast-grep itself —
   * failed for a transient reason in the current sweep. Only these block the
   * install: a slow `npx` says nothing about whether ast-grep is on this
   * machine, and letting it block turned "ast-grep is genuinely absent on a
   * slow host" into "ast-grep is never installed and npx is re-spawned every
   * sweep, forever".
   */
  sweepSawTransient = false;
  sweepTransientCause = "probe-timeout";
  /**
   * The DIRECT candidates that were unreachable, in ask order (#1568).
   *
   * The sweep returns at the first candidate that answers, so at the moment of
   * a win this is exactly the set of preferred tiers the winner did not beat on
   * the merits. Fallbacks are excluded on purpose: `npx` is asked before the
   * global-bin and platform-package tiers, and a slow `npx` says nothing about
   * a winner that is a real binary.
   *
   * Basenames, because a candidate can be an absolute global-bin or
   * platform-package path and this list is written to latency.log (#1568
   * review F3).
   */
  sweepUnreachable = [];
  /** A transient on the npx fallback: not evidence, but not nothing either. */
  sweepFallbackTransient = false;
  sweepFallbackCause = "probe-timeout";
  /** Host stall summed over every probe of the current sweep, ms. */
  sweepHostStallMs = 0;
  /**
   * Candidates — direct or fallback — that this sweep probed and found
   * DURABLY missing (real ENOENT/non-installable, not a stall) (#1593). The
   * retained-arm fallback below only sees whether SOME candidate stalled; this
   * list lets it also check whether the memoized winner itself is one of the
   * candidates this very sweep just disproved, rather than re-serving a
   * command that ENOENTed a moment ago because an unrelated sibling stalled.
   */
  sweepDurablyMissing = [];
  constructor(verbose = false) {
    this.log = verbose ? createSubsystemLogger("sg-runner") : () => {
    };
  }
  /**
   * Check if ast-grep CLI is available, auto-install if not.
   *
   * Re-entrancy safe: concurrent first-time callers share one flight, so
   * probing/auto-install isn't duplicated across session-start tasks. The
   * share and the clear-in-finally belong to `singleFlight` (#1753); this
   * method owns only the latch short-circuit above it. The sweep-local
   * bookkeeping `doEnsureAvailable` resets on entry is untouched — it is
   * per-sweep state, not concurrency state.
   */
  async ensureAvailable() {
    const memo = this.availabilityLatch.read();
    if (memo !== null)
      return memo;
    return this.ensureFlight.run("ast-grep", () => this.doEnsureAvailable());
  }
  async doEnsureAvailable() {
    const startedAt = Date.now();
    this.sweepSawTransient = false;
    this.sweepTransientCause = "probe-timeout";
    this.sweepUnreachable = [];
    this.sweepFallbackTransient = false;
    this.sweepFallbackCause = "probe-timeout";
    this.sweepHostStallMs = 0;
    this.sweepDurablyMissing = [];
    const pathCommand = await this.probeCommandCandidates([
      { cmd: "ast-grep", argsPrefix: [] },
      { cmd: "sg", argsPrefix: [] },
      // `npx --no -- ast-grep` starts a Node process before it can answer, so
      // it is the candidate most likely to blow a 5 s budget on a cold or busy
      // box. Marked a fallback so its timeout cannot veto the install.
      { cmd: "npx", argsPrefix: ["--no", "--", "ast-grep"], fallback: true }
    ]);
    if (pathCommand) {
      this.sgPath = pathCommand.cmd;
      this.sgArgsPrefix = pathCommand.argsPrefix;
      this.noteAvailable(startedAt, `ast-grep found on PATH: ${pathCommand.cmd}`);
      return true;
    }
    for (const name of ["ast-grep", "sg"]) {
      const globalBin = await findGlobalBinary(name);
      if (globalBin && await this.probeCommand(globalBin, [])) {
        this.sgPath = globalBin;
        this.sgArgsPrefix = [];
        this.noteAvailable(startedAt, `ast-grep found in global bin: ${globalBin}`);
        return true;
      }
    }
    const platformBinary = await this.probePlatformPackageBinary();
    if (platformBinary) {
      this.sgPath = platformBinary;
      this.sgArgsPrefix = [];
      this.noteAvailable(startedAt, `ast-grep found via platform package: ${platformBinary}`);
      return true;
    }
    if (process.platform === "darwin") {
      const brewBinary = await this.probeHomebrew();
      if (brewBinary) {
        this.sgPath = brewBinary;
        this.sgArgsPrefix = [];
        this.noteAvailable(startedAt, `ast-grep found via Homebrew: ${brewBinary}`);
        return true;
      }
    }
    if (this.sweepSawTransient) {
      if (this.availabilityLatch.isProvisional() && this.sgPath && !this.sweepDurablyMissing.includes(this.sgPath)) {
        this.noteAvailable(startedAt, `ast-grep re-probe stalled; keeping ${this.sgPath}`, { retained: true });
        return true;
      }
      this.log("ast-grep availability probe timed out; will retry (not installing)");
      return this.noteUnavailable(startedAt, "transient", this.sweepTransientCause);
    }
    const installed = await resolveManagedToolClient({
      toolId: "ast-grep",
      cwd: process.cwd(),
      probe: async () => ({ outcome: "missing" }),
      acceptInstalled: async (installedPath) => await this.probeCommand(installedPath, []) ? installedPath : null
    });
    if (installed.outcome === "success") {
      this.sgPath = installed.value;
      this.sgArgsPrefix = [];
      this.noteAvailable(startedAt, `ast-grep auto-installed: ${installed.value}`);
      return true;
    }
    if (this.sweepFallbackTransient) {
      return this.noteUnavailable(startedAt, "transient", this.sweepFallbackCause);
    }
    const { getInstallAttempt } = await import("../probes/installer.js");
    return this.noteUnavailable(startedAt, "missing", "not-found", describeInstallAttempt(getInstallAttempt("ast-grep"), {
      installedButRejected: installed.outcome === "non-installable"
    }), "caller");
  }
  /**
   * Record a successful sweep, with one decision record.
   *
   * The win is latched for the session UNLESS a direct candidate ahead of it
   * was unreachable (#1568). `sweepSawTransient` is set only by candidates the
   * sweep already asked, and the sweep returns at the first one that answers,
   * so at this point the flag means "a tier the winner is supposed to lose to
   * never got a fair hearing". Latching that pinned the session to
   * `npx --no -- ast-grep` — a Node start per invocation — over a healthy
   * ast-grep on PATH, until the next restart. Provisional instead: served now,
   * re-swept once the stalled tier's cooldown expires.
   *
   * The install arm cannot reach here provisionally: `doEnsureAvailable`
   * returns before Step 4 whenever `sweepSawTransient` is set.
   *
   * `retained` marks the other provisional case (#1568 review F1): no candidate
   * answered at all, and the winner being reported is the one the previous
   * sweep found, kept rather than discarded on a timeout.
   */
  noteAvailable(startedAt, message, opts = {}) {
    const provisional = this.sweepSawTransient;
    let retryAfterMs = 0;
    if (provisional) {
      retryAfterMs = this.availabilityLatch.noteProvisionallyAvailable(this.sweepTransientCause);
    } else {
      this.availabilityLatch.noteAvailable();
    }
    this.log(message);
    logAvailabilityDecision({
      tool: "ast-grep",
      verdict: "available",
      outcome: "success",
      cause: provisional ? this.sweepTransientCause : "ok",
      elapsedMs: Date.now() - startedAt,
      latched: !provisional,
      classifiedBy: "probe",
      hostStallMs: this.sweepHostStallMs,
      budgetMs: PROBE_TIMEOUT_MS,
      ...provisional && {
        provisional: true,
        unreachablePreferred: [...this.sweepUnreachable],
        ...opts.retained === true && { retained: true },
        ...retryAfterMs > 0 && { retryAfterMs }
      }
    });
  }
  /**
   * Record a failed sweep. A `transient` outcome expires after a bounded
   * cooldown; anything else is remembered for the session, as before. The
   * elapsed/stall numbers describe the WHOLE sweep, because the verdict is
   * about the sweep rather than any single candidate — reporting zero here
   * would erase the evidence that cracked #1467.
   */
  noteUnavailable(startedAt, outcome, cause, evidence, classifiedBy = "probe") {
    const retryAfterMs = this.availabilityLatch.noteUnavailable(outcome, cause);
    logAvailabilityDecision({
      tool: "ast-grep",
      verdict: "unavailable",
      outcome,
      cause,
      elapsedMs: Date.now() - startedAt,
      latched: outcome !== "transient",
      hostStallMs: this.sweepHostStallMs,
      ...retryAfterMs > 0 && { retryAfterMs },
      budgetMs: PROBE_TIMEOUT_MS,
      classifiedBy,
      ...evidence !== void 0 && { evidence }
    });
    return false;
  }
  /**
   * Probe platform-specific @ast-grep/cli-{os}-{arch} npm packages.
   * These ship the binary at the package root (sg / sg.exe).
   */
  async probePlatformPackageBinary() {
    const { platform, arch } = process;
    const exeName = platform === "win32" ? "sg.exe" : "sg";
    const pkgSuffixes = [];
    if (platform === "linux" && arch === "x64")
      pkgSuffixes.push("linux-x64-gnu");
    if (platform === "linux" && arch === "arm64")
      pkgSuffixes.push("linux-arm64-gnu");
    if (platform === "darwin" && arch === "arm64")
      pkgSuffixes.push("darwin-arm64");
    if (platform === "darwin" && arch === "x64")
      pkgSuffixes.push("darwin-x64");
    if (platform === "win32" && arch === "x64")
      pkgSuffixes.push("win32-x64-msvc");
    if (platform === "win32" && arch === "arm64")
      pkgSuffixes.push("win32-arm64-msvc");
    const searchRoots = [];
    let dir = process.cwd();
    for (let depth = 0; depth < 5; depth++) {
      searchRoots.push(path3.join(dir, "node_modules"));
      const parent = path3.dirname(dir);
      if (parent === dir)
        break;
      dir = parent;
    }
    for (const suffix of pkgSuffixes) {
      const pkgName = `@ast-grep/cli-${suffix}`;
      for (const root of searchRoots) {
        const candidate = path3.join(root, pkgName, exeName);
        try {
          if (fs3.existsSync(candidate) && await this.probeCommand(candidate, [])) {
            return candidate;
          }
        } catch {
        }
      }
    }
    return void 0;
  }
  /**
   * Probe Homebrew installation (macOS only).
   * Runs `brew --prefix ast-grep` and checks the resulting bin directory.
   */
  async probeHomebrew() {
    try {
      const result = await safeSpawnAsync("brew", ["--prefix", "ast-grep"], {
        timeout: 3e3
      });
      if (result.error || result.status !== 0)
        return void 0;
      const prefix = result.stdout.trim();
      if (!prefix)
        return void 0;
      for (const name of ["ast-grep", "sg"]) {
        const candidate = path3.join(prefix, "bin", name);
        if (fs3.existsSync(candidate) && await this.probeCommand(candidate, [])) {
          return candidate;
        }
      }
    } catch {
    }
    return void 0;
  }
  isAstGrepVersionOutput(output) {
    return /\bast[- ]grep\b/i.test(output);
  }
  /**
   * Probe one candidate. A failure is CLASSIFIED, not merely counted: a
   * timeout/abort marks the whole sweep transient so the caller retries later
   * instead of installing and latching. A command that answers but is not
   * ast-grep (Linux `/usr/bin/sg` is util-linux) is a durable no.
   */
  async probeCommand(cmd, argsPrefix, fallback = false) {
    const sampler = startHostStallSampler();
    let result;
    let hostStallMs;
    try {
      result = await probeToolAsync(cmd, [...argsPrefix, "--version"], {
        timeout: PROBE_TIMEOUT_MS
      });
    } finally {
      hostStallMs = sampler.stop();
      this.sweepHostStallMs += hostStallMs;
    }
    if (!result.error && result.status === 0 && this.isAstGrepVersionOutput(`${result.stdout}
${result.stderr}`)) {
      return true;
    }
    const { outcome, cause } = classifyProbeFailure(result, { hostStallMs });
    if (outcome === "transient") {
      if (fallback) {
        this.sweepFallbackTransient = true;
        this.sweepFallbackCause = cause;
      } else {
        this.sweepSawTransient = true;
        this.sweepTransientCause = cause;
        const name = path3.basename(cmd);
        if (!this.sweepUnreachable.includes(name)) {
          this.sweepUnreachable.push(name);
        }
      }
    } else if (!this.sweepDurablyMissing.includes(cmd)) {
      this.sweepDurablyMissing.push(cmd);
    }
    return false;
  }
  async probeCommandCandidates(candidates) {
    for (const candidate of candidates) {
      if (await this.probeCommand(candidate.cmd, candidate.argsPrefix, candidate.fallback ?? false)) {
        return candidate;
      }
    }
    return void 0;
  }
  /**
   * Get the ast-grep command to use, plus any npx prefix arguments.
   */
  getSgCommand() {
    return {
      cmd: this.sgPath || "ast-grep",
      argsPrefix: this.sgArgsPrefix
    };
  }
  failureForSpawnResult(result) {
    if (result.failure === "aborted")
      return "aborted";
    switch (result.spawnFailure?.kind) {
      case "tool-not-found":
        return "unavailable";
      case "timeout":
        return "timeout";
      case "killed":
        return result.failure === "aborted" ? "aborted" : "cli-failure";
      case "cwd-unresolvable":
      case "permission-denied":
      case "spawn-failed":
      case void 0:
        return result.error ? "cli-failure" : void 0;
    }
  }
  formatPatternError(stderr, args) {
    if (stderr.includes("Multiple AST nodes are detected")) {
      return `Invalid AST pattern: The pattern appears to contain multiple AST nodes or is malformed.
Common causes:
  1. Missing parentheses: use it($TEST) not it"test"
  2. Raw text without structure: use console.log($MSG) not just "console.log"
  3. Unclosed quotes or brackets

Original error: ${stderr}`;
    }
    if (stderr.includes("Cannot parse query")) {
      return `Pattern syntax error: The pattern could not be parsed as valid code.
Tips:
  - Patterns must be valid ${args.includes("--lang") ? args[args.indexOf("--lang") + 1] : "language"} syntax
  - Use metavariables like $NAME, $ARGS for variable parts
  - Example: 'function $NAME($$$PARAMS) { $$$BODY }'

Original error: ${stderr}`;
    }
    return stderr;
  }
  async execRaw(args, timeout = DEFAULT_EXEC_TIMEOUT_MS, options = {}) {
    const command = this.getSgCommand();
    const result = await safeSpawnAsync(command.cmd, [...command.argsPrefix, ...args], {
      timeout,
      deadlineAt: options.deadlineAt,
      signal: options.signal,
      maxOutputBytes: MAX_SG_OUTPUT_BYTES
    });
    const failure = this.failureForSpawnResult(result);
    return {
      stdout: result.stdout,
      stderr: result.stderr,
      status: result.status,
      error: result.error?.message,
      failure,
      ...result.outputTruncated ? { outputTruncated: true } : {}
    };
  }
  /**
   * Run ast-grep asynchronously, return parsed matches. The Windows Git
   * Bash/MSYS path deliberately remains positional-argument based, but the
   * child itself now goes through safeSpawnAsync so cancellation, deadlines,
   * tree-kill, and output caps are shared with every other CLI runner.
   */
  async exec(args, options = {}) {
    const command = this.getSgCommand();
    const allArgs = [...command.argsPrefix, ...args];
    const isWindows = process.platform === "win32";
    const hasBash = Boolean(process.env.MSYSTEM || process.env.GIT_SHELL);
    const useBash = isWindows && hasBash;
    const result = await safeSpawnAsync(useBash ? "bash" : command.cmd, useBash ? buildBashRunArgs(command.cmd, allArgs) : allArgs, {
      timeout: DEFAULT_EXEC_TIMEOUT_MS,
      deadlineAt: options.deadlineAt,
      signal: options.signal,
      maxOutputBytes: MAX_SG_OUTPUT_BYTES
    });
    const empty = () => ({
      matches: [],
      totalMatches: 0,
      truncated: false
    });
    if (truncatedByOutputCap(result)) {
      return {
        ...empty(),
        error: "Failed to parse output: output was truncated"
      };
    }
    const spawnFailure = this.failureForSpawnResult(result);
    if (spawnFailure) {
      return {
        ...empty(),
        error: spawnFailure === "unavailable" ? "ast-grep CLI not found. Install: npm i -D @ast-grep/cli" : result.error?.message || "ast-grep CLI failed to start"
      };
    }
    if (result.status !== 0) {
      const stdout = result.stdout.trim();
      const stderr = result.stderr.trim();
      if (result.status === 1 && stdout) {
        const matches = tryParseNonZeroSgMatches(result);
        if (matches) {
          return {
            matches,
            totalMatches: matches.length,
            truncated: false
          };
        }
      }
      if (result.status === 1 && !stdout && !stderr)
        return empty();
      return {
        ...empty(),
        error: this.formatPatternError(stderr || `Command failed with exit code ${result.status}`, args)
      };
    }
    if (!result.stdout.trim())
      return empty();
    {
      const matches = tryParseSgMatches(result.stdout);
      if (matches) {
        return {
          matches,
          totalMatches: matches.length,
          truncated: false
        };
      }
      return { ...empty(), error: "Failed to parse output" };
    }
  }
  // --- Shared helpers for temp-dir rule scans ---
  prepareTempScan(ruleId, ruleYaml) {
    const sessionDir = fs3.mkdtempSync(path3.join(os.tmpdir(), `pi-lens-temp-${ruleId}-`));
    try {
      const rulesSubdir = path3.join(sessionDir, "rules");
      const configFile = path3.join(sessionDir, ".sgconfig.yml");
      fs3.mkdirSync(rulesSubdir, { recursive: true });
      fs3.writeFileSync(configFile, `ruleDirs:
  - ./rules
`);
      fs3.writeFileSync(path3.join(rulesSubdir, `${ruleId}.yml`), ruleYaml);
      return { sessionDir, configFile };
    } catch (error) {
      this.cleanupTempScan(sessionDir);
      throw error;
    }
  }
  cleanupTempScan(sessionDir) {
    try {
      fs3.rmSync(sessionDir, { recursive: true, force: true });
    } catch (err) {
      this.log(`Cleanup failed: ${err.message}`);
    }
  }
  interpretScanResult(result, args) {
    if (truncatedByOutputCap(result)) {
      return {
        matches: [],
        status: result.status,
        error: "Failed to parse ast-grep scan output: output was truncated",
        failure: "parse-failure"
      };
    }
    const failure = this.failureForSpawnResult(result);
    if (failure) {
      return {
        matches: [],
        status: result.status,
        error: result.error?.message || "ast-grep CLI failed to start",
        failure
      };
    }
    if (result.status !== 0) {
      const stdout = result.stdout.trim();
      const stderr = result.stderr.trim();
      if (result.status === 1 && stdout) {
        const matches = tryParseNonZeroSgMatches(result);
        if (matches) {
          return { matches, status: result.status };
        }
      }
      if (result.status === 1 && !stdout && !stderr) {
        return { matches: [], status: result.status };
      }
      return {
        matches: [],
        status: result.status,
        error: this.formatPatternError(stderr || `ast-grep scan failed with exit code ${result.status}`, args),
        failure: "cli-failure"
      };
    }
    if (!result.stdout.trim())
      return { matches: [], status: result.status };
    {
      const matches = tryParseSgMatches(result.stdout);
      if (matches) {
        return { matches, status: result.status };
      }
      return {
        matches: [],
        status: result.status,
        error: "Failed to parse ast-grep scan output: invalid JSON",
        failure: "parse-failure"
      };
    }
  }
  async tempScanDetailedAsync(dir, ruleId, ruleYaml, timeout = DEFAULT_EXEC_TIMEOUT_MS, options = {}) {
    let sessionDir;
    try {
      ({ sessionDir } = this.prepareTempScan(ruleId, ruleYaml));
      const configFile = path3.join(sessionDir, ".sgconfig.yml");
      const { cmd: sgCmd, args: sgPre } = getSgCommand();
      const result = await safeSpawnAsync(sgCmd, [
        ...sgPre,
        "scan",
        "--config",
        configFile,
        "--json",
        ...sgExcludeArgsForProject(dir),
        dir
      ], {
        timeout,
        deadlineAt: options.deadlineAt,
        signal: options.signal,
        maxOutputBytes: MAX_SG_OUTPUT_BYTES
      });
      return this.interpretScanResult(result, ["scan"]);
    } finally {
      if (sessionDir)
        this.cleanupTempScan(sessionDir);
    }
  }
  /** Backwards-compatible match-only wrapper for existing session scanners. */
  async tempScanAsync(dir, ruleId, ruleYaml, timeout = DEFAULT_EXEC_TIMEOUT_MS, options = {}) {
    const result = await this.tempScanDetailedAsync(dir, ruleId, ruleYaml, timeout, options);
    return result.matches;
  }
  /**
   * Run a rule scan with optional fix application.
   * Dry-run: --json (returns matches for preview).
   * Apply:   --update-all (writes fixes defined in the YAML `fix:` field).
   */
  async tempScanWithFixAsync(dir, ruleId, ruleYaml, applyFixes, timeout = DEFAULT_EXEC_TIMEOUT_MS, options = {}) {
    let sessionDir;
    try {
      ({ sessionDir } = this.prepareTempScan(ruleId, ruleYaml));
      const configFile = path3.join(sessionDir, ".sgconfig.yml");
      const { cmd: sgCmd, args: sgPre } = getSgCommand();
      const scanArgs = [
        ...sgPre,
        "scan",
        "--config",
        configFile,
        "--json",
        ...sgExcludeArgsForProject(dir),
        dir
      ];
      const spawnOptions = {
        timeout,
        deadlineAt: options.deadlineAt,
        signal: options.signal,
        maxOutputBytes: MAX_SG_OUTPUT_BYTES
      };
      if (!applyFixes) {
        const result = await safeSpawnAsync(sgCmd, scanArgs, spawnOptions);
        const scan2 = this.interpretScanResult(result, ["scan"]);
        return scan2.failure || scan2.error ? { matches: [], error: scan2.error } : { matches: scan2.matches };
      }
      const jsonResult = await safeSpawnAsync(sgCmd, scanArgs, spawnOptions);
      const scan = this.interpretScanResult(jsonResult, ["scan"]);
      if (scan.failure || scan.error) {
        return { matches: [], error: scan.error };
      }
      const applyResult = await safeSpawnAsync(sgCmd, [
        ...sgPre,
        "scan",
        "--config",
        configFile,
        "--update-all",
        ...sgExcludeArgsForProject(dir),
        dir
      ], spawnOptions);
      if (applyResult.error || applyResult.failure || applyResult.status !== 0) {
        return {
          matches: [],
          error: applyResult.error?.message || `ast-grep apply failed with exit code ${applyResult.status}`
        };
      }
      return { matches: scan.matches };
    } catch (err) {
      return { matches: [], error: String(err) };
    } finally {
      if (sessionDir)
        this.cleanupTempScan(sessionDir);
    }
  }
  /**
   * Format matches for display
   */
  formatMatches(matches, isDryRun = false, maxItems = 50, showModeIndicator = false) {
    if (matches.length === 0) {
      if (showModeIndicator) {
        return isDryRun ? "[DRY-RUN] No matches found." : "[NOT APPLIED] No matches found \u2014 nothing was changed. Run ast_grep_search to confirm the pattern matches before applying.";
      }
      return "No matches found";
    }
    const shown2 = matches.slice(0, maxItems);
    const lines = shown2.map((m) => {
      const loc = `${m.file}:${m.range.start.line + 1}:${m.range.start.column + 1}`;
      const text = m.text.length > 100 ? `${m.text.slice(0, 100)}...` : m.text;
      const langSuffix = m.language ? `  [${m.language}]` : "";
      const base = isDryRun && m.replacement ? `${loc}
  - ${text}
  + ${m.replacement}` : `${loc}: ${text}${langSuffix}`;
      const captures = formatMetaVarCaptures(m.metaVariables);
      return captures ? `${base}
${captures}` : base;
    });
    if (matches.length > maxItems) {
      lines.unshift(`Found ${matches.length} matches (showing first ${maxItems}):`);
    }
    if (showModeIndicator) {
      const prefix = isDryRun ? "[DRY-RUN]" : "[APPLIED]";
      const suffix = isDryRun ? "\n\n(Dry run \u2014 use apply=true to apply changes)" : "";
      return `${prefix} ${matches.length} replacement(s):

${lines.join("\n")}${suffix}`;
    }
    return lines.join("\n");
  }
};

// dist/clients/ast-grep-client.js
function reportAstGrepRulesHealth(bundledRuleDir) {
  const health = checkAstGrepRulesHealth(bundledRuleDir);
  logLatency({
    type: "phase",
    phase: "ast_grep_rules_resolved",
    filePath: bundledRuleDir,
    durationMs: 0,
    metadata: {
      status: health.status,
      entryCount: health.status === "healthy" ? health.entryCount : 0
    }
  });
  reportBundledResourceDirHealth("ast-grep-rules-dir-missing", bundledRuleDir, health, "ast-grep rule descriptions");
}
function recordAstGrepApply(matches, lineage) {
  const bridge = getMutationBridge();
  if (!bridge || matches.length === 0)
    return;
  const rangesByFile = /* @__PURE__ */ new Map();
  for (const match of matches) {
    const start = (match.range?.start?.line ?? 0) + 1;
    const end = Math.max(start, (match.range?.end?.line ?? 0) + 1);
    const existing = rangesByFile.get(match.file);
    if (existing)
      existing.push([start, end]);
    else
      rangesByFile.set(match.file, [[start, end]]);
  }
  for (const [filePath, editRanges] of rangesByFile) {
    bridge.recordMutation({
      filePath,
      kind: "edit",
      editRanges,
      consumer: "ast_grep_replace",
      // #3763: the call's session, so an apply that lands after `/new`
      // writes none of the next session's state.
      ...lineage && { lineage }
    });
  }
}
function extractDebugAst(raw) {
  const lines = raw.split(/\r?\n/);
  const start = lines.findIndex((line) => /^Debug (?:A|C)ST:/.test(line.trim()));
  if (start < 0)
    return void 0;
  const out = [];
  for (const line of lines.slice(start + 1)) {
    if (!line.trim())
      break;
    out.push(line);
  }
  return out.length > 0 ? out.join("\n") : void 0;
}
function lineStartOffsets(source) {
  const offsets = [0];
  for (let index = 0; index < source.length; index++) {
    if (source.charCodeAt(index) === 10)
      offsets.push(index + 1);
  }
  return offsets;
}
function snippetForRange(source, offsets, startLine0, startCol0, endLine0, endCol0) {
  const start = (offsets[startLine0] ?? 0) + startCol0;
  const end = (offsets[endLine0] ?? source.length) + endCol0;
  const text = source.slice(start, end).replace(/\s+/g, " ").trim();
  return text.length > 80 ? `${text.slice(0, 77)}...` : text;
}
var MAX_VALIDATE_PATTERN_CHARS = 2e4;
var MAX_VALIDATE_RULE_CHARS = 2e5;
var VALIDATION_SNIPPETS = {
  bash: { ext: "sh", source: "echo pi_lens_validate\n" },
  c: { ext: "c", source: "int main(void) { return 0; }\n" },
  cpp: { ext: "cpp", source: "int main() { return 0; }\n" },
  csharp: { ext: "cs", source: "class C { static void Main() {} }\n" },
  css: { ext: "css", source: ".pi-lens { color: black; }\n" },
  go: { ext: "go", source: "package main\nfunc main() {}\n" },
  html: { ext: "html", source: "<main>pi-lens</main>\n" },
  java: { ext: "java", source: "class Main { void run() {} }\n" },
  javascript: { ext: "js", source: "const piLensValidate = 1;\n" },
  json: { ext: "json", source: '{"piLensValidate":true}\n' },
  kotlin: { ext: "kt", source: "fun main() {}\n" },
  lua: { ext: "lua", source: "local pi_lens_validate = 1\n" },
  php: { ext: "php", source: "<?php $piLensValidate = 1;\n" },
  python: { ext: "py", source: "pi_lens_validate = 1\n" },
  ruby: { ext: "rb", source: "pi_lens_validate = 1\n" },
  rust: { ext: "rs", source: "fn main() {}\n" },
  tsx: { ext: "tsx", source: "export function App() { return <div />; }\n" },
  typescript: { ext: "ts", source: "const piLensValidate = 1;\n" },
  yaml: { ext: "yaml", source: "piLensValidate: true\n" }
};
function validationSnippetFor(language) {
  const key = language.toLowerCase().replace(/^"|"$/g, "");
  return VALIDATION_SNIPPETS[key] ?? {
    ext: key.replace(/[^a-z0-9_-]/gi, "") || "txt",
    source: "pi_lens_validate\n"
  };
}
function validateInputShape(value, maxChars, label) {
  if (value.includes("\0"))
    return `${label} contains NUL bytes`;
  if (value.length > maxChars) {
    return `${label} is too large (${value.length} chars, max ${maxChars})`;
  }
  return void 0;
}
function stderrHasError(stderr) {
  return stderr.split(/\r?\n/).some((line) => /^\s*(error|Error):/.test(line));
}
function formatDebugAst(tree, source) {
  const offsets = lineStartOffsets(source);
  return tree.split(/\r\n|\n/).map((line) => {
    const match = /^([ \t]*)([^ \t(][^(]*)? \((\d+),(\d+)\)-\((\d+),(\d+)\)$/.exec(line);
    if (!match)
      return line;
    const [, indent = "", label = "", startLine, startCol, endLine, endCol] = match;
    const sl = Number(startLine);
    const sc = Number(startCol);
    const el = Number(endLine);
    const ec = Number(endCol);
    const snippet = snippetForRange(source, offsets, sl, sc, el, ec);
    return `${indent}${label} [${sl + 1},${sc + 1}] - [${el + 1},${ec + 1}] ${JSON.stringify(snippet)}`;
  }).join("\n");
}
var AstGrepClient = class {
  ruleDir;
  log;
  ruleManager;
  runner;
  // #2636 review F3/F7:
  usingBundledRuleDirFallback;
  rulesHealthReportedGeneration;
  constructor(ruleDir, verbose = false) {
    const projectRuleDir = path4.join(process.cwd(), "rules");
    const hasProjectRuleDir = fs4.existsSync(projectRuleDir);
    this.usingBundledRuleDirFallback = !ruleDir && !hasProjectRuleDir;
    this.ruleDir = ruleDir || (hasProjectRuleDir ? projectRuleDir : resolvePackagePath(import.meta.url, "rules"));
    this.log = verbose ? createSubsystemLogger("ast-grep") : () => {
    };
    this.ensureRulesHealthReported();
    this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
    this.runner = new SgRunner(verbose);
  }
  /**
   * #2636 review F3: `AstGrepClient` is a per-PROCESS singleton
   * (`index.ts`/`mcp/server.ts`/`clients/mcp/session.ts` each construct it
   * exactly once), but `resetDegradationLedger()` wipes the ledger's
   * once-per-subject latch on EVERY `session_start`
   * (`runtime-session.ts`'s `handleSessionStart`). A report that only ever
   * fires at construction is therefore invisible after the first session
   * boundary — `pilens_health` shows nothing for a bug that is still there
   * (shape 17's inverse: `resolveSkillPaths`/`ruleFilesForLanguage` avoid
   * this because pi calls them fresh per request, but `AstGrepClient` has
   * no such per-request re-entry).
   *
   * Re-checked against the ledger's own generation counter at the START of
   * every real scan entry point (`ensureAvailable`, called by every
   * `tools/ast-grep-*`/`ast-dump` handler before doing any work) — the same
   * pattern `clients/tree-sitter-client.ts`'s `refreshGrammarSessionLatches`
   * uses for its own per-instance session latches, adapted here: the
   * degradation ledger already re-arms its OWN once-per-subject dedup on
   * reset, so this method only has to decide WHETHER to call
   * `reportAstGrepRulesHealth` again, never re-implement the dedup itself.
   */
  ensureRulesHealthReported() {
    const generation = getDegradationLedgerGeneration();
    if (this.rulesHealthReportedGeneration === generation)
      return;
    this.rulesHealthReportedGeneration = generation;
    if (this.usingBundledRuleDirFallback) {
      reportAstGrepRulesHealth(this.ruleDir);
    }
  }
  /**
   * Check if ast-grep CLI is available, auto-install if not
   */
  ensureAvailable() {
    this.ensureRulesHealthReported();
    return this.runner.ensureAvailable();
  }
  /**
   * Replace using a raw YAML rule that includes a `fix:` field (Phase 3/4 of #125).
   * Dry-run returns matches for preview; apply writes fixes to disk.
   */
  async replaceWithRule(ruleYaml, paths, apply) {
    const allMatches = [];
    for (const scanPath of paths) {
      if (apply) {
        const preCheck = await this.tempScanDetailed(scanPath, ruleYaml);
        if (preCheck.failure || preCheck.error) {
          return {
            matches: allMatches,
            totalMatches: allMatches.length,
            applied: false,
            error: preCheck.error ?? "ast-grep preview failed"
          };
        }
        if (preCheck.matches.length === 0) {
          return {
            matches: [],
            totalMatches: 0,
            applied: false,
            stalePreview: true
          };
        }
      }
      const result = await this.runner.tempScanWithFixAsync(scanPath, "agent-rule", ruleYaml, apply);
      if (result.error) {
        return {
          matches: allMatches,
          totalMatches: allMatches.length,
          applied: false,
          error: result.error
        };
      }
      allMatches.push(...result.matches);
    }
    return {
      matches: allMatches,
      totalMatches: allMatches.length,
      applied: apply
    };
  }
  /**
   * Search using a raw YAML rule (Phase 4 of #125).
   * Routes through sg scan --config rather than sg run -p.
   * Each path is scanned independently; results are merged.
   */
  async tempScanDetailed(dir, ruleYaml, options = {}) {
    const detailed = this.runner.tempScanDetailedAsync;
    if (detailed) {
      return detailed.call(this.runner, dir, "agent-rule", ruleYaml, 3e4, options);
    }
    const matches = await this.runner.tempScanAsync(dir, "agent-rule", ruleYaml, 3e4, options);
    return { matches, status: 0 };
  }
  async searchWithRule(ruleYaml, paths, options = {}) {
    const allMatches = [];
    for (const scanPath of paths) {
      try {
        const results = await this.tempScanDetailed(scanPath, ruleYaml, options);
        if (results.failure || results.error) {
          return {
            matches: allMatches,
            totalMatches: allMatches.length,
            error: results.error || `ast-grep scan failed (${results.failure ?? "unknown failure"})`
          };
        }
        allMatches.push(...results.matches);
      } catch (err) {
        return {
          matches: allMatches,
          totalMatches: allMatches.length,
          error: String(err)
        };
      }
    }
    return { matches: allMatches, totalMatches: allMatches.length };
  }
  /**
   * Dump the parsed tree-sitter AST for a snippet using ast-grep CLI.
   */
  async dumpAst(source, lang, options = {}) {
    const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "pi-lens-ast-dump-"));
    const tmpFile = path4.join(tmpDir, `snippet.${lang.replace(/[^a-z0-9_-]/gi, "") || "txt"}`);
    try {
      fs4.writeFileSync(tmpFile, source, "utf-8");
      const mode = options.includeAnonymous ? "cst" : "ast";
      const result = await this.runner.execRaw([
        "run",
        "--lang",
        lang,
        "-p",
        source,
        `--debug-query=${mode}`,
        tmpFile
      ]);
      const raw = result.stderr || result.stdout;
      const tree = extractDebugAst(raw);
      if (tree)
        return { output: formatDebugAst(tree, source) };
      return {
        error: result.error || result.stderr.trim() || result.stdout.trim() || `ast-grep did not return a debug AST for language ${lang}`
      };
    } finally {
      fs4.rmSync(tmpDir, { recursive: true, force: true });
    }
  }
  async validatePattern(pattern, lang, options) {
    const shapeError = validateInputShape(pattern, MAX_VALIDATE_PATTERN_CHARS, "pattern");
    if (shapeError)
      return { valid: false, error: shapeError };
    const snippet = validationSnippetFor(lang);
    const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "pi-lens-sg-validate-"));
    const tmpFile = path4.join(tmpDir, `snippet.${snippet.ext}`);
    try {
      fs4.writeFileSync(tmpFile, snippet.source, "utf-8");
      const args = ["run", "-p", pattern, "--lang", lang, "--json=compact"];
      if (options?.selector)
        args.push("--selector", options.selector);
      if (options?.strictness)
        args.push("--strictness", options.strictness);
      args.push(tmpFile);
      const result = await this.runner.execRaw(args, 1e4, options);
      const stderr = result.stderr.trim();
      const stdout = result.stdout.trim();
      if (truncatedByOutputCap(result)) {
        return {
          valid: false,
          error: "ast-grep validation output was truncated"
        };
      }
      if (result.error)
        return { valid: false, error: result.error };
      if (result.status !== 0 && !(result.status === 1 && !stderr)) {
        return {
          valid: false,
          error: stderr || `ast-grep validation failed with exit code ${result.status}`
        };
      }
      if (stderrHasError(stderr))
        return { valid: false, error: stderr };
      const warning = stderr || stdout || void 0;
      return {
        valid: true,
        ...warning ? { warning } : {}
      };
    } finally {
      try {
        fs4.rmSync(tmpDir, { recursive: true, force: true });
      } catch {
      }
    }
  }
  async validateRule(ruleYaml, options = {}) {
    const shapeError = validateInputShape(ruleYaml, MAX_VALIDATE_RULE_CHARS, "rule");
    if (shapeError)
      return { valid: false, error: shapeError };
    const language = /^\s*language:\s*([^\s#]+)/im.exec(ruleYaml)?.[1] ?? "typescript";
    const snippet = validationSnippetFor(language);
    const tmpDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "pi-lens-sg-rule-"));
    try {
      fs4.writeFileSync(path4.join(tmpDir, `snippet.${snippet.ext}`), snippet.source, "utf-8");
      const result = await this.tempScanDetailed(tmpDir, ruleYaml, options);
      if (result.failure || result.error) {
        return {
          valid: false,
          error: result.error || `ast-grep rule validation failed (${result.failure ?? "unknown failure"})`
        };
      }
      return { valid: true };
    } catch (err) {
      return { valid: false, error: String(err) };
    } finally {
      try {
        fs4.rmSync(tmpDir, { recursive: true, force: true });
      } catch {
      }
    }
  }
  /**
   * Syntax-only code outline via `ast-grep outline` (#311) — symbols, imports,
   * exports, and members for file or directory input. Raw, fast, no index/LSP;
   * complements module_report (which adds the cached graph's who-uses-this,
   * complexity, and blast radius). Returns parsed JSON; args go through
   * `execRaw` (execFile-style, no shell), so no interpolation risk.
   */
  async outline(paths, options = {}) {
    if (paths.length === 0)
      return { error: "no paths provided" };
    const args = ["outline", "--json=compact", "--color", "never"];
    if (options.lang)
      args.push("--lang", options.lang);
    if (options.items)
      args.push("--items", options.items);
    if (options.view)
      args.push("--view", options.view);
    if (options.types?.length)
      args.push("--type", options.types.join(","));
    if (options.match)
      args.push("--match", options.match);
    if (options.pubMembers)
      args.push("--pub-members");
    for (const glob of options.globs ?? [])
      args.push("--globs", glob);
    args.push(...paths);
    const result = await this.runner.execRaw(args);
    const raw = (result.stdout ?? "").trim();
    if (!raw) {
      return {
        error: result.error || result.stderr?.trim() || "ast-grep outline returned no output"
      };
    }
    try {
      return { output: JSON.parse(raw) };
    } catch (err) {
      return {
        error: `failed to parse ast-grep outline JSON: ${err instanceof Error ? err.message : String(err)}`
      };
    }
  }
  /**
   * Search for AST patterns in files
   */
  async search(pattern, lang, paths, options) {
    const args = ["run", "-p", pattern, "--lang", lang, "--json=compact"];
    if (options?.selector) {
      args.push("--selector", options.selector);
    }
    if (options?.context !== void 0) {
      args.push("--context", String(options.context));
    }
    if (options?.strictness) {
      args.push("--strictness", options.strictness);
    }
    args.push(...paths);
    const result = await this.runner.exec(args, options);
    return {
      matches: result.matches,
      totalMatches: result.totalMatches,
      truncated: result.truncated,
      error: result.error
    };
  }
  /**
   * Search and replace AST patterns
   */
  async replace(pattern, rewrite, lang, paths, apply = false, options) {
    const baseArgs = ["run", "-p", pattern, "-r", rewrite, "--lang", lang];
    if (options?.strictness) {
      baseArgs.push("--strictness", options.strictness);
    }
    if (!apply) {
      const result = await this.runner.exec([
        ...baseArgs,
        "--json=compact",
        ...paths
      ]);
      return {
        matches: result.matches,
        totalMatches: result.totalMatches,
        truncated: result.truncated,
        applied: false,
        error: result.error
      };
    }
    const preCheck = await this.runner.exec([
      ...baseArgs,
      "--json=compact",
      ...paths
    ]);
    if (preCheck.error) {
      return {
        matches: [],
        totalMatches: 0,
        truncated: false,
        applied: false,
        error: preCheck.error
      };
    }
    if (preCheck.matches.length === 0) {
      return {
        matches: [],
        totalMatches: 0,
        truncated: false,
        applied: false,
        stalePreview: true
      };
    }
    const applyResult = await this.runner.exec([
      ...baseArgs,
      "--update-all",
      ...paths
    ]);
    if (applyResult.error) {
      return {
        matches: [],
        totalMatches: 0,
        truncated: false,
        applied: false,
        error: applyResult.error
      };
    }
    recordAstGrepApply(preCheck.matches, options?.lineage);
    return {
      matches: preCheck.matches,
      totalMatches: preCheck.totalMatches,
      truncated: preCheck.truncated,
      applied: true,
      error: void 0
    };
  }
  /**
   * Run a one-off scan with a temporary rule and configuration
   */
  async runTempScanAsync(dir, ruleId, ruleYaml, timeout = 3e4) {
    if (!await this.ensureAvailable())
      return [];
    return this.runner.tempScanAsync(dir, ruleId, ruleYaml, timeout);
  }
  /**
   * Find similar functions by comparing normalized AST structure
   */
  async findSimilarFunctions(dir, lang = "typescript") {
    const ruleYaml = `id: find-functions
language: ${lang}
rule:
  kind: function_declaration
severity: info
message: found
`;
    const matches = await this.runTempScanAsync(dir, "find-functions", ruleYaml);
    if (matches.length === 0)
      return [];
    return this.groupSimilarFunctions(matches);
  }
  groupSimilarFunctions(matches) {
    const grouped = /* @__PURE__ */ new Map();
    for (const item of matches) {
      const name = this.extractFunctionName(item.text);
      if (!name)
        continue;
      const signature = this.normalizeFunction(item.text);
      const line = (item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0) + 1;
      const group = grouped.get(signature) ?? [];
      group.push({ name, file: item.file, line });
      grouped.set(signature, group);
    }
    return Array.from(grouped.entries()).filter(([, functions]) => functions.length > 1).map(([pattern, functions]) => ({ pattern, functions }));
  }
  /**
   * Extract function name from match text
   */
  extractFunctionName(text) {
    return text.match(/function\s+(\w+)/)?.[1] ?? null;
  }
  normalizeFunction(text) {
    const normalizedText = text.replace(/function\s+\w+/, "function FN").replace(/\bconst\b|\blet\b|\bvar\b/g, "VAR").replace(/["'].*?["']/g, "STR").replace(/`[^`]*`/g, "TMPL").replace(/\b\d+\b/g, "NUM").replace(/\btrue\b|\bfalse\b/g, "BOOL").replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
    const bodyMatch = normalizedText.match(/\{(.*)\}/);
    const body = bodyMatch ? bodyMatch[1].trim() : normalizedText;
    return body.slice(0, 200);
  }
  /**
   * Scan for exported function names in a directory
   */
  async scanExports(dir, lang = "typescript") {
    const exports = /* @__PURE__ */ new Map();
    const ruleYaml = `id: find-functions
language: ${lang}
rule:
  kind: function_declaration
severity: info
message: found
`;
    const matches = await this.runTempScanAsync(dir, "find-functions", ruleYaml, 15e3);
    this.log(`scanExports output length: ${matches.length}`);
    for (const item of matches) {
      const text = item.text || "";
      const nameMatch = text.match(/function\s+(\w+)/);
      if (nameMatch?.[1]) {
        this.log(`scanExports found: ${nameMatch[1]} in ${item.file}`);
        exports.set(nameMatch[1], item.file);
      }
    }
    return exports;
  }
  formatMatches(matches, isDryRun = false, showModeIndicator = false, maxItems = 50) {
    return this.runner.formatMatches(matches, isDryRun, maxItems, showModeIndicator);
  }
  /**
   * Format diagnostics for LLM consumption
   */
  formatDiagnostics(diags) {
    if (diags.length === 0)
      return "";
    const errors = diags.filter((d) => d.severity === "error");
    const warnings = diags.filter((d) => d.severity === "warning");
    const infos = diags.filter((d) => d.severity === "info");
    const hints = diags.filter((d) => d.severity === "hint");
    let output = `[ast-grep] ${diags.length} structural issue(s)`;
    if (errors.length)
      output += ` \u2014 ${errors.length} error(s)`;
    if (warnings.length)
      output += ` \u2014 ${warnings.length} warning(s)`;
    if (infos.length)
      output += ` \u2014 ${infos.length} info(s)`;
    if (hints.length)
      output += ` \u2014 ${hints.length} hint(s)`;
    output += ":\n";
    for (const d of diags.slice(0, 10)) {
      const loc = d.line === d.endLine ? `L${d.line}` : `L${d.line}-${d.endLine}`;
      const ruleInfo = d.ruleDescription ? `${d.rule}: ${d.ruleDescription.message}` : d.rule;
      const fix = d.fix || d.ruleDescription?.note ? " [fixable]" : "";
      output += `  ${ruleInfo} (${loc})${fix}
`;
      if (d.ruleDescription?.note) {
        const shortNote = d.ruleDescription.note.split(/\r?\n/)[0];
        output += `    \u2192 ${shortNote}
`;
      }
    }
    if (diags.length > 10) {
      output += `  ... and ${diags.length - 10} more
`;
    }
    return output;
  }
  getRuleDescription(ruleId) {
    return this.ruleManager.loadRuleDescriptions().get(ruleId);
  }
};

// dist/clients/lens-engine.js
import * as path28 from "node:path";

// dist/clients/project-diagnostics/scanner.js
import * as fs6 from "node:fs";
import * as path6 from "node:path";

// dist/clients/project-diagnostics/cache.js
import * as fs5 from "node:fs";
import * as path5 from "node:path";
var PROJECT_DIAGNOSTICS_CACHE_VERSION = 3;
var SNAPSHOT_CACHE_FILE = "project-diagnostics.json";
var DELTA_CACHE_FILE = "project-diagnostics-delta.json";
function cachePath(cwd, fileName) {
  return path5.join(getProjectDataDir(cwd), "cache", fileName);
}
function loadProjectDiagnosticsSnapshot(cwd) {
  return readJsonCache(cachePath(cwd, SNAPSHOT_CACHE_FILE), (parsed) => {
    if (!parsed || typeof parsed !== "object")
      return void 0;
    const snapshot = parsed;
    if (snapshot.version !== PROJECT_DIAGNOSTICS_CACHE_VERSION)
      return void 0;
    if (!Array.isArray(snapshot.diagnostics))
      return void 0;
    return snapshot;
  });
}
function saveProjectDiagnosticsSnapshot(cwd, snapshot) {
  const filePath = cachePath(cwd, SNAPSHOT_CACHE_FILE);
  fs5.mkdirSync(path5.dirname(filePath), { recursive: true });
  writeFileAtomic(filePath, JSON.stringify(snapshot, null, 2));
}
function loadProjectDiagnosticsDeltaReport(cwd) {
  return readJsonCache(cachePath(cwd, DELTA_CACHE_FILE), (parsed) => {
    if (!parsed || typeof parsed !== "object")
      return void 0;
    const report = parsed;
    if (report.version !== PROJECT_DIAGNOSTICS_CACHE_VERSION)
      return void 0;
    if (!Array.isArray(report.diagnostics))
      return void 0;
    return report;
  });
}
function writeProjectDiagnosticsDeltaReport(cwd, report) {
  const filePath = cachePath(cwd, DELTA_CACHE_FILE);
  fs5.mkdirSync(path5.dirname(filePath), { recursive: true });
  writeFileAtomic(filePath, JSON.stringify(report, null, 2));
}
function reconcileProjectDiagnosticsSnapshot(snapshot) {
  const scannedAtMs = Date.parse(snapshot.scannedAt);
  if (!Number.isFinite(scannedAtMs))
    return { snapshot, staleDropped: 0 };
  const staleByFile = /* @__PURE__ */ new Map();
  const isStale = (filePath) => {
    const cached = staleByFile.get(filePath);
    if (cached !== void 0)
      return cached;
    let stale;
    let stat;
    try {
      stat = fs5.statSync(filePath);
    } catch {
      stat = void 0;
    }
    const fingerprint = snapshot.fileFingerprints?.[filePath];
    if (fingerprint && stat) {
      stale = true;
      if (stat.size === fingerprint.sizeBytes) {
        try {
          stale = hashDiagnosticContent(fs5.readFileSync(filePath, "utf-8")) !== fingerprint.contentHash;
        } catch {
          stale = true;
        }
      }
      staleByFile.set(filePath, stale);
      return stale;
    }
    const verdict = freshnessFromMtime({
      mtimeMs: stat?.mtimeMs,
      referenceMs: scannedAtMs
    });
    stale = verdict.verdict !== "fresh";
    staleByFile.set(filePath, stale);
    return stale;
  };
  const kept = snapshot.diagnostics.filter((d) => !isStale(d.filePath));
  if (kept.length === snapshot.diagnostics.length) {
    return { snapshot, staleDropped: 0 };
  }
  const staleDropped = [...staleByFile.values()].filter(Boolean).length;
  return { snapshot: { ...snapshot, diagnostics: kept }, staleDropped };
}

// dist/clients/project-diagnostics/scanner.js
var AST_GREP_MAX_FILE_BYTES = 1024 * 1024;
var AST_GREP_SCAN_MAX_MATCHES_PER_RULE = 25;
var AST_GREP_SCAN_MAX_DIAGNOSTICS_PER_FILE = 100;
var FACT_RULE_EXTENSIONS = /* @__PURE__ */ new Set([
  ".ts",
  ".tsx",
  ".js",
  ".jsx",
  ".mjs",
  ".cjs"
]);
var TREE_SITTER_EXT_TO_LANG = EXT_TO_LANG;
function normalizeSeverity(severity) {
  if (severity === "error" || severity === "warning" || severity === "hint") {
    return severity;
  }
  return severity === "info" ? "info" : "warning";
}
function normalizeSemantic(diagnostic) {
  if (diagnostic.semantic === "blocking")
    return "blocking";
  if (diagnostic.semantic === "warning")
    return "warning";
  return "none";
}
function fromDispatchDiagnostic(diagnostic, runner) {
  return {
    filePath: path6.resolve(diagnostic.filePath),
    line: diagnostic.line,
    column: diagnostic.column,
    severity: normalizeSeverity(diagnostic.severity),
    semantic: normalizeSemantic(diagnostic),
    tool: diagnostic.tool,
    runner,
    rule: diagnostic.rule,
    code: diagnostic.code,
    message: diagnostic.message,
    source: "project-scan"
  };
}
async function scanFileMajorRules(cwd, files, signal) {
  const client = getSharedTreeSitterClient();
  client?.ensureTreeCacheCapacity(files.length);
  const treeSitterReady = !!client && client.isAvailable() && await client.init();
  const queryMap = treeSitterReady ? await queryLoader.loadQueries(cwd) : void 0;
  const facts = new FactStore("project-diagnostics-scanner");
  const pi = { getFlag: () => void 0 };
  const treeSitter = [];
  const factRules = [];
  const astGrep = [];
  const fingerprints = {};
  const sgModule = await loadSg();
  let phaseOneFilesScanned = 0;
  let astGrepFilesScanned = 0;
  let astGrepDurationMs = 0;
  let filesScanned = 0;
  let wasmAborted = isTreeSitterWasmAborted();
  const ruleCoverage = /* @__PURE__ */ new Map();
  const hasTreeSitterRules = (langId) => {
    if (!queryMap || !client)
      return false;
    const memo = ruleCoverage.get(langId);
    if (memo !== void 0)
      return memo;
    const covered = queriesForLanguage(queryMap, langId).length > 0;
    ruleCoverage.set(langId, covered);
    return covered;
  };
  const scanTreeSitterFile = async (filePath, langId, content) => {
    if (!queryMap || !langId || !client || content === null)
      return;
    const queries = queriesForLanguage(queryMap, langId);
    try {
      const found = await client.runQueriesOnFile(queries, filePath, langId, { maxResults: 50 }, content);
      for (const { queryDef: query, match } of found) {
        treeSitter.push({
          filePath,
          line: match.line ?? 1,
          column: match.column,
          severity: query.severity === "error" ? "error" : query.severity,
          semantic: query.inline_tier === "blocking" || query.severity === "error" ? "blocking" : "warning",
          tool: "tree-sitter",
          runner: "tree-sitter",
          rule: query.id,
          message: query.message,
          source: "project-scan"
        });
      }
    } catch {
    }
  };
  const scanFactRulesFile = async (filePath, content) => {
    facts.dropFileFacts(filePath);
    facts.setFileFact(filePath, "file.content", content);
    const ctx = createDispatchContext(filePath, cwd, pi, facts, false);
    try {
      await runProviders(ctx);
      for (const diagnostic of evaluateRules(ctx)) {
        factRules.push(fromDispatchDiagnostic(diagnostic, "fact-rules"));
      }
    } catch {
    }
  };
  const scanAstGrepFile = (filePath, content, lang) => {
    const startedAt2 = Date.now();
    try {
      const rootNode = lang.parse(content).root();
      const fileDiagnostics = evaluateAstGrepRules(filePath, rootNode, cwd, detectFileKind(filePath), {
        maxMatchesPerRule: AST_GREP_SCAN_MAX_MATCHES_PER_RULE,
        maxTotalDiagnostics: AST_GREP_SCAN_MAX_DIAGNOSTICS_PER_FILE,
        content,
        sgModule
      });
      for (const diagnostic of fileDiagnostics) {
        astGrep.push(fromDispatchDiagnostic(diagnostic, "ast-grep-napi"));
      }
    } catch {
    } finally {
      astGrepFilesScanned++;
      astGrepDurationMs += Date.now() - startedAt2;
    }
  };
  const scan = async () => {
    for (const filePath of files) {
      if (signal?.aborted || isTreeSitterWasmAborted()) {
        wasmAborted ||= isTreeSitterWasmAborted();
        break;
      }
      if (isTestFile(filePath))
        continue;
      const ext = path6.extname(filePath);
      const grammarId = TREE_SITTER_EXT_TO_LANG[ext];
      const langId = grammarId && hasTreeSitterRules(grammarId) ? grammarId : void 0;
      const factEligible = FACT_RULE_EXTENSIONS.has(ext);
      let astGrepLang;
      if (sgModule && canHandle(filePath)) {
        try {
          if (fs6.statSync(filePath).size <= AST_GREP_MAX_FILE_BYTES) {
            astGrepLang = getLang(filePath, sgModule);
          }
        } catch {
        }
      }
      if (!langId && !factEligible && !astGrepLang)
        continue;
      if (langId || factEligible)
        phaseOneFilesScanned++;
      let content;
      let contentBytes;
      try {
        const buf = fs6.readFileSync(filePath);
        contentBytes = buf.length;
        content = buf.toString("utf-8");
      } catch {
        content = null;
        contentBytes = void 0;
      }
      const rowsBeforeFile = treeSitter.length + factRules.length + astGrep.length;
      try {
        await scanTreeSitterFile(filePath, langId, content);
        if (isTreeSitterWasmAborted()) {
          wasmAborted = true;
          break;
        }
        if (factEligible) {
          await scanFactRulesFile(filePath, content);
          if (isTreeSitterWasmAborted()) {
            wasmAborted = true;
            break;
          }
        }
        let graphIr;
        if (content !== null) {
          try {
            graphIr = await captureReviewGraphStructuralIr(filePath, cwd, content, facts);
          } catch {
            graphIr = { complete: false };
          }
          if (isTreeSitterWasmAborted()) {
            wasmAborted = true;
            break;
          }
        }
        if (signal?.aborted) {
          astGrep.length = 0;
          break;
        }
        if (astGrepLang && content !== null) {
          scanAstGrepFile(filePath, content, astGrepLang);
        }
        if (signal?.aborted)
          break;
        if (content !== null && graphIr?.complete && graphIr.structural) {
          publishReviewGraphFileIr(cwd, {
            filePath,
            contentHash: reviewGraphIrContentHash(content),
            ...graphIr
          });
        }
        if (content !== null && contentBytes !== void 0 && treeSitter.length + factRules.length + astGrep.length > rowsBeforeFile) {
          fingerprints[filePath] = {
            sizeBytes: contentBytes,
            contentHash: hashDiagnosticContent(content)
          };
        }
        filesScanned++;
      } finally {
        facts.dropFileFacts(filePath);
      }
    }
    if (wasmAborted) {
      astGrep.length = 0;
    }
  };
  if (!client) {
    await scan();
    return {
      treeSitter,
      factRules,
      astGrep,
      filesScanned,
      wasmAborted,
      fingerprints
    };
  }
  const startedAt = Date.now();
  await client.withParseCacheMeasurement(scan, (stats) => {
    logTreeSitterCacheStats({
      scope: "project_diagnostics_scan",
      filePath: cwd,
      fileCount: phaseOneFilesScanned,
      // The merged pass interleaves ast-grep with the phase-one consumers;
      // subtract its share so this metric stays comparable to the historical
      // phase-major scans.
      durationMs: Date.now() - startedAt - astGrepDurationMs,
      stats,
      // #1935 review: ast-grep's own cost is often a scan's most expensive
      // phase (production evidence: 13168ms over 86 files, the single
      // biggest contributor). Its duration is subtracted above so it stays
      // comparable to the historical phase-major scans; carry it here too
      // so it stays visible SOMEWHERE instead of disappearing when the
      // vacuous `project_diagnostics_ast_grep_scan` cache_stats record
      // (below) was removed.
      astGrep: {
        durationMs: astGrepDurationMs,
        fileCount: astGrepFilesScanned
      }
    });
  });
  return {
    treeSitter,
    factRules,
    astGrep,
    filesScanned,
    wasmAborted,
    fingerprints
  };
}
async function scanProjectDiagnostics(options) {
  const cwd = path6.resolve(options.cwd);
  const { signal } = options;
  if (!options.files && isAtOrAboveHomeDir(cwd, options.homeDir)) {
    return {
      version: PROJECT_DIAGNOSTICS_CACHE_VERSION,
      cwd,
      tier: options.tier,
      scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
      diagnostics: [],
      filesScanned: 0,
      runners: [],
      unsafeRoot: true
    };
  }
  const maxFiles = Math.max(1, options.maxFiles ?? getProjectDiagnosticsScannerMaxFiles(cwd));
  const collected = options.files ? { files: options.files.slice(0, maxFiles), entryBudgetExceeded: false } : await collectSourceFilesWithBudgetAsync(cwd, {
    maxFiles,
    maxScanEntries: options.maxScanEntries,
    // #1107 phase 2 review: the actionable opt-out generatedSkipNotice
    // points a user at.
    includeGenerated: options.includeGenerated === true
  });
  const files = collected.files;
  const runners = [];
  const diagnostics = [];
  let fileFingerprints = {};
  let wasmAborted = false;
  let filesScanned = files.length;
  if (!signal?.aborted) {
    const scanned = await scanFileMajorRules(cwd, files, signal);
    diagnostics.push(...scanned.treeSitter, ...scanned.factRules);
    runners.push("tree-sitter", "fact-rules");
    wasmAborted = scanned.wasmAborted;
    if (wasmAborted)
      filesScanned = scanned.filesScanned;
    if (!signal?.aborted && !wasmAborted) {
      diagnostics.push(...scanned.astGrep);
      runners.push("ast-grep-napi");
    }
    const rowFiles = new Set(diagnostics.map((d) => d.filePath));
    fileFingerprints = Object.fromEntries(Object.entries(scanned.fingerprints).filter(([file]) => rowFiles.has(file)));
  }
  const snapshot = {
    version: PROJECT_DIAGNOSTICS_CACHE_VERSION,
    cwd,
    tier: options.tier,
    scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
    diagnostics,
    filesScanned,
    runners,
    fileFingerprints
  };
  if (collected.entryBudgetExceeded)
    snapshot.scanTruncated = true;
  if (collected.generatedOrArtifactSkips) {
    snapshot.generatedFileSkips = collected.generatedOrArtifactSkips;
  }
  if (collected.generatedNameOnlySkips) {
    snapshot.generatedNameOnlySkips = collected.generatedNameOnlySkips;
  }
  if (collected.generatedDirSkips) {
    snapshot.generatedDirSkips = collected.generatedDirSkips;
  }
  if (wasmAborted) {
    snapshot.scanTruncated = true;
    snapshot.treeSitterStatus = "wasm_aborted_restart_required";
    logTreeSitter({
      phase: "runner_skip",
      filePath: cwd,
      status: "aborted",
      reason: "wasm_aborted_mid_scan",
      metadata: {
        scope: "project_diagnostics_scan",
        filesScanned,
        totalFiles: files.length,
        abortPoint: filesScanned + 1
      }
    });
  }
  if (signal?.aborted || options.files || wasmAborted)
    return snapshot;
  saveProjectDiagnosticsSnapshot(cwd, snapshot);
  return snapshot;
}

// dist/clients/mcp/review.js
import { spawn } from "node:child_process";
import { existsSync as existsSync4 } from "node:fs";
import { join as join6 } from "node:path";
function resolveRebuildScript(serverFilePath) {
  return serverFilePath.replace(/\\/g, "/").includes("/dist/") ? "build:dist" : "build";
}
function analyzeFileFresh(workerPath, file, cwd, options = {}, timeoutMs = 12e4) {
  return new Promise((resolve30) => {
    const args = [workerPath, `--file=${file}`, `--cwd=${cwd}`];
    if (options.flags)
      args.push(`--flags=${JSON.stringify(options.flags)}`);
    let child;
    try {
      child = spawn(process.execPath, args, {
        stdio: ["ignore", "pipe", "pipe"]
      });
    } catch (err) {
      resolve30({
        error: `failed to fork worker: ${err instanceof Error ? err.message : String(err)}`
      });
      return;
    }
    const stdout = createBoundedOutputSink();
    const stderr = createBoundedOutputSink();
    let settled = false;
    const finish = (outcome) => {
      if (settled)
        return;
      settled = true;
      clearTimeout(timer);
      resolve30(outcome);
    };
    const timer = setTimeout(() => {
      child.kill();
      finish({ error: `fresh analyze timed out after ${timeoutMs}ms` });
    }, timeoutMs);
    child.stdout.setEncoding("utf8");
    child.stdout.on("data", (chunk) => {
      if (settled)
        return;
      stdout.append(chunk);
      if (!stdout.truncated)
        return;
      incrementDegradationCount({
        kind: "spawn-output-cap-truncated",
        subject: "mcp-fresh-analyze",
        reason: `worker stdout reached the ${DEFAULT_MAX_OUTPUT_BYTES}-byte default cap after ${stdout.observedBytes} bytes; worker terminated`,
        metadata: {
          capBytes: DEFAULT_MAX_OUTPUT_BYTES,
          capSource: "default",
          observedBytes: stdout.observedBytes,
          killed: true
        }
      });
      try {
        child.kill();
      } catch {
      }
      finish({
        error: `worker output exceeded ${DEFAULT_MAX_OUTPUT_BYTES} bytes`
      });
    });
    child.stderr.setEncoding("utf8");
    child.stderr.on("data", (chunk) => {
      if (settled)
        return;
      stderr.append(chunk);
    });
    child.on("error", (err) => finish({ error: `failed to fork worker: ${err.message}` }));
    child.on("close", (code) => {
      if (code !== 0) {
        finish({
          error: `worker exited ${code}: ${stderr.text.trim() || "(no stderr)"}`
        });
        return;
      }
      try {
        finish({ result: JSON.parse(stdout.text) });
      } catch {
        finish({
          error: `worker produced invalid JSON (${stderr.text.trim() || stdout.text.slice(0, 200)})`
        });
      }
    });
  });
}
function canRebuildPiLens(repoRoot) {
  const inNodeModules = /(^|[\\/])node_modules([\\/]|$)/i.test(repoRoot);
  return !inNodeModules && existsSync4(join6(repoRoot, "tsconfig.dist.json"));
}
var REBUILD_UNAVAILABLE_MESSAGE = "pilens_rebuild is unavailable in an installed pi-lens package. Rebuilding is only safe from a source checkout containing tsconfig.dist.json; reinstall or update pi-lens through your package manager instead.";
async function runRebuild(repoRoot, script, timeoutMs = 3e5) {
  const start = Date.now();
  if (!canRebuildPiLens(repoRoot)) {
    return {
      ok: false,
      script,
      durationMs: Date.now() - start,
      output: REBUILD_UNAVAILABLE_MESSAGE
    };
  }
  const packageManager = await resolveNodePackageManager(repoRoot);
  const res = await safeSpawnAsync(pmBinary(packageManager), runScriptArgs(script), {
    cwd: repoRoot,
    timeout: timeoutMs,
    ignoreAmbientSignal: true
  });
  const output = `${res.stdout}
${res.stderr}`.trim();
  return {
    ok: !res.error && res.status === 0,
    script,
    packageManager,
    durationMs: Date.now() - start,
    output: output.slice(-2e3)
  };
}
function summarizeScan(diagnostics) {
  const seen = /* @__PURE__ */ new Set();
  const deduped = [];
  const byRule = {};
  const byFile = {};
  for (const diagnostic of diagnostics) {
    const ruleId = diagnostic.rule ?? diagnostic.runner ?? diagnostic.tool ?? "unknown";
    const key = `${diagnostic.filePath}|${diagnostic.line ?? "?"}|${diagnostic.column ?? "?"}|${ruleId}`;
    if (seen.has(key))
      continue;
    seen.add(key);
    deduped.push(diagnostic);
    byRule[ruleId] = (byRule[ruleId] ?? 0) + 1;
    byFile[diagnostic.filePath] = (byFile[diagnostic.filePath] ?? 0) + 1;
  }
  return { deduped, byRule, byFile };
}

// dist/clients/mcp/session.js
import * as crypto from "node:crypto";
import * as fs18 from "node:fs";
import * as path24 from "node:path";

// dist/clients/bootstrap.js
var residentClients = null;
var bootstrapShutdown = false;
var bootstrapShutdownController = new AbortController();
var bootstrapFailureStrikes = 0;
var bootstrapLoadAttempts = 0;
var BOOTSTRAP_FLIGHT_KEY = "analyzer-bootstrap";
var bootstrapFlight = createSingleFlight();
var BOOTSTRAP_FAILURE_STRIKE_LIMIT = 3;
function degradedClient() {
  return new Proxy({}, {
    get(_target, prop) {
      if (typeof prop === "symbol" || prop === "then")
        return void 0;
      return () => void 0;
    }
  });
}
async function logBootstrapFailures(failures) {
  for (const { name, err } of failures) {
    logExtension({
      subsystem: "bootstrap",
      message: `analyzer "${name}" disabled (degraded mode): ${err?.message ?? String(err)}`,
      metadata: { analyzer: name }
    });
  }
  try {
    const { collectInstallDiagnostics, formatInstallDiagnostics } = await import("../chunk-NABLC7W3.js");
    logExtension({
      subsystem: "bootstrap",
      message: formatInstallDiagnostics(collectInstallDiagnostics(), failures[0]?.err),
      metadata: { kind: "install_diagnostics" }
    });
  } catch {
  }
}
async function buildBootstrapClients() {
  const failures = [];
  async function load(name, make) {
    try {
      return await make();
    } catch (err) {
      failures.push({ name, err });
      return degradedClient();
    }
  }
  async function loadList(name, make) {
    try {
      return await make();
    } catch (err) {
      failures.push({ name, err });
      return [];
    }
  }
  const [ruffClient, biomeClient, knipClient, todoScanner, jscpdClient, depChecker, testRunnerClient, metricsClient, complexityClient, goClient, govulncheckClient, gitleaksClient, trivyClient, opengrepClient, rustClient, agentBehaviorClient2, deadCodeClients] = await Promise.all([
    load("ruff", async () => new (await import("../chunk-52H5B5TC.js")).RuffClient()),
    load("biome", async () => new (await import("../chunk-4RERDEXR.js")).BiomeClient()),
    load("knip", async () => new (await import("../chunk-AJ3QXTRQ.js")).KnipClient()),
    load("todo", async () => new (await import("../chunk-NL5GLMFI.js")).TodoScanner()),
    load("jscpd", async () => new (await import("../chunk-HVUZXSWK.js")).JscpdClient()),
    load("dependency-checker", async () => new (await import("../chunk-T6D4Q4AQ.js")).DependencyChecker()),
    load("test-runner", async () => new (await import("../chunk-AHDW3GPE.js")).TestRunnerClient()),
    load("metrics", async () => new (await import("../chunk-27BBHSZC.js")).MetricsClient()),
    load("complexity", async () => new (await import("../chunk-OS32RNR2.js")).ComplexityClient()),
    // The process-wide singleton, NOT a fresh instance (#2455 fix round 4,
    // F2). `handleSessionStart` reads this object for its "Active tools"
    // line while `resetGoAvailability` re-arms the singleton's latch; a
    // second instance here meant the reset never reached the surface a
    // user actually sees.
    load("go", async () => (await import("../chunk-NTHH2J74.js")).goClient),
    load("govulncheck", async () => new (await import("../chunk-54B42YRX.js")).GovulncheckClient()),
    load("gitleaks", async () => new (await import("../chunk-RIISMHKN.js")).GitleaksClient()),
    load("trivy", async () => new (await import("../chunk-IIXOAGHR.js")).TrivyClient()),
    load("opengrep", async () => new (await import("../chunk-ORTMY4Q4.js")).OpengrepClient()),
    // The process-wide singleton, for the same reason as "go" above.
    load("rust", async () => (await import("../chunk-4W2PLC4J.js")).rustClient),
    load("agent-behavior", async () => (await import("../chunk-IXEBJSUE.js")).agentBehaviorClient),
    loadList("dead-code", async () => (await import("../chunk-UY752QSN.js")).getDeadCodeClients())
  ]);
  if (failures.length > 0)
    await logBootstrapFailures(failures);
  return {
    ruffClient,
    biomeClient,
    knipClient,
    todoScanner,
    jscpdClient,
    depChecker,
    testRunnerClient,
    metricsClient,
    complexityClient,
    goClient,
    govulncheckClient,
    gitleaksClient,
    trivyClient,
    opengrepClient,
    rustClient,
    agentBehaviorClient: agentBehaviorClient2,
    deadCodeClients
  };
}
var BootstrapUnavailableError = class extends Error {
  unavailableReason;
  constructor(unavailableReason, message) {
    super(message);
    this.name = "BootstrapUnavailableError";
    this.unavailableReason = unavailableReason;
  }
};
function residentBootstrapAccess(clients) {
  return {
    peek: () => clients,
    request: async () => clients
  };
}
function loadBootstrapClients() {
  if (residentClients)
    return Promise.resolve(residentClients);
  if (bootstrapShutdown && !bootstrapFlight.has(BOOTSTRAP_FLIGHT_KEY)) {
    return Promise.reject(new BootstrapUnavailableError("shutdown", "analyzer bootstrap declined: primary session is shutting down"));
  }
  if (bootstrapFailureStrikes >= BOOTSTRAP_FAILURE_STRIKE_LIMIT) {
    return Promise.reject(new BootstrapUnavailableError("latched", `analyzer bootstrap declined: ${bootstrapFailureStrikes} consecutive load failures`));
  }
  return bootstrapFlight.run(BOOTSTRAP_FLIGHT_KEY, async () => {
    const attempt = bootstrapLoadAttempts += 1;
    const startedAt = Date.now();
    let clients;
    try {
      clients = await buildBootstrapClients();
    } catch (err) {
      noteBootstrapBuildFailure(err);
      throw err;
    }
    bootstrapFailureStrikes = 0;
    residentClients = clients;
    logLatency({
      type: "phase",
      phase: "bootstrap_clients_load",
      filePath: process.cwd(),
      startedAt: new Date(startedAt).toISOString(),
      durationMs: Date.now() - startedAt,
      metadata: { attempt }
    });
    return clients;
  });
}
function noteBootstrapBuildFailure(err) {
  bootstrapFailureStrikes += 1;
  if (bootstrapFailureStrikes !== BOOTSTRAP_FAILURE_STRIKE_LIMIT)
    return;
  recordDegradationOnce({
    kind: "analyzer-bootstrap-latched",
    subject: BOOTSTRAP_FLIGHT_KEY,
    reason: `${BOOTSTRAP_FAILURE_STRIKE_LIMIT} consecutive load failures; no further builds this session: ${err?.message ?? String(err)}`
  });
}
function resetAnalyzerBootstrapSessionState() {
  bootstrapShutdown = false;
  bootstrapShutdownController = new AbortController();
  bootstrapFailureStrikes = 0;
}

// dist/clients/formatters-lazy.js
var lazyFormatters = createLazyImport(() => import("../chunk-XK4CIILI.js"));
function warmFormatters() {
  return lazyFormatters.get();
}
function loadFormatters() {
  return warmFormatters();
}

// dist/clients/format-service.js
var globalFormatService = null;
var currentSessionID = null;
function resetFormatService() {
  void loadFormatters().then(({ clearFormatterRuntimeState }) => clearFormatterRuntimeState());
  globalFormatService = null;
  currentSessionID = null;
}

// dist/clients/runtime-coordinator.js
import { randomBytes } from "node:crypto";
import * as fs9 from "node:fs";
import * as path8 from "node:path";

// dist/clients/project-changes.js
import * as fs7 from "node:fs";
import * as path7 from "node:path";
function getProjectChangeLogPath(cwd) {
  return path7.join(getProjectDataDir(cwd), "change-log.jsonl");
}
function parseChangeLine(line) {
  try {
    const parsed = JSON.parse(line);
    if (typeof parsed.seq !== "number" || typeof parsed.fileSeq !== "number" || typeof parsed.filePath !== "string" || typeof parsed.source !== "string") {
      return void 0;
    }
    return {
      seq: parsed.seq,
      timestamp: parsed.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
      sessionId: parsed.sessionId ?? "unknown",
      turnIndex: parsed.turnIndex ?? 0,
      source: parsed.source,
      filePath: parsed.filePath,
      fileSeq: parsed.fileSeq,
      changedRange: parsed.changedRange,
      ...parsed.unlocked === true ? { unlocked: true } : {}
    };
  } catch {
    return void 0;
  }
}
var changeLogCursors = new BoundedFifoMap(16);
function parseChangeLog(logPath, content) {
  const entries = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map(parseChangeLine).filter((entry) => Boolean(entry));
  const bytes = Buffer.byteLength(content.slice(0, content.lastIndexOf("\n") + 1));
  let maxSeq = 0;
  for (const entry of entries)
    maxSeq = Math.max(maxSeq, entry.seq);
  changeLogCursors.set(logPath, { bytes, maxSeq });
  return entries;
}
function readProjectChanges(cwd) {
  const logPath = getProjectChangeLogPath(cwd);
  try {
    return parseChangeLog(logPath, fs7.readFileSync(logPath, "utf-8"));
  } catch {
    return [];
  }
}
async function readProjectChangesAsync(cwd) {
  const logPath = getProjectChangeLogPath(cwd);
  try {
    return parseChangeLog(logPath, await fs7.promises.readFile(logPath, "utf-8"));
  } catch {
    return [];
  }
}
var _sequenceFoldCountForTests = 0;
function foldEntry(entry, fileSeqByPath) {
  _sequenceFoldCountForTests++;
  const key = normalizeMapKey(path7.resolve(entry.filePath));
  fileSeqByPath.set(key, Math.max(fileSeqByPath.get(key) ?? 0, entry.fileSeq));
}
function fullReplay(entries) {
  let projectSeq = 0;
  const fileSeqByPath = /* @__PURE__ */ new Map();
  for (const entry of entries) {
    projectSeq = Math.max(projectSeq, entry.seq);
    foldEntry(entry, fileSeqByPath);
  }
  return { projectSeq, fileSeqByPath };
}
function partialReplay(entries, base) {
  let logMaxSeq = 0;
  for (const entry of entries) {
    if (entry.seq > logMaxSeq)
      logMaxSeq = entry.seq;
  }
  if (base.sinceSeq > logMaxSeq)
    return null;
  const fileSeqByPath = new Map(base.fileSeqByPath);
  let projectSeq = Math.max(0, base.projectSeq);
  for (const entry of entries) {
    if (entry.seq <= base.sinceSeq)
      continue;
    projectSeq = Math.max(projectSeq, entry.seq);
    foldEntry(entry, fileSeqByPath);
  }
  return { projectSeq, fileSeqByPath };
}
function readLatestProjectSequence(cwd, base) {
  return replay(readProjectChanges(cwd), base);
}
function replay(entries, base) {
  let unlockedThrough = 0;
  entries.forEach((entry, index2) => {
    if (entry.unlocked)
      unlockedThrough = index2 + 1;
  });
  const index = base && unlockedThrough <= (base.logEntries ?? 0) && partialReplay(entries, base) || fullReplay(entries);
  return { ...index, logEntries: entries.length, unlockedThrough };
}
async function readLatestProjectSequenceAsync(cwd, base) {
  return replay(await readProjectChangesAsync(cwd), base);
}
function readChangeLogMaxSeq(logPath) {
  let fd;
  try {
    fd = fs7.openSync(logPath, "r");
  } catch (err) {
    if (err.code === "ENOENT")
      return 0;
    throw err;
  }
  try {
    const cursor = changeLogCursors.get(logPath) ?? { bytes: 0, maxSeq: 0 };
    const size = fs7.fstatSync(fd).size;
    if (size > cursor.bytes) {
      const tail = Buffer.alloc(size - cursor.bytes);
      const read = fs7.readSync(fd, tail, 0, tail.length, cursor.bytes);
      const complete = tail.subarray(0, read).lastIndexOf(10) + 1;
      for (const line of tail.subarray(0, complete).toString("utf-8").split("\n")) {
        const entry = line.trim() ? parseChangeLine(line) : void 0;
        if (entry && entry.seq > cursor.maxSeq)
          cursor.maxSeq = entry.seq;
      }
      cursor.bytes += complete;
    }
    changeLogCursors.set(logPath, cursor);
    return cursor.maxSeq;
  } finally {
    fs7.closeSync(fd);
  }
}
var CHANGE_LOG_LOCK = { staleMs: 5e3, waitMs: 500 };
function appendProjectChangeAllocated(cwd, build) {
  const logPath = getProjectChangeLogPath(cwd);
  fs7.mkdirSync(path7.dirname(logPath), { recursive: true });
  readChangeLogMaxSeq(logPath);
  const append = (locked) => {
    const entry = build(readChangeLogMaxSeq(logPath));
    const line = locked ? entry : { ...entry, unlocked: true };
    fs7.appendFileSync(logPath, `${JSON.stringify(line)}
`, "utf-8");
  };
  const result = withGenerationLockSync(`${logPath}.locks`, CHANGE_LOG_LOCK, () => append(true));
  if (result.held)
    return;
  const code = result.cause?.code;
  incrementDegradationCount({
    kind: "change-log-lock-unavailable",
    subject: logPath,
    reason: `appended unlocked: ${code ? `lock failed (${code})` : "lock wait ran out"}`
  });
  append(false);
}

// dist/clients/inline-blocker-fields.js
function inlineBlockerSources(blockers) {
  return [...new Set(blockers.map((d) => d.tool?.trim() || "unknown"))];
}
function inlineBlockerLines(blockers, filePath) {
  const own = normalizeEphemeralMapKey(filePath);
  return blockers.filter((d) => normalizeEphemeralMapKey(d.filePath) === own).map((d) => d.line).filter((line) => typeof line === "number");
}

// dist/clients/partial-edit-apply.js
import { createHash } from "node:crypto";
import * as fs8 from "node:fs";
var MAX_APPLIED_RECORDS_PER_FILE = 8;
var MAX_APPLIED_RECORD_FILES = 64;
function editPairDigest(oldText, newText) {
  return createHash("sha256").update(JSON.stringify([oldText, newText ?? ""]), "utf8").digest("hex");
}
var PartialApplyRecordStore = class {
  // Bounded on the PathKeyedMap itself (#2442): the file axis used to
  // hand-roll `keys().next().value` here, which also evicted an unrelated
  // file when a path ALREADY in the map was re-recorded at capacity.
  files = new PathKeyedMap(normalizeMapKey, MAX_APPLIED_RECORD_FILES);
  find(filePath, oldText, newText) {
    if (!oldText)
      return void 0;
    const pairDigest = editPairDigest(oldText, newText);
    const records = this.files.get(filePath) ?? [];
    return records.find((r) => r.pairDigest === pairDigest);
  }
  record(filePath, oldText, newText, contentHash) {
    if (!oldText)
      return;
    const pairDigest = editPairDigest(oldText, newText);
    const records = this.files.get(filePath) ?? [];
    const existing = records.findIndex((r) => r.pairDigest === pairDigest);
    if (existing >= 0)
      records.splice(existing, 1);
    const entry = {
      pairDigest,
      contentHash: contentHash ?? hashFile(filePath),
      appliedAtMs: Date.now()
    };
    if (records.length >= MAX_APPLIED_RECORDS_PER_FILE)
      records.shift();
    records.push(entry);
    this.files.set(filePath, records);
  }
  /**
   * Stamps the post-afterWrite file hash onto an existing record so a later
   * identical retry against the formatter-rewritten file is still recognized as
   * already-applied (#2402). No-op when the pair was never recorded.
   */
  noteAfterWriteHash(filePath, oldText, newText, afterWriteContentHash) {
    if (!oldText)
      return;
    const pairDigest = editPairDigest(oldText, newText);
    const record = this.files.get(filePath)?.find((r) => r.pairDigest === pairDigest);
    if (record)
      record.afterWriteContentHash = afterWriteContentHash;
  }
  clear() {
    this.files.clear();
  }
  get fileCount() {
    return this.files.size;
  }
};
var fileLock = createFileTime("partial-edit-apply");
function hashFile(filePath) {
  try {
    return createHash("sha256").update(fs8.readFileSync(filePath)).digest("hex");
  } catch {
    return void 0;
  }
}

// dist/clients/turn-summary.js
var TurnSummaryCollector = class {
  filesByKey = /* @__PURE__ */ new Map();
  getOrCreate(filePath) {
    const key = normalizeMapKey(filePath);
    let entry = this.filesByKey.get(key);
    if (!entry) {
      entry = { filePath, displayPath: filePath, events: [] };
      this.filesByKey.set(key, entry);
    }
    return entry;
  }
  record(filePath, event) {
    this.getOrCreate(filePath).events.push(event);
  }
  recordDiagnostic(filePath, args) {
    this.record(filePath, { kind: "diagnostic", ...args });
  }
  recordAutofix(filePath, args) {
    this.record(filePath, { kind: "autofix", ...args });
  }
  recordFormat(filePath, args) {
    this.record(filePath, { kind: "format", ...args });
  }
  isEmpty() {
    return this.filesByKey.size === 0;
  }
  /** Snapshot without clearing — used for tests/observability. */
  peek() {
    return [...this.filesByKey.values()];
  }
  clear() {
    this.filesByKey.clear();
  }
  /** Consume (snapshot + clear) the run's collection, building the details payload. */
  consume(turnIndex, toDisplayPath3) {
    const files = [...this.filesByKey.values()].map((entry) => ({
      ...entry,
      displayPath: toDisplayPath3 ? toDisplayPath3(entry.filePath) : entry.filePath
    }));
    this.filesByKey.clear();
    const counts = {
      diagnostics: 0,
      autofixes: 0,
      formats: 0,
      byTool: { diagnostic: {}, autofix: {}, format: {} }
    };
    for (const file of files) {
      for (const event of file.events) {
        if (event.kind === "diagnostic")
          counts.diagnostics++;
        else if (event.kind === "autofix")
          counts.autofixes++;
        else
          counts.formats++;
        const bucket = counts.byTool[event.kind];
        bucket[event.tool] = (bucket[event.tool] ?? 0) + 1;
      }
    }
    return { version: 1, turnIndex, files, counts };
  }
};

// dist/clients/model-provider.js
var KNOWN_PREFIXES = [
  ["claude-", "anthropic"],
  ["gpt-", "openai"],
  ["o1-", "openai"],
  ["o3-", "openai"],
  ["gemini-", "google"],
  ["llama-", "meta"],
  ["mistral-", "mistral"],
  ["mixtral-", "mistral"],
  ["command-", "cohere"]
];
function deriveProviderFromModelId(modelId) {
  const id = modelId?.trim().toLowerCase();
  if (!id)
    return "";
  const separators = [...id].filter((c) => c === "/" || c === ":").length;
  if (separators === 1) {
    const sep2 = id.includes("/") ? "/" : ":";
    const [providerPart, ...rest] = id.split(sep2);
    if (providerPart && rest.join(sep2).length > 0)
      return providerPart;
    return "";
  }
  if (separators > 1)
    return "";
  for (const [prefix, provider] of KNOWN_PREFIXES) {
    if (id.startsWith(prefix))
      return provider;
  }
  return "";
}

// dist/clients/runtime-coordinator.js
var MAX_PENDING_CASCADE_RUNS = 32;
var MAX_CUT_ADVISORY_LANES = 16;
var MAX_MUTATION_RECEIPTS = 512;
function recordViewIncomplete(cwd, cause) {
  const why = `the change log reached seq ${cause.logMaxSeq} while this runtime was at ${cause.ownSeq} (${cause.ownSeq === 0 ? "a sibling process, or a timed-out session_start read that seeded 0; see snapshot-sequence-read-timeout" : "a sibling process logged above it"})`;
  recordDegradationOnce({
    kind: "snapshot-view-incomplete",
    subject: path8.resolve(cwd),
    reason: `snapshots are stamped incomplete (never fresh) until the next seed from the change log: ${why}`
  });
}
var MAX_RESOLVED_BLOCKER_FILES = 10;
var TOOL_CALL_ATTRIBUTION_CAPACITY = 256;
var TOOL_CALL_ATTRIBUTION_TTL_MS = 5 * 6e4;
var TurnWarningsBySession = class {
  bySession = /* @__PURE__ */ new Map();
  add(sessionId, items) {
    let partition = this.bySession.get(sessionId);
    if (!partition) {
      partition = /* @__PURE__ */ new Map();
      this.bySession.set(sessionId, partition);
    }
    for (const item of items)
      partition.set(item.id, item);
  }
  peek(sessionId) {
    return [...this.bySession.get(sessionId)?.values() ?? []];
  }
  clear(sessionId) {
    this.bySession.delete(sessionId);
  }
};
var RuntimeCoordinator = class {
  _projectRoot = normalizeMapKey(process.cwd());
  // #3611: the session generation is this scope's ticket, drawn from one
  // process counter, so two coordinators (two entry evaluations) never hold
  // the same number (N4). The construction scope's ticket is the
  // coordinator's id on the `session_scope_transition` row.
  _scope = beginScope({ role: "primary" });
  _coordinatorId = this._scope.scopeId;
  _sessionStartedAt = Date.now();
  _errorDebtBaseline = null;
  _pipelineCrashCounts = /* @__PURE__ */ new Map();
  _cachedExports = /* @__PURE__ */ new Map();
  _startupScansInFlight = /* @__PURE__ */ new Map();
  _cascadeRuns = [];
  // #3813/#3901: items an item-bearing turn-end advisory showed and the cap
  // cut (knip, dead-code, call-graph impact), keyed by lane. Parked once,
  // taken once; see `parkCutAdvisoryItems`.
  _cutAdvisoryItems = new BoundedFifoMap(MAX_CUT_ADVISORY_LANES);
  _turnEndCascadeSettleStarts = /* @__PURE__ */ new Map();
  _nextCascadeSettleToken = 0;
  // Cascade computes are kicked off unawaited by the pipeline (#450); their
  // promises park here until turn_end drains them via settleCascadeRuns. Each is
  // guaranteed non-rejecting by the pipeline's .catch.
  _pendingCascadeRuns = [];
  _cascadeSessionStats = {
    runs: 0,
    diagnosticsSurfaced: 0,
    coldSnapshotTouches: 0
  };
  _complexityBaselines = /* @__PURE__ */ new Map();
  _fixedThisTurn = new PathKeyedMap(normalizeMapKey);
  _writtenThisTurn = new PathKeyedMap(normalizeMapKey);
  _autofixDemotedThisTurn = new PathKeyedMap(normalizeMapKey);
  _reportedThisTurn = /* @__PURE__ */ new Set();
  _mutationReceipts = [];
  _droppedMutationReceipts = 0;
  _projectRulesScan = {
    rules: [],
    hasCustomRules: false
  };
  _telemetrySessionId = `lens-${Date.now().toString(36)}`;
  _lifecycleReason;
  _hasStableSessionId = false;
  _telemetryModel = "unknown";
  // Raw model/provider identity, separate from the combined `provider/model`
  // display string above — worklog/disposition attribution (#1448) wants the
  // two fields apart, blank when the host never supplied them. `_telemetryProvider`
  // is the explicit host value when given, else derived from the model id
  // (deriveProviderFromModelId, blank on ambiguity — never guessed).
  _telemetryModelId = "";
  _telemetryProvider = "";
  // True once a host has supplied an explicit provider this session. An
  // explicit provider is never downgraded by a derivation from a later
  // model-only call; a DERIVED provider, by contrast, is re-derived on
  // every model-only call so a mid-session model switch (e.g. gpt-5-mini →
  // claude-sonnet-4-5) doesn't leave a stale provider from the old model.
  _telemetryProviderIsExplicit = false;
  _turnIndex = 0;
  /**
   * #3540 r2: the turn half of a write order token (`writeOrderToken`).
   * `beginTurn` advances it and `resetForSession` never restarts it: the
   * widget's write guards outlive a session reset (`/reload` keeps them),
   * so a later turn's token must outrank every earlier one in the process.
   * `_turnIndex` restarts per session for telemetry.
   * #3611 (N3): drawn from the process counter (`nextOrderTurn`), because a
   * `/reload` that re-evaluates the entry builds a new coordinator while the
   * widget module keeps its guards.
   */
  _writeOrderTurn = 0;
  _writeIndex = 0;
  /**
   * #3613 F2: the per-turn cap key of each concurrent session's current turn
   * (`turnKey`), and how many such turns have started. Entries go at the
   * session's shutdown (`forgetTurnSession`).
   */
  _foreignTurnKeys = /* @__PURE__ */ new Map();
  _foreignTurns = 0;
  /**
   * #3613 G1: every turn start on this coordinator, of any session, since the
   * session reset: what `_turnIndex` counted before #3613, kept for the
   * process-level turn cadences (`turnStartCount`).
   */
  _turnStarts = 0;
  _projectSeq = 0;
  // #3511: the highest logged seq this runtime's view is known to have missed
  // (a sibling process logged it above our seq); 0 when none. Cleared by a
  // seed from the log, or by a late read that covers it.
  _viewMissingThrough = 0;
  // #3511 review round 2: the change-log entries folded by this session's
  // seed or late merge. An `unlocked` entry after it may share a seq we
  // hold, so a snapshot stamped with it is judged against the log's tags.
  _viewLogEntries = 0;
  _turnStartProjectSeq = 0;
  _fileSeq = /* @__PURE__ */ new Map();
  // File key → the projectSeq value at that file's most recent bump (#451). Lets
  // the review-graph builder ask "which files changed since I last built?" and
  // skip its per-build O(project) walk+stat sweep when only pi-observed edits
  // occurred. Keyed identically to _fileSeq (normalizeMapKey + path.resolve).
  _fileLastProjectSeq = /* @__PURE__ */ new Map();
  _gitGuardHasBlockers = false;
  _gitGuardSummary = "";
  _gitGuardCacheUnknownReason;
  callGraph = null;
  wordIndex = null;
  _readGuard = null;
  _pendingDeferredMutations = new PathKeyedMap(normalizeMapKey);
  /** tool_call → tool_result path-attribution correlation (#1642). */
  _toolCallAttributions = new BoundedLruCache(TOOL_CALL_ATTRIBUTION_CAPACITY);
  /**
   * #3555: reads carry no {@link ToolCallAttribution} (it is recorded for
   * mutations, and its origin cwd would change how a read's path resolves),
   * so a widening rides its own correlation, with the same bound.
   */
  _readWidenings = new BoundedLruCache(TOOL_CALL_ATTRIBUTION_CAPACITY);
  _lspReadWarmState = /* @__PURE__ */ new Map();
  _pendingInlineBlockers = new PathKeyedMap(normalizeMapKey);
  /**
   * #3507: one per-path order for BOTH verbs on `_pendingInlineBlockers`,
   * the widget store's guard shape (`widget-state.ts` diagnosticsWriteGuard).
   * Pipelines of two same-file edits overlap under pi's parallel tools, so
   * an older run that settles last must neither erase nor replace the newer
   * run's verdict (#1198 invariants 1-2). The last applied token lives here,
   * not on the record, so it survives a clear.
   */
  _inlineBlockerWriteOrder = new WriteOrderingGuard();
  /**
   * #3218 criterion 2: inline blockers a fresh clean verdict retired this
   * turn, keyed by resolved path so a file is named once. Bounded at
   * `MAX_RESOLVED_BLOCKER_FILES`; the overflow is counted so the delivery can
   * still say "… and N more" (counting retire events past the cap). The
   * turn-end composer consumes it, so a retire is delivered exactly once; an
   * entry no turn_end delivered stays until one does, and session reset clears
   * it.
   */
  _resolvedBlockerFilesThisTurn = new PathKeyedMap(normalizeMapKey);
  _resolvedBlockerFilesDropped = 0;
  _actionableWarningsThisTurn = new TurnWarningsBySession();
  _codeQualityWarningsThisTurn = new TurnWarningsBySession();
  // #484: opt-in per-RUN summary of diagnostics/autofixes/formats,
  // accumulated across the run's turns and consumed once at the
  // agent_settled quiet window. The collector itself is always constructed
  // (cheap, empty Map) but callers gate recording behind the
  // `lens-turn-summary` flag so it's a true no-op when the feature is off.
  _turnSummary = new TurnSummaryCollector();
  // #2402/#1053: session-scoped applied-edit records for exact-retry
  // recognition. Both producers (partial-apply commit, full native-edit
  // success) write here; the preflight consults it before declaring an
  // oldText miss, so an identical retry never re-executes a committed write.
  partialApplyRecords = new PartialApplyRecordStore();
  resetForSession(startedAt = Date.now()) {
    const previous = this._scope;
    retireScope(previous, "superseded");
    this._scope = beginScope({
      role: "primary",
      parentScopeId: previous.scopeId,
      coordinatorId: this._coordinatorId
    });
    this._sessionStartedAt = startedAt;
    this._complexityBaselines.clear();
    this._pipelineCrashCounts.clear();
    this._cachedExports.clear();
    this.wordIndex = null;
    this._startupScansInFlight.clear();
    this._cascadeRuns = [];
    this._cutAdvisoryItems.clear();
    this._pendingCascadeRuns = [];
    this._turnEndCascadeSettleStarts.clear();
    this._cascadeSessionStats = {
      runs: 0,
      diagnosticsSurfaced: 0,
      coldSnapshotTouches: 0
    };
    this._fixedThisTurn.clear();
    this._writtenThisTurn.clear();
    this._autofixDemotedThisTurn.clear();
    this._reportedThisTurn.clear();
    this._mutationReceipts = [];
    this._droppedMutationReceipts = 0;
    this._actionableWarningsThisTurn.clear(this._telemetrySessionId);
    this._codeQualityWarningsThisTurn.clear(this._telemetrySessionId);
    this._telemetrySessionId = `lens-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
    this._hasStableSessionId = false;
    this._telemetryModel = "unknown";
    this._telemetryModelId = "";
    this._telemetryProvider = "";
    this._telemetryProviderIsExplicit = false;
    this._turnIndex = 0;
    this._turnStarts = 0;
    this._writeIndex = 0;
    this._projectSeq = 0;
    this._viewMissingThrough = 0;
    this._viewLogEntries = 0;
    this._turnStartProjectSeq = 0;
    this._fileSeq.clear();
    this._fileLastProjectSeq.clear();
    this._gitGuardHasBlockers = false;
    this._gitGuardSummary = "";
    this._gitGuardCacheUnknownReason = void 0;
    this._readGuard = null;
    this._pendingDeferredMutations.clear();
    this._toolCallAttributions.clear();
    this._readWidenings.clear();
    this._lspReadWarmState.clear();
    this._pendingInlineBlockers.clear();
    this._inlineBlockerWriteOrder.clear();
    this._resolvedBlockerFilesThisTurn.clear();
    this._resolvedBlockerFilesDropped = 0;
    this._turnSummary.clear();
    this.partialApplyRecords.clear();
  }
  get sessionStartedAt() {
    return this._sessionStartedAt;
  }
  get cascadeSessionStats() {
    return this._cascadeSessionStats;
  }
  recordCascadeRun(diagnosticsSurfaced, coldSnapshotTouches) {
    this._cascadeSessionStats.runs += 1;
    this._cascadeSessionStats.diagnosticsSurfaced += diagnosticsSurfaced;
    this._cascadeSessionStats.coldSnapshotTouches += coldSnapshotTouches;
  }
  /**
   * Record the turn-end policy verdict on every live inline-blocker record
   * (#3248): `true` for a file whose every blocker the policy suppressed,
   * `false` for every other record in the map.
   *
   * ONE writer, rewriting the WHOLE axis each turn end, so the verdict can
   * never outlive the pass that made it: a record demoted since, or one whose
   * survivors came back because the file's bytes moved under a content-bound
   * anchor, is cleared by the same call that sets its sibling. A fresh
   * dispatch replaces the record wholesale (`recordInlineBlockers`), so a NEW
   * blocker is never born pre-suppressed (#1198 ordering).
   *
   * @returns how many records this call CHANGED — 0 means the gate's view of
   * the map is already what the policy just decided.
   */
  applyInlineBlockerPolicyVerdicts(suppressedFilePaths) {
    const suppressed = new Set(suppressedFilePaths.map((filePath) => path8.resolve(filePath)));
    let changed = 0;
    for (const [key, entry] of this._pendingInlineBlockers.entries()) {
      const policySuppressed = suppressed.has(path8.resolve(entry.filePath));
      if ((entry.policySuppressed ?? false) === policySuppressed)
        continue;
      this._pendingInlineBlockers.set(key, { ...entry, policySuppressed });
      changed += 1;
    }
    return changed;
  }
  updateGitGuardStatus(hasBlockers, output) {
    const blocking = this.getInlineBlockersSnapshot().filter((entry) => !entry.policySuppressed);
    this._gitGuardHasBlockers = hasBlockers || blocking.length > 0;
    if (!this._gitGuardHasBlockers) {
      this._gitGuardSummary = "";
      return;
    }
    const firstLine2 = output.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
    const summaries = blocking.map((entry) => entry.summary.trim()).filter(Boolean);
    this._gitGuardSummary = (summaries[0] ?? firstLine2 ?? "Unresolved blockers detected").slice(0, 160);
  }
  get gitGuardHasBlockers() {
    return this._gitGuardHasBlockers;
  }
  get gitGuardSummary() {
    return this._gitGuardSummary;
  }
  markGitGuardCacheUnknown(reason) {
    this._gitGuardCacheUnknownReason = reason;
  }
  clearGitGuardCacheUnknown() {
    this._gitGuardCacheUnknownReason = void 0;
  }
  get gitGuardCacheUnknownReason() {
    return this._gitGuardCacheUnknownReason;
  }
  /**
   * Start a turn. `sessionId` is the turn's own session (its ctx's stable id);
   * omitted, or equal to this coordinator's session, the turn is the
   * coordinator's own. #3613 (S4, N2): a concurrent secondary's turn on this
   * coordinator advances only its own turn identity and its own per-turn
   * records. The turn counter, the write-order turn, the turn's change
   * window and the carried cascade runs are the primary's.
   */
  beginTurn(sessionId) {
    this._turnStarts += 1;
    const turnSession = this.turnSession(sessionId);
    if (turnSession !== this._telemetrySessionId) {
      beginTurnContext(turnSession);
      this._foreignTurns += 1;
      this._foreignTurnKeys.set(turnSession, this._turnIndex + this._foreignTurns / (this._foreignTurns + 1));
      this._actionableWarningsThisTurn.clear(turnSession);
      this._codeQualityWarningsThisTurn.clear(turnSession);
      return;
    }
    this._cascadeRuns = this._cascadeRuns.flatMap((run) => {
      const carriedTurns = (run.carriedTurns ?? 0) + 1;
      if (carriedTurns > 1) {
        logCascade({
          phase: "cascade_carry_over_drop",
          filePath: run.filePath,
          neighborCount: run.neighborCount,
          diagnosticCount: run.diagnosticCount,
          metadata: { carriedTurns, turnIndex: this._turnIndex }
        });
        return [];
      }
      return [{ ...run, carriedTurns }];
    });
    this._actionableWarningsThisTurn.clear(turnSession);
    this._codeQualityWarningsThisTurn.clear(turnSession);
    this._turnStartProjectSeq = this._projectSeq;
    this._turnIndex += 1;
    this._writeOrderTurn = nextOrderTurn();
    beginTurnContext(this._telemetrySessionId);
    this._writeIndex = 0;
    this._reportedThisTurn.clear();
    this._writtenThisTurn.clear();
    this._autofixDemotedThisTurn.clear();
  }
  /**
   * THE one mutation seam. Every producer of an in-project file mutation —
   * native write/edit, recognized bash writes, format/autofix, LSP workspace
   * edits, and (phase 2) opaque script-write recovery — records through here,
   * once: it bumps the seq store (`bumpFileSeq`), appends a bounded attributed
   * receipt, and optionally appends the durable change-log entry. Consumers
   * derive from `getFilesChangedSince` / `getMutationsSince`; no producer or
   * consumer may hand-roll a parallel bump+log pairing (#2000 phase 1).
   *
   * Receipt recording never throws; change-log append failures route to
   * `onAppendError` (default: swallowed) so telemetry cannot break dispatch.
   */
  recordProjectMutation(args) {
    let logged;
    if (args.cwd !== void 0) {
      try {
        const cwd = args.cwd;
        appendProjectChangeAllocated(cwd, (logMaxSeq) => {
          const ownSeq = this._projectSeq;
          logged = this.bumpFileSeq(args.filePath, logMaxSeq);
          if (this._viewMissingThrough > 0) {
            recordViewIncomplete(cwd, { ownSeq, logMaxSeq });
          }
          return {
            seq: logged.projectSeq,
            timestamp: (/* @__PURE__ */ new Date()).toISOString(),
            sessionId: this.telemetrySessionId,
            turnIndex: this.turnIndex,
            source: args.source,
            filePath: path8.resolve(args.filePath),
            fileSeq: logged.fileSeq,
            changedRange: args.changedRange
          };
        });
      } catch (err) {
        args.onAppendError?.(err);
      }
    }
    const { projectSeq, fileSeq, key } = logged ?? this.bumpFileSeq(args.filePath);
    if (this._mutationReceipts.length >= MAX_MUTATION_RECEIPTS) {
      this._mutationReceipts.shift();
      this._droppedMutationReceipts += 1;
    }
    this._mutationReceipts.push({
      seq: projectSeq,
      // Reuse the bump's normalized key (a realpath syscall: ~200us on
      // Windows, ~1.8us on POSIX since #3098) — never re-derive it here.
      filePath: key,
      source: args.source,
      turnIndex: this._turnIndex,
      ts: Date.now()
    });
    return { projectSeq, fileSeq };
  }
  /**
   * Attributed mutations whose seq is strictly after `seq` — the receipt-level
   * counterpart of {@link getFilesChangedSince}. Bounded: entries evicted by
   * the ring cap are gone; compare `droppedMutationReceiptCount` against zero
   * before treating the result as a complete window.
   */
  getMutationsSince(seq) {
    return this._mutationReceipts.filter((r) => r.seq > seq).map((r) => ({ ...r }));
  }
  get droppedMutationReceiptCount() {
    return this._droppedMutationReceipts;
  }
  /**
   * Atomically records write/edit ordering before debounce can coalesce it.
   *
   * Takes the classified {@link MutationKind}, not a raw tool name (#2423):
   * an edit-shaped tool pi-lens does not name still demotes this file to the
   * deferred pass, which is the safe timing.
   */
  recordMutationToolReceipt(filePath, kind) {
    if (kind === "write") {
      this._writtenThisTurn.set(filePath, true);
    } else if (this._writtenThisTurn.has(filePath)) {
      this._autofixDemotedThisTurn.set(filePath, true);
      this._fixedThisTurn.delete(filePath);
    }
    return {
      autofixMode: kind === "edit" || this._autofixDemotedThisTurn.has(filePath) ? "deferred" : "immediate"
    };
  }
  get reportedThisTurn() {
    return this._reportedThisTurn;
  }
  nextWriteIndex() {
    this._writeIndex += 1;
    return this._writeIndex;
  }
  peekWriteIndex() {
    return this._writeIndex;
  }
  /**
   * #3540: draw the next write index as an order token that spans turns
   * (`writeOrderToken`), for a writer whose token is compared against
   * another turn's: the widget store and the inline-blocker retire.
   */
  nextWriteOrderToken() {
    return writeOrderToken(this._writeOrderTurn, this.nextWriteIndex());
  }
  setTelemetryIdentity(identity) {
    if (identity.sessionId && identity.sessionId.trim()) {
      this._telemetrySessionId = identity.sessionId.trim();
      setTurnContextSession(this._telemetrySessionId);
    }
    const model = identity.model?.trim();
    const provider = identity.provider?.trim();
    if (model && provider) {
      this._telemetryModel = `${provider}/${model}`;
    } else if (model) {
      this._telemetryModel = model;
    } else if (provider) {
      this._telemetryModel = provider;
    }
    if (model)
      this._telemetryModelId = model;
    if (provider) {
      this._telemetryProvider = provider;
      this._telemetryProviderIsExplicit = true;
    } else if (model && !this._telemetryProviderIsExplicit) {
      this._telemetryProvider = deriveProviderFromModelId(model);
    }
  }
  get telemetrySessionId() {
    return this._telemetrySessionId;
  }
  /**
   * Pin the session identity to pi's STABLE session id and record why this
   * session started (#190). Called AFTER {@link resetForSession} (which assigns
   * a fresh random id), so the stable id — when pi provides one via
   * `ctx.sessionManager.getSessionId()` — wins and survives a quit→resume.
   */
  setSessionLifecycle(args) {
    if (args.sessionId && args.sessionId.trim()) {
      this._telemetrySessionId = args.sessionId.trim();
      this._hasStableSessionId = true;
    }
    this._lifecycleReason = args.reason;
  }
  /** Why the current session started: new | resume | fork | reload | startup. */
  get sessionLifecycleReason() {
    return this._lifecycleReason;
  }
  /** True once a stable pi session id has been pinned (vs the random fallback). */
  get hasStableSessionId() {
    return this._hasStableSessionId;
  }
  get telemetryModel() {
    return this._telemetryModel;
  }
  /** Raw model id (never the combined `provider/model` display string), blank
   * when the host hasn't reported one this session. Worklog/disposition
   * attribution (#1448) reads this, not {@link telemetryModel}. */
  get telemetryModelId() {
    return this._telemetryModelId;
  }
  /** Explicit host-reported provider, or a conservative derivation from the
   * model id (see clients/model-provider.ts), blank when neither is known. */
  get telemetryProviderId() {
    return this._telemetryProvider;
  }
  get turnIndex() {
    return this._turnIndex;
  }
  /**
   * #3613 G1: the turn starts of every session on this coordinator since the
   * session reset. A process-level turn cadence (the memory sample, the
   * smells re-check) paces on it: an integer, so its `% N` gate is met, and
   * moved by a subagent's turns, which run inside one primary turn.
   */
  get turnStartCount() {
    return this._turnStarts;
  }
  /** #3540 r2: the order turn a write token is drawn in; never restarts. */
  get writeOrderTurn() {
    return this._writeOrderTurn;
  }
  get projectSeq() {
    return this._projectSeq;
  }
  /** True when a snapshot of this runtime must not claim `projectSeq` (#3511). */
  get viewMissesLoggedEntries() {
    return this._viewMissingThrough > 0;
  }
  /** The change-log entries this session's seed or merge folded (#3511). */
  get viewLogEntries() {
    return this._viewLogEntries;
  }
  get turnStartProjectSeq() {
    return this._turnStartProjectSeq;
  }
  seedProjectSequence(projectSeq, fileSeqByPath, logEntries = 0) {
    this._projectSeq = Math.max(0, Math.floor(projectSeq));
    this._viewMissingThrough = 0;
    this._viewLogEntries = logEntries;
    this._turnStartProjectSeq = this._projectSeq;
    this._fileSeq.clear();
    this._fileLastProjectSeq.clear();
    for (const [filePath, seq] of fileSeqByPath ?? []) {
      this._fileSeq.set(normalizeMapKey(path8.resolve(filePath)), Math.max(0, seq));
    }
  }
  /**
   * #3511 review B2: fold a late sequence read (session_start's deferred one,
   * after a timed-out read seeded 0) into a session that has already
   * advanced. Unlike `seedProjectSequence` it never lowers the seq or a
   * file's seq, and keeps the in-window changed-since marks. It clears the
   * incomplete mark only when the read reaches every entry the view missed:
   * locked allocation appends entries in rising seq order, so a read whose
   * max is at or above that seq saw all of them. An unlocked entry breaks
   * that order; readers catch it by its tag after the fold point, which the
   * merge only ever advances (review round 2).
   */
  mergeProjectSequence(projectSeq, fileSeqByPath, logEntries = 0) {
    this._projectSeq = Math.max(this._projectSeq, Math.floor(projectSeq));
    this._viewLogEntries = Math.max(this._viewLogEntries, logEntries);
    for (const [filePath, seq] of fileSeqByPath) {
      const key = normalizeMapKey(path8.resolve(filePath));
      this._fileSeq.set(key, Math.max(this._fileSeq.get(key) ?? 0, seq));
    }
    if (projectSeq >= this._viewMissingThrough)
      this._viewMissingThrough = 0;
  }
  bumpFileSeq(filePath, logMaxSeq = 0) {
    const key = normalizeMapKey(path8.resolve(filePath));
    if (logMaxSeq > this._projectSeq) {
      this._projectSeq = logMaxSeq;
      this._viewMissingThrough = Math.max(this._viewMissingThrough, logMaxSeq);
    }
    this._projectSeq += 1;
    const fileSeq = (this._fileSeq.get(key) ?? 0) + 1;
    this._fileSeq.set(key, fileSeq);
    this._fileLastProjectSeq.set(key, this._projectSeq);
    return { projectSeq: this._projectSeq, fileSeq, key };
  }
  /**
   * Files whose most recent bump happened AFTER `seq` — i.e. every file the
   * review graph would need to re-ingest to catch up from a build taken at
   * projectSeq `seq` (#451). Returns NORMALIZED keys (normalizeMapKey +
   * path.resolve), the same form the builder's fileSignatures map uses, so the
   * caller can compare without re-normalizing.
   */
  getFilesChangedSince(seq) {
    const changed = [];
    for (const [key, lastSeq] of this._fileLastProjectSeq) {
      if (lastSeq > seq)
        changed.push(key);
    }
    return changed;
  }
  getFileSeq(filePath) {
    return this._fileSeq.get(normalizeMapKey(path8.resolve(filePath))) ?? 0;
  }
  getFileSeqEntries() {
    return [...this._fileSeq.entries()];
  }
  get sessionGeneration() {
    return this._scope.scopeId;
  }
  /** #3611: the session scope this coordinator serves. */
  get sessionScope() {
    return this._scope;
  }
  /**
   * #3499: a handle on the current session, for a write that lands after an
   * await which can outlive the session (a fire-and-forget quiet window).
   * #3611: a `LineageHandle` on the current scope.
   */
  captureSessionGeneration() {
    return this._scope.capture();
  }
  isCurrentSession(generation) {
    return this._scope.scopeId === generation && this._scope.isLive();
  }
  markStartupScanInFlight(name, generation) {
    this._startupScansInFlight.set(name, generation);
  }
  clearStartupScanInFlight(name, generation) {
    const owner = this._startupScansInFlight.get(name);
    if (owner === generation) {
      this._startupScansInFlight.delete(name);
    }
  }
  isStartupScanInFlight(name) {
    return this._startupScansInFlight.has(name);
  }
  formatPipelineCrashNotice(filePath, err) {
    const key = path8.resolve(filePath);
    const count = (this._pipelineCrashCounts.get(key) ?? 0) + 1;
    this._pipelineCrashCounts.set(key, count);
    const message = err instanceof Error ? err.message : String(err);
    const shortMessage = message.split("\n")[0].slice(0, 220);
    const shouldSurface = count <= RUNTIME_CONFIG.crashNotice.alwaysShowFirstN || count % RUNTIME_CONFIG.crashNotice.showEveryNth === 0;
    if (!shouldSurface)
      return "";
    return [
      "\u26A0\uFE0F pi-lens pipeline crashed while analyzing this write.",
      `File: ${path8.basename(filePath)} | crash count this session: ${count}`,
      `Error: ${shortMessage}`,
      "Recovery: LSP service was reset. If this repeats, rerun with --no-lsp and report the file + stack."
    ].join("\n");
  }
  getCrashEntries() {
    return Array.from(this._pipelineCrashCounts.entries());
  }
  get projectRoot() {
    return this._projectRoot;
  }
  set projectRoot(value) {
    this._projectRoot = normalizeMapKey(value);
  }
  get errorDebtBaseline() {
    return this._errorDebtBaseline;
  }
  set errorDebtBaseline(value) {
    this._errorDebtBaseline = value;
  }
  get cachedExports() {
    return this._cachedExports;
  }
  appendCascadeRun(run) {
    this._cascadeRuns.push(run);
  }
  /**
   * #3512: `generation` is the session the compute was dispatched in. The
   * admitting handler awaits the pipeline first and can resume after a
   * same-cwd replacement's reset, so a capture taken here would name the new
   * session; it is therefore required. `filePath` is the edited file the
   * compute belongs to, the subject of a dropped admission's ledger row.
   */
  appendCascadePromise(p, generation, filePath) {
    if (generation.guardedWrite(filePath, () => true) === void 0)
      return;
    if (this._pendingCascadeRuns.length < MAX_PENDING_CASCADE_RUNS) {
      this._pendingCascadeRuns.push(p);
      return;
    }
    recordDegradationOnce({
      kind: "cascade-pending-cap",
      subject: "runtime-coordinator",
      reason: `deferred cascade admission capped at ${MAX_PENDING_CASCADE_RUNS}`
    });
    void p.then((run) => generation.guardedWrite(run.filePath, () => this.appendCascadeRun(run))).catch(() => {
    });
  }
  /**
   * The active turn_end settle clock, or undefined outside that wait.
   *
   * #1462 review F-F: every cascade compute in flight on this coordinator
   * reads the SAME clock. `_turnEndCascadeSettleStarts` is keyed by settle
   * token so `settleCascadeRuns` can distinguish its own drain from a nested
   * one, but this getter always answers with the latest active window —
   * there is one turn_end per runtime, not one per caller. A cascade begun
   * moments before turn_end and one begun long before it both measure elapsed
   * time against the same start, which is correct: they are racing the same
   * settle wait and share its deadline, not separate ones.
   */
  getTurnEndCascadeSettleStart() {
    let latest;
    for (const start of this._turnEndCascadeSettleStarts.values())
      latest = start;
    return latest;
  }
  /**
   * Drain the deferred cascade computes kicked off this turn (#450), racing them
   * against a bounded wait. Fulfilled runs feed the same accumulator as inline
   * runs (appendCascadeRun). A promise still pending at the cap is retained so a
   * late-resolving compute is picked up on the next turn_end rather than lost.
   * The stored promises never reject (pipeline guarantees an "error" skip-run).
   */
  async settleCascadeRuns(maxWaitMs, settleOptions = {}) {
    const pending2 = this._pendingCascadeRuns;
    if (pending2.length === 0)
      return { settled: 0, timedOut: 0 };
    const { generation } = settleOptions;
    if (generation && generation.guardedWrite("cascade-settle", () => true) === void 0)
      return { settled: 0, timedOut: 0 };
    this._pendingCascadeRuns = [];
    const settleToken = settleOptions.trackTurnEndClock ? ++this._nextCascadeSettleToken : void 0;
    if (settleToken !== void 0) {
      this._turnEndCascadeSettleStarts.set(settleToken, Date.now());
    }
    try {
      const tracked = pending2.map((p) => {
        const entry = { done: false, promise: p };
        entry.promise = p.then((run) => {
          entry.done = true;
          entry.run = run;
          return run;
        });
        return entry;
      });
      const timeout = new Promise((resolve30) => {
        setTimeout(resolve30, maxWaitMs).unref?.();
      });
      await Promise.race([
        Promise.allSettled(tracked.map((t) => t.promise)),
        timeout
      ]);
      const commit = (subject, write) => {
        if (generation)
          generation.guardedWrite(subject, write);
        else
          write();
      };
      let settled = 0;
      let timedOut = 0;
      for (const entry of tracked) {
        const { run } = entry;
        if (entry.done && run) {
          commit(run.filePath, () => this.appendCascadeRun(run));
          settled += 1;
        } else {
          commit("cascade-pending", () => this._pendingCascadeRuns.push(entry.promise));
          timedOut += 1;
        }
      }
      return { settled, timedOut };
    } finally {
      if (settleToken !== void 0) {
        this._turnEndCascadeSettleStarts.delete(settleToken);
      }
    }
  }
  /**
   * #3813/#3901: park the items a lane's advisory showed and the turn-end cap
   * cut. Their producers have no queue to restore (the scan cache is
   * overwritten and the edited-file worklist retired before the cap runs), so
   * the lane re-checks each parked item against its next fresh run. Replaces
   * the lane's earlier entry. Returns the lane evicted to stay within
   * `MAX_CUT_ADVISORY_LANES`, if any.
   */
  parkCutAdvisoryItems(lane, items) {
    this._cutAdvisoryItems.delete(lane);
    if (items.length === 0)
      return [];
    return this._cutAdvisoryItems.set(lane, [...items]).map(([key]) => key);
  }
  /** Remove and return a lane's parked items: they are offered once. */
  takeCutAdvisoryItems(lane) {
    const items = this._cutAdvisoryItems.get(lane);
    this._cutAdvisoryItems.delete(lane);
    return items ?? [];
  }
  consumeCascadeRuns() {
    const runs = this._cascadeRuns;
    this._cascadeRuns = [];
    return runs;
  }
  /**
   * R1 (#1443 follow-up): non-destructive peek used by turn_end's read-only
   * fast path. A carried cascade run (or one still in flight) represents a
   * DELIVERY OPPORTUNITY, not turn activity — an agent that answers a question
   * without editing anything must still get yesterday's late finding. Before
   * this, the files-empty early return skipped `settleCascadeRuns` /
   * `consumeCascadeRuns` entirely on a read-only turn, so `beginTurn`'s next
   * carry pass saw the run as having survived a turn_start with no offsetting
   * drain and dropped it — burning the one-turn carry allowance on a turn that
   * never had a chance to deliver.
   */
  hasCascadeRuns() {
    return this._cascadeRuns.length > 0;
  }
  /**
   * Record a file's blocking verdict. Returns its freshness baseline, or
   * undefined when a newer dispatch of the same file already recorded or
   * cleared it (#3507). `recordedAtMs` is the pipeline's analysis read time
   * (#3503); a caller without one stamps now.
   */
  recordInlineBlockers(filePath, summary, writeIndex, sources, lines, contentBaseline, diagnostics, orderTurn = this._writeOrderTurn, recordedAtMs = Date.now()) {
    const writeOrder = writeOrderToken(orderTurn, writeIndex);
    if (!this._inlineBlockerWriteOrder.shouldWrite(normalizeMapKey(filePath), writeOrder))
      return void 0;
    this._resolvedBlockerFilesThisTurn.delete(filePath);
    this._pendingInlineBlockers.set(path8.resolve(filePath), {
      filePath,
      summary,
      writeIndex,
      writeOrder,
      sources,
      lines,
      diagnostics,
      recordedAtMs,
      stale: false,
      ...contentBaseline ? {
        recordedSize: contentBaseline.size,
        recordedHash: contentBaseline.sha256
      } : {}
    });
    return recordedAtMs;
  }
  /**
   * #3814: add a settled collect-later runner's blocking findings to the file's
   * inline-blocker record, the one channel the commit gate's latch
   * (`updateGitGuardStatus`) and the turn-end replay both read. A deferred
   * runner answers after `recordInlineBlockers` has run for its edit, so it
   * MERGES into the record instead of replacing it: the fast runners' blockers
   * for the same bytes stay.
   *
   * Idempotent by finding identity, because two callers hand the same answer
   * here: the commit gate (before turn end) and the turn-end drain (after).
   * Ordering needs no token of its own: the caller admits only answers the
   * path-freshness gate called current, and a later write to the file replaces
   * or clears the whole record (`recordInlineBlockers` / `clearInlineBlockers`).
   * A new record carries the analysed bytes' baseline, so a disposition verdict
   * on it is confirmable by the gate (`firstExpiredSuppressionVerdict`); a
   * merge keeps the existing baseline and its stale demotion. An existing
   * record with no structured diagnostics is left alone: the file already
   * blocks, and merging into text alone would drop it from the replay.
   *
   * @returns how many findings were new to the record (0 = a replay).
   */
  recordDeferredInlineBlockers(filePath, blocking, analysed) {
    const key = path8.resolve(filePath);
    const existing = this._pendingInlineBlockers.get(key);
    if (existing && !existing.diagnostics)
      return 0;
    const identity = (d) => JSON.stringify([d.tool, d.id, d.line, d.message]);
    const known = new Set((existing?.diagnostics ?? []).map(identity));
    const added = blocking.flatMap((d) => known.has(identity(d)) ? [] : [d]);
    if (added.length === 0)
      return 0;
    const diagnostics = [...existing?.diagnostics ?? [], ...added];
    this._pendingInlineBlockers.set(key, {
      ...existing ?? {
        filePath,
        recordedAtMs: analysed.recordedAtMs,
        stale: false,
        ...analysed.contentBaseline ? {
          recordedSize: analysed.contentBaseline.size,
          recordedHash: analysed.contentBaseline.sha256
        } : {}
      },
      summary: formatDiagnostics(diagnostics, "blocking").trim(),
      diagnostics,
      // The same two derivations the pipeline's writer uses: `lines` keeps only
      // this file's own rows, `sources` pins an untagged finding as "unknown".
      sources: [
        .../* @__PURE__ */ new Set([
          ...existing?.sources ?? [],
          ...inlineBlockerSources(added)
        ])
      ],
      lines: [
        ...existing?.lines ?? [],
        ...inlineBlockerLines(added, filePath)
      ],
      // The turn-end policy verdict was about the OLD set; the added findings
      // passed the policy already and the next turn end re-derives the rest.
      policySuppressed: false
    });
    return added.length;
  }
  /**
   * #3218 criterion 2: remember one file whose blocker a clean verdict just
   * retired, for the turn-end "Resolved" line. Called from the two retire
   * seams (`clearInlineBlockers` and `retireInlineBlockerOnConfirmedClean`)
   * with the record that was removed and the retiring write's order, so the
   * composer never has to re-derive the count from rendered text. Bounded at
   * `MAX_RESOLVED_BLOCKER_FILES`; the overflow is counted for the "… and N
   * more" tail.
   */
  noteResolvedBlockerFile(record, retiringWriteOrder, confirmedClean) {
    if (!this._resolvedBlockerFilesThisTurn.has(record.filePath) && this._resolvedBlockerFilesThisTurn.size >= MAX_RESOLVED_BLOCKER_FILES) {
      this._resolvedBlockerFilesDropped += 1;
      return;
    }
    this._resolvedBlockerFilesThisTurn.set(record.filePath, {
      filePath: record.filePath,
      blockerCount: record.diagnostics?.length ?? record.lines?.length ?? 1,
      // A write order token carries its turn above the write index
      // (`writeOrderToken`); the line names only the write.
      writeIndex: confirmedClean || retiringWriteOrder === void 0 ? void 0 : retiringWriteOrder % 2 ** 32,
      confirmedClean,
      turnIndex: this._turnIndex
    });
  }
  /**
   * Clear a file's verdict after a clean dispatch. Returns false when a newer
   * dispatch of the same file already recorded or cleared it (#3507), or when
   * `analysis` ran without authorship over the very bytes the record is about
   * (#4137 round 3). Such a run withholds the blocker channel (#3226), so its
   * clean result is no evidence against an authored verdict on the same bytes;
   * before this, an opaque recovery of bytes the agent authored cleared the
   * record whenever the already-analysed latch was gone (a concurrent
   * session's turn start, the session's own next turn), and turn end then
   * reported a false "Resolved". The refusal consumes no write order and is
   * counted as `blocker-clear-refused` per file key. A record with no
   * `recordedHash` (#2982: the file was unreadable or over the baseline cap at
   * record time) cannot be matched and is cleared as before.
   */
  clearInlineBlockers(filePath, writeIndex, orderTurn = this._writeOrderTurn, analysis) {
    const existing = this._pendingInlineBlockers.get(path8.resolve(filePath));
    if (existing && analysis && !analysis.authored && analysis.sha256 !== void 0 && existing.recordedHash === analysis.sha256) {
      incrementDegradationCount({
        kind: "blocker-clear-refused",
        subject: normalizeMapKey(filePath),
        reason: "an analysis without authorship read the bytes an authored verdict is about; its clean result does not retire the record (#4137)"
      });
      return false;
    }
    if (!this._inlineBlockerWriteOrder.shouldWrite(normalizeMapKey(filePath), writeOrderToken(orderTurn, writeIndex)))
      return false;
    if (existing) {
      this.noteResolvedBlockerFile(existing, writeOrderToken(orderTurn, writeIndex), false);
    }
    this._pendingInlineBlockers.delete(path8.resolve(filePath));
    return true;
  }
  /**
   * #1631: demote a cached blocker to stale without dropping it (#1419
   * demote-not-drop). Called by the turn-boundary freshness sweep when the
   * file or one of its forward imports drifted on disk after the verdict.
   * Idempotent: re-marking an already-stale entry (for ANY reason) is a
   * no-op — a `"past-eof"` demotion already took this record out of the
   * authoritative channel, and `sweepInlineBlockerFreshness` re-checks next
   * turn once that heals. Returns true only when this call transitioned the
   * entry to stale, so a caller can log the demotion exactly once.
   */
  markInlineBlockerStale(filePath, reason = "dependency-drift") {
    const key = path8.resolve(filePath);
    const existing = this._pendingInlineBlockers.get(key);
    if (!existing || existing.stale)
      return false;
    this._pendingInlineBlockers.set(key, {
      ...existing,
      stale: true,
      staleReason: reason
    });
    return true;
  }
  /**
   * #1641: re-derive the past-EOF demotion for one inline-blocker record.
   * Unlike {@link markInlineBlockerStale} (a one-way latch for the
   * dependency-drift gate), this RE-ARMS: called every turn end with the
   * gate's freshly-computed verdict, it can both demote (rising edge) and
   * heal (falling edge — a transient shrink-then-restore of the file). Never
   * touches a record a sibling gate demoted (`staleReason !== "past-eof"`
   * while `stale`) — composing with #1631's dependency-drift gate means each
   * gate only heals its own demotions. Returns true only on an actual
   * transition, so the caller logs/resyncs exactly once per edge.
   */
  setInlineBlockerPastEofStale(filePath, isPastEof) {
    const key = path8.resolve(filePath);
    const existing = this._pendingInlineBlockers.get(key);
    if (!existing)
      return false;
    if (existing.stale && existing.staleReason !== "past-eof")
      return false;
    const currentlyPastEof = !!existing.stale && existing.staleReason === "past-eof";
    if (currentlyPastEof === isPastEof)
      return false;
    this._pendingInlineBlockers.set(key, {
      ...existing,
      stale: isPastEof,
      staleReason: isPastEof ? "past-eof" : void 0
    });
    return true;
  }
  /**
   * #2982: re-derive the self-drift demotion for one inline-blocker record.
   *
   * Modelled on {@link setInlineBlockerPastEofStale}, not on
   * {@link markInlineBlockerStale}: it RE-ARMS rather than latching, so a
   * file whose bytes drift and then come back (a checkout, a revert, an
   * editor writing the original content) un-demotes on the next sweep
   * instead of staying demoted for the session. That re-arm is what lets
   * this axis skip the #1950 delivery cap entirely — a record that can heal
   * needs no bounded-noise retirement, and applying the cap here would walk
   * an ast-grep or tree-sitter security finding out of turn-end rendering
   * permanently (#2982 review).
   *
   * Composes with the sibling gates the same way they compose with each
   * other: it never touches a record another gate demoted, and never heals a
   * demotion it did not make. Returns true only on an actual transition, so
   * the caller counts exactly one edge.
   */
  setInlineBlockerSelfDriftStale(filePath, isSelfDrift) {
    const key = path8.resolve(filePath);
    const existing = this._pendingInlineBlockers.get(key);
    if (!existing)
      return false;
    if (existing.stale && existing.staleReason !== "self-drift")
      return false;
    const currentlySelfDrift = !!existing.stale && existing.staleReason === "self-drift";
    if (currentlySelfDrift === isSelfDrift)
      return false;
    const { staleReason: _cleared, ...rest } = existing;
    this._pendingInlineBlockers.set(key, {
      ...rest,
      stale: isSelfDrift,
      ...isSelfDrift ? { staleReason: "self-drift" } : {}
    });
    return true;
  }
  /**
   * #1944: retire a past-EOF demotion after its ONE degraded delivery.
   *
   * The past-EOF gate demotes and re-arms, but nothing ever retired the
   * record, so a blocker whose file shrank past the cited lines re-served on
   * every turn end for the rest of the session — measured live at 80+
   * minutes on session 01a0234c. The re-serve is unbounded, not the six
   * turns the first evidence window suggested.
   *
   * It is unbounded because the record cannot resolve itself. The two
   * clearing events (`clearInlineBlockers` on a later dispatch of the same
   * path, `retireInlineBlockerOnConfirmedClean` on a fresh clean verdict)
   * both need the file to be looked at again, and the agent has no reason to
   * look: the coordinates it was handed do not exist. "Re-run to confirm" is
   * an instruction this record makes impossible to follow.
   *
   * So the delivery surface retires it after serving it once, degraded. That
   * is a DROP, which #1419's demote-not-drop rule normally forbids; the
   * exception is narrow and stated here. The finding was already delivered
   * this turn with its dead coordinates annotated, and what remains is a
   * message pinned to a line the file does not have. `deadLines` is required
   * and must be non-empty, so a record stale for any OTHER reason, or
   * past-EOF with no identified dead line, still stands.
   *
   * @returns true when an entry was retired — the caller records it, so the
   * suppression is never silent (#1432 Gap 1).
   */
  retireDemotedPastEofBlocker(filePath, deadLines) {
    const key = this.pastEofRetireKey(filePath, deadLines);
    if (key === void 0)
      return false;
    this._pendingInlineBlockers.delete(key);
    return true;
  }
  /**
   * #3813: the retire rule of {@link retireDemotedPastEofBlocker}, without
   * the delete. The turn-end composer asks it at render time (to word the
   * retirement note) and only retires once the cap has let the delivery
   * through; the rule stays here, in one place.
   */
  wouldRetireDemotedPastEofBlocker(filePath, deadLines) {
    return this.pastEofRetireKey(filePath, deadLines) !== void 0;
  }
  /** The store key of the record the past-EOF retire rule admits, if any. */
  pastEofRetireKey(filePath, deadLines) {
    if (deadLines.length === 0)
      return void 0;
    const key = path8.resolve(filePath);
    const existing = this._pendingInlineBlockers.get(key);
    if (!existing)
      return void 0;
    if (!existing.stale)
      return void 0;
    return (existing.staleReason ?? "past-eof") === "past-eof" ? key : void 0;
  }
  /**
   * #1950 fix-round F1: read the current delivery count WITHOUT committing
   * an increment. `runtime-turn.ts` needs the count a delivery would reach
   * BEFORE it knows whether that delivery will actually reach the agent —
   * the turn-end signature dedupe (`turn-end-findings-last`) can suppress a
   * turn whose rendered content is identical to the last one, and a
   * suppressed turn must not advance the counter (the agent never saw it).
   * The caller peeks, renders the tentative body, and only calls
   * {@link incrementInlineBlockerStaleDelivery} once it knows the turn's
   * content is NOT being suppressed.
   */
  peekInlineBlockerStaleDeliveryCount(filePath) {
    const key = path8.resolve(filePath);
    return this._pendingInlineBlockers.get(key)?.staleDeliveryCount ?? 0;
  }
  /**
   * #1950: commit one more turn end that re-served this record while
   * `stale`. The caller (`runtime-turn.ts`) only calls this for a
   * `"dependency-drift"` demotion — a `"past-eof"` demotion retires after
   * its own single delivery (#1944, `retireDemotedPastEofBlocker`) and
   * never accumulates a count — and only AFTER confirming the turn's
   * content will not be suppressed by the signature dedupe (see
   * {@link peekInlineBlockerStaleDeliveryCount}), so this counts ACTUAL
   * deliveries the agent saw, not turn-end loop iterations. Returns the new
   * count, or 0 when there is no entry to count against (already retired,
   * or never recorded).
   */
  incrementInlineBlockerStaleDelivery(filePath) {
    const key = path8.resolve(filePath);
    const existing = this._pendingInlineBlockers.get(key);
    if (!existing)
      return 0;
    const next = (existing.staleDeliveryCount ?? 0) + 1;
    this._pendingInlineBlockers.set(key, {
      ...existing,
      staleDeliveryCount: next
    });
    return next;
  }
  /**
   * #1950: retire a dependency-drift demotion once it has been delivered
   * `DEPENDENCY_DRIFT_MAX_DELIVERIES` times with no re-run confirming or
   * clearing it. Unlike {@link retireDemotedPastEofBlocker}, the record is
   * NOT provably unrecoverable — its coordinates are still in bounds, and a
   * fresh dispatch of the file could still confirm or clear it. This is a
   * bounded-noise cap, not a "this can never resolve" verdict, and the
   * caller's ledger reason must say so (`demoted-finding-retired`'s "capped,
   * re-run can still confirm" vs. past-EOF's "retired, unrecoverable").
   *
   * Only ever touches this gate's OWN demotion (`staleReason ===
   * "dependency-drift"`) — a past-EOF demotion on the same file is that
   * gate's own record to retire, not this one's.
   *
   * @returns true when an entry was retired — the caller records it, so the
   * suppression is never silent (#1432 Gap 1).
   */
  retireDemotedDependencyDriftBlocker(filePath) {
    const key = path8.resolve(filePath);
    const existing = this._pendingInlineBlockers.get(key);
    if (!existing)
      return false;
    if (!existing.stale)
      return false;
    if (existing.staleReason !== "dependency-drift")
      return false;
    this._pendingInlineBlockers.delete(key);
    return true;
  }
  /**
   * Retire a file's inline blocker because a FRESH, content-bound diagnostic
   * verdict proved it gone (#1561).
   *
   * The map was invalidated by exactly two events: a later dispatch of the
   * SAME path returning no blockers, and that path ceasing to exist (#1245).
   * Neither covers the common case that produced #1561 — a blocker on file F
   * whose cause lives in file G. Fixing G re-dispatches G, not F, so F's
   * verdict was never re-taken and "Unresolved from this turn" was re-injected
   * for the rest of the session. In the live case the agent then ran
   * `lsp_diagnostics` on F, pi-lens answered "confirmed clean", and the
   * blocker was STILL re-served on the next three turn ends: #571 wired that
   * tool's confirmed result into the widget footer and stopped there.
   *
   * This is the seam #1198 invariant 4 left undecided, decided: an affirmative
   * clean from the authoritative current view retires the stale verdict.
   *
   * Ordering (#1198 invariants 1-2). Both stores draw from the same
   * `nextWriteIndex()` counter, ordered turn first (#3540: the counter
   * restarts at every turn), so when both sides are stamped the retire
   * requires the clean verdict to be strictly NEWER. A slow old clean that
   * settles after a fresh dispatch found real blockers must not erase them.
   * When either side is unstamped the two cannot be ordered at all; the fresh
   * confirmed verdict wins, because it is the only one of the two backed by an
   * actual observation of current content. Only production's single record site
   * is stamped, so in practice the unstamped branch is legacy callers/tests.
   *
   * COVERAGE (#1561 F1). A blocker is retired only by a verdict that can speak
   * for the tools that raised it. Inline blockers come from the whole dispatch,
   * not just the language server: `dispatcher.ts` collects every
   * `semantic === "blocking"` diagnostic across all runners, so eslint,
   * biome-check, actionlint, elixir/gleam-check and ast-grep security rules all
   * land in this map. A language-server check knows nothing about any of them,
   * so `coveredSources` must be a SUPERSET of the recorded sources or the entry
   * stands. This is a commit gate: a record whose provenance is unknown (no
   * `sources`) fails CLOSED and is never retired, because "we don't know what
   * raised this" must not resolve to "an LSP check can clear it".
   *
   * @returns true when an entry was retired — the caller logs it, so an
   * eviction is never silent (#1432 Gap 1).
   */
  retireInlineBlockerOnConfirmedClean(filePath, confirmedAtWriteOrder, coveredSources) {
    const key = path8.resolve(filePath);
    const existing = this._pendingInlineBlockers.get(key);
    if (!existing)
      return false;
    if (existing.writeOrder !== void 0 && confirmedAtWriteOrder !== void 0 && confirmedAtWriteOrder <= existing.writeOrder) {
      return false;
    }
    if (!existing.sources || existing.sources.length === 0)
      return false;
    const covered = new Set(coveredSources ?? []);
    if (!existing.sources.every((source) => covered.has(source)))
      return false;
    this.noteResolvedBlockerFile(existing, void 0, true);
    this._pendingInlineBlockers.delete(key);
    return true;
  }
  /**
   * #3218 criterion 2: a retirement no turn_end has delivered yet. The
   * read-only turn_end path uses it to fall through to the composer instead of
   * returning early, so a `lens_diagnostics` confirmation reaches the agent.
   */
  hasResolvedBlockerFiles() {
    return this._resolvedBlockerFilesThisTurn.size > 0;
  }
  /**
   * #3218 criterion 2: hand the turn-end composer the retired files it can
   * deliver, and remove exactly those, so a retire is delivered once. An entry
   * `holdBack` names STAYS for the next turn_end: the composer holds back a
   * file its own message still lists as unresolved (a retire that landed after
   * the blockers were snapshotted), because one message must never say both,
   * and a line that would not fit the message cap (called in list order).
   * The "… and N more" overflow count rides with a batch that lists files and
   * is otherwise kept, so a bare tail is never delivered.
   */
  consumeResolvedBlockerFiles(holdBack) {
    const files = [];
    const held = [];
    for (const entry of this._resolvedBlockerFilesThisTurn.values()) {
      (holdBack(entry) ? held : files).push(entry);
    }
    this._resolvedBlockerFilesThisTurn.clear();
    for (const entry of held) {
      this._resolvedBlockerFilesThisTurn.set(entry.filePath, entry);
    }
    if (files.length === 0)
      return { files, dropped: 0 };
    const dropped = this._resolvedBlockerFilesDropped;
    this._resolvedBlockerFilesDropped = 0;
    return { files, dropped };
  }
  reconcileInlineBlockers() {
    const survivors = [];
    for (const [displayPath, value] of this._pendingInlineBlockers.entries()) {
      if (fs9.existsSync(displayPath))
        survivors.push([displayPath, value]);
    }
    if (survivors.length !== this._pendingInlineBlockers.size) {
      this._pendingInlineBlockers.clear();
      for (const [displayPath, value] of survivors) {
        this._pendingInlineBlockers.set(displayPath, value);
      }
    }
  }
  /**
   * Stale-entry reconcile (#1245): a blocker recorded for a file that has
   * since been deleted can never be cleared — `clearInlineBlockers` fires
   * only on a LATER dispatch of the same path, which a deleted file never
   * gets. Every read of the blocker map (turn_end injection, git-guard
   * size/summary, `syncGitGuardRecord`) therefore drops entries whose file no
   * longer exists on disk: a blocker for a deleted file is stale by
   * definition (the agent cannot fix it), so it must not re-surface every
   * turn or gate a commit. The map is tiny (per-turn blockers) and reads are
   * bounded (once per turn_end / tool_result), so the probe cost is
   * negligible.
   */
  getInlineBlockersSnapshot() {
    this.reconcileInlineBlockers();
    return [...this._pendingInlineBlockers.values()];
  }
  consumeInlineBlockers() {
    const entries = this.getInlineBlockersSnapshot();
    this._pendingInlineBlockers.clear();
    return entries;
  }
  /**
   * #3613: the partition a per-turn record of `sessionId`'s turn lives in,
   * the rule {@link beginTurn} applies too: a turn with no session id of its
   * own is this coordinator's.
   */
  turnSession(sessionId) {
    return sessionId ?? this._telemetrySessionId;
  }
  recordActionableWarnings(warnings, sessionId) {
    this._actionableWarningsThisTurn.add(this.turnSession(sessionId), warnings);
  }
  peekActionableWarnings(sessionId) {
    return this._actionableWarningsThisTurn.peek(this.turnSession(sessionId));
  }
  clearActionableWarnings(sessionId) {
    this._actionableWarningsThisTurn.clear(this.turnSession(sessionId));
  }
  recordCodeQualityWarnings(warnings, sessionId) {
    this._codeQualityWarningsThisTurn.add(this.turnSession(sessionId), warnings);
  }
  peekCodeQualityWarnings(sessionId) {
    return this._codeQualityWarningsThisTurn.peek(this.turnSession(sessionId));
  }
  clearCodeQualityWarnings(sessionId) {
    this._codeQualityWarningsThisTurn.clear(this.turnSession(sessionId));
  }
  /**
   * #3613 F2: the key a per-turn cap (the observed-mutation budget, a
   * bounded-telemetry `capPerTurn`) uses for the current turn of `sessionId`.
   * This coordinator's own turn is its turn index, as before; a concurrent
   * session's turn is the key its own turn start drew, so each of its turns
   * re-arms and no two sessions' turns share a cap.
   */
  turnKey(sessionId) {
    const turnSession = this.turnSession(sessionId);
    if (turnSession === this._telemetrySessionId)
      return this._turnIndex;
    return this._foreignTurnKeys.get(turnSession) ?? this._turnIndex;
  }
  /**
   * #3613 G2: whether `key` is a live turn key: this coordinator's current
   * turn, or a live concurrent session's current one.
   */
  isLiveTurnKey(key) {
    if (key === this._turnIndex)
      return true;
    for (const live of this._foreignTurnKeys.values())
      if (live === key)
        return true;
    return false;
  }
  /**
   * #3613: a concurrent session's per-turn records end with its session, so
   * a subagent that shuts down before its turn end leaves no partition
   * behind. This coordinator's own records are reset by `resetForSession`.
   */
  forgetTurnSession(sessionId) {
    const turnSession = this.turnSession(sessionId);
    if (turnSession === this._telemetrySessionId)
      return;
    this._foreignTurnKeys.delete(turnSession);
    this._actionableWarningsThisTurn.clear(turnSession);
    this._codeQualityWarningsThisTurn.clear(turnSession);
  }
  /** #484: the per-run diagnostics/autofix/format collector (accumulates
   * across turns; consumed once at the agent_settled quiet window). Always
   * present; callers gate recording behind the `lens-turn-summary` opt-in
   * flag. */
  get turnSummary() {
    return this._turnSummary;
  }
  get complexityBaselines() {
    return this._complexityBaselines;
  }
  get fixedThisTurn() {
    const facade = {
      add: (filePath) => {
        this._fixedThisTurn.set(filePath, true);
        return facade;
      },
      has: (filePath) => this._fixedThisTurn.has(filePath),
      delete: (filePath) => this._fixedThisTurn.delete(filePath),
      clear: () => this._fixedThisTurn.clear()
    };
    return facade;
  }
  get projectRulesScan() {
    return this._projectRulesScan;
  }
  set projectRulesScan(value) {
    this._projectRulesScan = value;
  }
  get readGuard() {
    this._readGuard ??= new ReadGuard(this._telemetrySessionId, {}, this._scope);
    return this._readGuard;
  }
  /**
   * Record the canonical target `tool_call` resolved for `toolCallId`
   * (#1642). The paired `tool_result` claims this exactly once via
   * {@link takeToolCallAttribution} instead of re-deriving a path from its
   * own diff metadata.
   */
  recordToolCallAttribution(toolCallId, attribution) {
    this._toolCallAttributions.set(toolCallId, {
      ...attribution,
      recordedAt: Date.now()
    });
  }
  /** #3555: note the widening the tool_call applied to this read. */
  recordReadWidening(toolCallId, widening) {
    this._readWidenings.set(toolCallId, widening);
  }
  /** #3555: one-shot claim of a read's widening, as with an attribution. */
  takeReadWidening(toolCallId) {
    const widening = this._readWidenings.get(toolCallId);
    this._readWidenings.delete(toolCallId);
    return widening;
  }
  /** #3523: see {@link ToolCallAttribution.editInPlace}. */
  markToolCallEditInPlace(toolCallId) {
    const attribution = this._toolCallAttributions.get(toolCallId);
    if (attribution)
      attribution.editInPlace = true;
  }
  /** #3525: see {@link ToolCallAttribution.fileTimeStale}. */
  markToolCallFileTimeStale(toolCallId) {
    const attribution = this._toolCallAttributions.get(toolCallId);
    if (attribution)
      attribution.fileTimeStale = true;
  }
  /**
   * One-shot claim of a previously recorded tool-call attribution. Removed
   * on read: a given `tool_call`/`tool_result` pair correlates exactly once,
   * and leaving it behind would let a later, unrelated `tool_result` that
   * happens to reuse a stale id inherit a stranger's resolved path.
   *
   * An expired entry (older than `TOOL_CALL_ATTRIBUTION_TTL_MS`) is treated
   * as a miss and removed rather than returned — see the constant's doc.
   */
  takeToolCallAttribution(toolCallId) {
    const attribution = this._toolCallAttributions.get(toolCallId);
    if (attribution === void 0)
      return void 0;
    this._toolCallAttributions.delete(toolCallId);
    if (Date.now() - attribution.recordedAt > TOOL_CALL_ATTRIBUTION_TTL_MS) {
      return void 0;
    }
    return attribution;
  }
  /**
   * Queue one mutation kind for `filePath` at `agent_end`. Returns `true`
   * when this call created a pending entry or added a new kind, and `false`
   * for a same-kind re-touch. Callers publish each kind's first transition
   * without spamming repeated edits before `agent_end`.
   */
  deferMutation(filePath, cwd, toolName, turnStateCwd, kind, ownerSessionId, originCwd, readGuardBranchEpoch = this.readGuard.currentBranchEpoch) {
    const key = path8.resolve(filePath);
    const now = Date.now();
    const resolvedOriginCwd = originCwd ?? turnStateCwd;
    const existing = this._pendingDeferredMutations.get(key);
    if (existing) {
      existing.readGuardBranchEpoch = Math.max(existing.readGuardBranchEpoch, readGuardBranchEpoch);
      const addedKind = !existing.kinds.has(kind);
      existing.lastTouchedAt = now;
      existing.cwd = cwd;
      existing.turnStateCwd = turnStateCwd;
      existing.toolNames.add(toolName);
      existing.kinds.add(kind);
      existing.queuedTurnIndex = this._turnIndex;
      existing.queuedTurnId = `${this._telemetrySessionId}:${this._turnIndex}`;
      existing.ownerSessionId = ownerSessionId;
      existing.originCwd = resolvedOriginCwd;
      return addedKind;
    }
    this._pendingDeferredMutations.set(key, {
      filePath: key,
      cwd,
      turnStateCwd,
      firstTouchedAt: now,
      lastTouchedAt: now,
      toolNames: /* @__PURE__ */ new Set([toolName]),
      kinds: /* @__PURE__ */ new Set([kind]),
      queuedTurnIndex: this._turnIndex,
      queuedTurnId: `${this._telemetrySessionId}:${this._turnIndex}`,
      ownerSessionId,
      originCwd: resolvedOriginCwd,
      readGuardBranchEpoch
    });
    return true;
  }
  deferFormat(filePath, cwd, toolName, turnStateCwd, ownerSessionId, originCwd) {
    return this.deferMutation(filePath, cwd, toolName, turnStateCwd, "format", ownerSessionId, originCwd);
  }
  get pendingDeferredFormatCount() {
    return this._pendingDeferredMutations.size;
  }
  get pendingDeferredMutationCount() {
    return this._pendingDeferredMutations.size;
  }
  /**
   * Legacy unconditional drain — still exposed for any caller that
   * genuinely wants "everything, no ownership check" (and for tests). New
   * flush call sites should prefer {@link claimDeferredFormatFiles}.
   */
  consumeDeferredFormatFiles() {
    const records = [...this._pendingDeferredMutations.values()];
    this._pendingDeferredMutations.clear();
    return records;
  }
  /**
   * Ownership-filtered drain (#791). Claims and removes only the records
   * this flush is entitled to:
   *  - `ownerSessionId` unset on the record, OR `currentSessionId` unset, OR
   *    they match → claimed as "same session" (the common case, and the
   *    fail-safe default when either side lacks a stable session id).
   *  - otherwise (both known, and they differ) the record belongs to a
   *    DIFFERENT session (e.g. a concurrent in-process secondary/subagent)
   *    and is left queued for its owner's own flush — UNLESS it has sat
   *    unclaimed longer than `staleAfterMs` (the owner presumably died), in
   *    which case this flush claims it as an orphan-recovery fallback ONLY
   *    when `currentOriginCwd` (this flush's own workspace/worktree) exactly
   *    matches the record's `originCwd` (the cwd it was actually queued
   *    under, #1642 F3 — NOT `turnStateCwd`, which is always the workspace
   *    root by design and so would match every session unconditionally). A
   *    mismatch means the record belongs to a DIFFERENT checkout/worktree
   *    than the one now running `agent_end` — claiming it across that
   *    boundary is exactly how a worktree's queued edit got formatted onto
   *    the parent checkout in the reported incident. It is left queued
   *    (NOT deleted) and logged instead: a legitimate crashed-session
   *    orphan from, say, a monorepo subdir must still be claimable later by
   *    a flush whose origin actually matches, or stay visible for
   *    observability — deleting it here would drop it forever with no
   *    origin ever able to reclaim it.
   *
   * Returns the claimed records, the left-queued mismatched-origin records,
   * and per-skipped record why it was left behind — callers use this for
   * `agent_end`'s latency-log provenance and for the "stale fallback
   * fired"/"orphan mismatch" log lines.
   */
  claimDeferredFormatFiles(currentSessionId, now, staleAfterMs, currentOriginCwd) {
    const claimed = [];
    const staleClaimed = [];
    const deferredToOwner = [];
    const droppedOrphans = [];
    for (const [key, record] of this._pendingDeferredMutations) {
      const sameSession = record.ownerSessionId === void 0 || currentSessionId === void 0 || record.ownerSessionId === currentSessionId;
      if (sameSession) {
        claimed.push(record);
        this._pendingDeferredMutations.delete(key);
        continue;
      }
      const age = now - record.lastTouchedAt;
      if (age > staleAfterMs) {
        const originMatches = currentOriginCwd === void 0 || pathsEqual(record.originCwd, currentOriginCwd);
        if (originMatches) {
          this._pendingDeferredMutations.delete(key);
          staleClaimed.push(record);
        } else {
          droppedOrphans.push(record);
        }
        continue;
      }
      deferredToOwner.push(record);
    }
    return { claimed, staleClaimed, deferredToOwner, droppedOrphans };
  }
  /** Return claimed records that were never started by an aborted drain. */
  requeueDeferredFormatFiles(records) {
    for (const record of records) {
      const key = path8.resolve(record.filePath);
      const existing = this._pendingDeferredMutations.get(key);
      if (existing) {
        for (const kind of record.kinds)
          existing.kinds.add(kind);
        for (const toolName of record.toolNames)
          existing.toolNames.add(toolName);
        existing.readGuardBranchEpoch = Math.max(existing.readGuardBranchEpoch, record.readGuardBranchEpoch);
        continue;
      }
      this._pendingDeferredMutations.set(key, {
        ...record,
        kinds: new Set(record.kinds),
        toolNames: new Set(record.toolNames)
      });
    }
  }
  claimDeferredMutations(currentSessionId, now, staleAfterMs, currentOriginCwd) {
    return this.claimDeferredFormatFiles(currentSessionId, now, staleAfterMs, currentOriginCwd);
  }
  requeueDeferredMutations(records) {
    this.requeueDeferredFormatFiles(records);
  }
  shouldWarmLspOnRead(filePath, maxAgeMs = 12e4) {
    const state = this._lspReadWarmState.get(path8.resolve(filePath));
    if (!state)
      return true;
    if (state.status === "warming")
      return false;
    return Date.now() - state.ts > maxAgeMs;
  }
  markLspReadWarmStarted(filePath) {
    this._lspReadWarmState.set(path8.resolve(filePath), {
      status: "warming",
      ts: Date.now()
    });
  }
  markLspReadWarmCompleted(filePath) {
    this._lspReadWarmState.set(path8.resolve(filePath), {
      status: "ready",
      ts: Date.now()
    });
  }
  clearLspReadWarmState(filePath) {
    this._lspReadWarmState.delete(path8.resolve(filePath));
  }
};

// dist/clients/runtime-session.js
import * as nodeFs from "node:fs";
import * as path12 from "node:path";

// dist/clients/message-end-attribution.js
var lastStableSessionId;
var previousSessionId;
function rotateMessageEndAttribution() {
  if (lastStableSessionId !== void 0) {
    previousSessionId = lastStableSessionId;
  }
  lastStableSessionId = void 0;
}

// dist/clients/dead-code-logger.js
import * as path9 from "node:path";
var LOG_DIR = getGlobalPiLensLogDir();
var LOG_FILE = path9.join(LOG_DIR, "dead-code.log");
var LOG_BACKUP_FILE = path9.join(LOG_DIR, "dead-code.log.1");
var MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_DEAD_CODE_LOG_MAX_BYTES ?? "1048576", 10) || 1048576);
var writer = createNdjsonLogger({
  filePath: LOG_FILE,
  maxBytes: MAX_LOG_BYTES,
  backupPath: LOG_BACKUP_FILE
});
function logDeadCodeScan(event) {
  if (isTestMode())
    return;
  writer.log({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...event });
}

// dist/clients/print-mode.js
function isPrintMode(argv = process.argv) {
  return argv.includes("--print") || argv.includes("-p");
}

// dist/clients/rules-scanner.js
import * as fs10 from "node:fs";
import * as path10 from "node:path";
var RULE_DIRS = [
  { dir: ".claude/rules", source: ".claude/rules" },
  { dir: ".agents/rules", source: ".agents/rules" }
];
var RULE_FILES = [
  { file: "CLAUDE.md", source: "root" },
  { file: "AGENTS.md", source: "root" },
  { file: ".cursorrules", source: "root" }
];
var RULES_SCAN_MAX_DEPTH = 8;
var RULES_SCAN_MAX_FILES = 500;
function findMarkdownFiles(dir, baseDir, depth = 0, collected = { count: 0 }) {
  const results = [];
  if (depth > RULES_SCAN_MAX_DEPTH)
    return results;
  if (!fs10.existsSync(dir))
    return results;
  const entries = fs10.readdirSync(dir, { withFileTypes: true });
  for (const entry of entries) {
    if (collected.count >= RULES_SCAN_MAX_FILES)
      break;
    const fullPath = path10.join(dir, entry.name);
    if (entry.isDirectory()) {
      results.push(...findMarkdownFiles(fullPath, baseDir, depth + 1, collected));
    } else if (entry.isFile() && entry.name.endsWith(".md")) {
      collected.count += 1;
      results.push({
        source: path10.relative(baseDir, dir) || path10.basename(baseDir),
        name: entry.name,
        filePath: fullPath,
        relativePath: path10.relative(baseDir, fullPath).replace(/\\/g, "/")
      });
    }
  }
  return results;
}
function scanProjectRules(cwd) {
  const rules = [];
  for (const { dir, source } of RULE_DIRS) {
    const dirPath = path10.join(cwd, dir);
    if (fs10.existsSync(dirPath)) {
      const found = findMarkdownFiles(dirPath, path10.join(cwd, dir));
      for (const rule of found) {
        rules.push({
          source,
          name: rule.name,
          filePath: rule.filePath,
          relativePath: `${dir}/${rule.relativePath}`
        });
      }
    }
  }
  for (const { file, source } of RULE_FILES) {
    const filePath = path10.join(cwd, file);
    if (fs10.existsSync(filePath)) {
      rules.push({
        source,
        name: file,
        filePath,
        relativePath: file
      });
    }
  }
  return {
    rules,
    hasCustomRules: rules.length > 0
  };
}

// dist/clients/smells-rollup.js
import * as fs11 from "node:fs";
import * as path11 from "node:path";
var SMELLS_TAIL_BYTES_PER_FILE = 64 * 1024;
var SMELLS_ROLLING_WINDOW_MS = 24 * 60 * 6e4;
var SMELLS_THRESHOLDS = {
  staleCtxEmitFailed: 5,
  opengrepRespawn: 5
};
var SMELL_LABELS = {
  staleCtxEmitFailed: "stale-ctx emit_failed (bus-events.log)",
  opengrepRespawn: "opengrep respawn (latency.log)"
};
var SMELL_KEYS = ["staleCtxEmitFailed", "opengrepRespawn"];
function tailReadText(filePath, maxBytes) {
  let fd;
  try {
    fd = fs11.openSync(filePath, "r");
  } catch {
    return "";
  }
  try {
    const size = fs11.fstatSync(fd).size;
    const readBytes2 = Math.min(size, maxBytes);
    if (readBytes2 <= 0)
      return "";
    const start = size - readBytes2;
    const buf = Buffer.alloc(readBytes2);
    fs11.readSync(fd, buf, 0, readBytes2, start);
    const text = buf.toString("utf8");
    if (start <= 0)
      return text;
    const firstNewline = text.indexOf("\n");
    return firstNewline >= 0 ? text.slice(firstNewline + 1) : "";
  } catch {
    return "";
  } finally {
    try {
      fs11.closeSync(fd);
    } catch {
    }
  }
}
function countMatchingLines(text, predicate, sessionStartMs) {
  let count = 0;
  for (const line of text.split("\n")) {
    if (!line.trim())
      continue;
    let entry;
    try {
      entry = JSON.parse(line);
    } catch {
      continue;
    }
    if (entry && typeof entry === "object" && (sessionStartMs === void 0 || isAtOrAfterSessionStart(entry, sessionStartMs)) && predicate(entry)) {
      count++;
    }
  }
  return count;
}
function isAtOrAfterSessionStart(entry, sessionStartMs) {
  const timestamp = typeof entry.ts === "string" ? Date.parse(entry.ts) : NaN;
  return Number.isFinite(timestamp) && timestamp >= sessionStartMs;
}
function isStaleCtxEmitFailed(entry) {
  return entry.outcome === "emit_failed" && typeof entry.error === "string" && entry.error.includes("stale after session replacement");
}
function isOpengrepRespawn(entry) {
  if (entry.phase !== "lsp_server_respawn")
    return false;
  const metadata = entry.metadata;
  return metadata?.serverId === "opengrep";
}
function countRecentSmells(root = getGlobalPiLensLogDir(), sessionStartMs) {
  const sinceMs = sessionStartMs ?? Date.now() - SMELLS_ROLLING_WINDOW_MS;
  const busTail = tailReadText(path11.join(root, "bus-events.log"), SMELLS_TAIL_BYTES_PER_FILE);
  const latencyTail = tailReadText(path11.join(root, "latency.log"), SMELLS_TAIL_BYTES_PER_FILE);
  return {
    staleCtxEmitFailed: countMatchingLines(busTail, isStaleCtxEmitFailed, sinceMs),
    opengrepRespawn: countMatchingLines(latencyTail, isOpengrepRespawn, sinceMs)
  };
}
function formatSmellsSessionStartLine(counts) {
  const bits = [];
  for (const key of SMELL_KEYS) {
    if (counts[key] >= SMELLS_THRESHOLDS[key]) {
      bits.push(`${SMELL_LABELS[key]} x${counts[key]}`);
    }
  }
  if (bits.length === 0)
    return null;
  const tailKb = Math.round(SMELLS_TAIL_BYTES_PER_FILE / 1024);
  return `session_start smells (last ${tailKb}KB tail): ${bits.join(", ")} \u2014 run \`npm run logs:smells\` for detail`;
}
var notifiedThisSession = /* @__PURE__ */ new Set();
function resetSmellsSessionState() {
  notifiedThisSession.clear();
}

// dist/clients/situational-tool-telemetry.js
var situationalTools = TOOL_REGISTRY.filter((tool) => "situational" in tool && tool.situational === true).map((tool) => tool.name);
var situationalToolSet = new Set(situationalTools);
var activated = /* @__PURE__ */ new Set();
var called = /* @__PURE__ */ new Set();
var sessionStarted = false;
var emitted = false;
var connectionEnded = false;
var sessionHost = "mcp";
function observe(set, name) {
  if (situationalToolSet.has(name)) {
    set.add(name);
  }
}
function clearObservations() {
  activated.clear();
  called.clear();
}
function observeSituationalToolCall(name) {
  observe(called, name);
}
function resetSituationalToolTelemetry() {
  if (sessionStarted)
    return;
  clearObservations();
  emitted = false;
}
function startSituationalToolTelemetrySession(host) {
  if (host === "mcp") {
    if (connectionEnded)
      return;
    if (sessionStarted)
      return;
    sessionHost = "mcp";
    clearObservations();
    emitted = false;
    sessionStarted = true;
    return;
  }
  if (sessionStarted) {
    sessionHost = "pi";
    return;
  }
  sessionHost = "pi";
  clearObservations();
  emitted = false;
  sessionStarted = true;
}
function endSituationalToolTelemetry() {
  if (!sessionStarted)
    return;
  emitSituationalDeadWeight();
  if (sessionHost === "mcp")
    connectionEnded = true;
  clearObservations();
  emitted = false;
  sessionStarted = false;
}
function emitSituationalDeadWeight() {
  if (emitted)
    return;
  emitted = true;
  const used = /* @__PURE__ */ new Set([...activated, ...called]);
  logExtension({
    subsystem: "tools",
    level: "debug",
    message: "situational tool dead weight",
    metadata: {
      tools: situationalTools.filter((name) => !used.has(name))
    }
  });
}

// dist/clients/test-runner-delivery.js
var pending = /* @__PURE__ */ new Map();
function resetTestRunnerDelivery() {
  pending.clear();
}

// dist/clients/runtime-session.js
async function demandBootstrapDeps(deps, reason) {
  const clients = await deps.bootstrap.request(reason);
  if (!clients)
    return null;
  return { ...deps, ...clients };
}
var HOST_STALL_THRESHOLD_MS = 3e4;
function logHostReadyDelay(deps, cwd) {
  if (!deps.emitHostReadyDelay || deps.sessionStartMonotonicAt === void 0 || deps.extensionLoadedAt === void 0) {
    return;
  }
  const durationMs = Math.max(0, deps.sessionStartMonotonicAt - deps.extensionLoadedAt);
  logLatency({
    type: "phase",
    filePath: cwd,
    phase: "host_ready_delay",
    durationMs,
    metadata: {
      hostStallSuspected: durationMs > HOST_STALL_THRESHOLD_MS,
      sessionStart: "first-process-session",
      reason: deps.sessionReason
    }
  });
}
function resolveSnapshotRoot(cwd) {
  const resolvedCwd = path12.resolve(cwd);
  const nearest = findNearestProjectRoot(resolvedCwd);
  if (!nearest || isAtOrAboveHomeDir(nearest)) {
    return resolvedCwd;
  }
  return nearest;
}
function snapshotSequenceBase(root) {
  const meta = readProjectSnapshotMeta(root);
  if (!meta || meta.version !== PROJECT_SNAPSHOT_VERSION)
    return void 0;
  const index = meta.sequenceIndex;
  if (!index)
    return void 0;
  return {
    projectSeq: index.projectSeq,
    fileSeqByPath: index.fileSeqByPath,
    sinceSeq: meta.seq,
    logEntries: meta.logEntries ?? 0
  };
}
function recordSnapshotSequenceTimeout(args) {
  let reason = "sequence read timed out; no snapshot on disk";
  try {
    const stat = nodeFs.statSync(args.snapshotPath);
    reason = `sequence read timed out; snapshot age=${Math.round(Date.now() - stat.mtimeMs)}ms size=${stat.size}b`;
  } catch {
  }
  incrementDegradationCount({
    kind: "snapshot-sequence-read-timeout",
    subject: args.snapshotRoot,
    reason
  });
}
function retroactivelyHydrateAfterDeferredSequence(args) {
  return (latestSeq) => {
    const published = args.getWarmupOwnSnapshotRead();
    const snapshot = published !== void 0 ? published : loadProjectSnapshotExportsAndRules(args.snapshotRoot);
    if (!snapshot || snapshot.version !== PROJECT_SNAPSHOT_VERSION || snapshot.incomplete || (snapshot.logEntries ?? 0) < (latestSeq.unlockedThrough ?? 0) || snapshot.seq !== latestSeq.projectSeq) {
      return;
    }
    const hydrated = hydrateRuntimeFromProjectSnapshotIfIdle(args.runtime, snapshot);
    if (hydrated) {
      args.dbg(`session_start: deferred sequence read confirmed snapshot freshness \u2014 hydrating cachedExports/projectRulesScan late (seq=${snapshot.seq})`);
    } else {
      args.dbg(`session_start: deferred sequence read confirmed snapshot freshness (seq=${snapshot.seq}), but the runtime already has live state (warmup or another task got there first) \u2014 skipping late hydration to avoid clobbering it`);
    }
  };
}
function loadSnapshotBodyUnlessStale(args) {
  const meta = readProjectSnapshotMeta(args.root);
  if (meta && isProjectSnapshotMetaStale(meta, args.currentProjectSeq, args.unlockedThrough)) {
    args.dbg(`project_snapshot: meta gate stale (metaSeq=${meta.seq} metaVersion=${meta.version} current=${args.currentProjectSeq}) \u2014 skipping body parse`);
    return { snapshot: null, skippedStale: true };
  }
  return {
    snapshot: loadProjectSnapshot(args.root),
    skippedStale: false
  };
}
function describeSnapshotMiss(snapshot, currentProjectSeq, args) {
  if (args.skippedStale)
    return "stale-meta-gate";
  if (!snapshot)
    return args.bodyPresent ? "invalid-body" : "missing";
  if (snapshot.seq !== currentProjectSeq) {
    return `stale(seq=${snapshot.seq}, current=${currentProjectSeq})`;
  }
  if ((snapshot.logEntries ?? 0) < args.unlockedThrough) {
    return `unlocked-entry-after-fold(folded=${snapshot.logEntries ?? 0}, unlocked=${args.unlockedThrough})`;
  }
  return "incompatible";
}
function logProjectSnapshotProbe(args) {
  args.dbg(`project_snapshot: probe root=${args.root} path=${getProjectSnapshotPath(args.root)} currentSeq=${args.currentProjectSeq}`);
  if (isProjectSnapshotFresh(args.snapshot, args.currentProjectSeq, args.unlockedThrough)) {
    args.dbg(`project_snapshot: loaded seq=${args.snapshot.seq} exports=${args.snapshot.cachedExports.length} files=${Object.keys(args.snapshot.files ?? {}).length} reverseDeps=${Object.keys(args.snapshot.reverseDeps ?? {}).length} startupScan=${Boolean(args.snapshot.startupScan)} languageProfile=${Boolean(args.snapshot.languageProfile)}`);
  } else {
    args.dbg(`project_snapshot: miss reason=${args.missReason}`);
  }
}
function resolveStartupMode(projectConfig, globalConfig) {
  const envMode = (process.env.PI_LENS_STARTUP_MODE ?? "").trim().toLowerCase();
  if (envMode === "full" || envMode === "minimal" || envMode === "quick") {
    return envMode;
  }
  const configMode = projectConfig?.startup?.mode ?? globalConfig?.startup?.mode;
  if (configMode)
    return configMode;
  if (isPrintMode()) {
    return "quick";
  }
  return "full";
}
function sequenceReadBudgetMs() {
  const raw = Number(process.env.PI_LENS_SEQUENCE_READ_BUDGET_MS ?? 250);
  return Number.isFinite(raw) && raw > 0 ? raw : 250;
}
var UNKNOWN_PROJECT_SEQ = -1;
async function readSequenceWithBudget(args) {
  const { snapshotRoot, base, cwd, runtime, sessionGeneration, dbg, onDeferredSequenceResolved } = args;
  const readPromise = readLatestProjectSequenceAsync(snapshotRoot, base);
  let timeoutHandle;
  const timeoutPromise = new Promise((resolve30) => {
    timeoutHandle = setTimeout(() => resolve30({ timedOut: true }), sequenceReadBudgetMs());
    timeoutHandle.unref();
  });
  const raced = await Promise.race([
    readPromise.then((latestSeq) => ({ timedOut: false, latestSeq })),
    timeoutPromise
  ]);
  if (timeoutHandle)
    clearTimeout(timeoutHandle);
  if (!raced.timedOut) {
    return { latestSeq: raced.latestSeq, timedOut: false };
  }
  dbg(`session_start: sequence read exceeded ${sequenceReadBudgetMs()}ms budget \u2014 falling back to cold-start sequence`);
  if (!isPrintMode()) {
    readPromise.then((latestSeq) => {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      if (runtime.projectSeq > 0) {
        const mergeStartedAt = Date.now();
        runtime.mergeProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath, latestSeq.logEntries);
        logLatency({
          type: "phase",
          phase: "session_start_sequence_read_deferred_reseed",
          filePath: cwd,
          startedAt: new Date(mergeStartedAt).toISOString(),
          durationMs: Date.now() - mergeStartedAt,
          metadata: {
            entries: latestSeq.fileSeqByPath.size,
            deferred: true,
            merged: true
          }
        });
        dbg(`session_start: deferred sequence read completed after an in-window edit \u2014 merged projectSeq=${latestSeq.projectSeq} into the advanced session (now ${runtime.projectSeq})`);
        return;
      }
      const reseedStartedAt = Date.now();
      runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath, latestSeq.logEntries);
      logLatency({
        type: "phase",
        phase: "session_start_sequence_read_deferred_reseed",
        filePath: cwd,
        startedAt: new Date(reseedStartedAt).toISOString(),
        durationMs: Date.now() - reseedStartedAt,
        metadata: { entries: latestSeq.fileSeqByPath.size, deferred: true }
      });
      dbg(`session_start: deferred sequence read completed \u2014 reseeded projectSeq=${latestSeq.projectSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
      onDeferredSequenceResolved?.(latestSeq);
    }).catch((err) => {
      dbg(`session_start: deferred sequence read failed: ${err}`);
    });
  }
  return {
    latestSeq: { projectSeq: 0, fileSeqByPath: /* @__PURE__ */ new Map() },
    timedOut: true
  };
}
async function igniteWarmFiles(cwd, warmFiles, runtime, sessionGeneration, dbg) {
  try {
    dbg(`session_start lsp-warm: ${warmFiles.length} warm file(s) configured`);
    await initLSPConfig(cwd);
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    const lspService = (await loadLspService()).getLSPService();
    const total = warmFiles.length;
    let loaded = 0;
    let errors = 0;
    for (const relPath of warmFiles) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      const filePath = path12.isAbsolute(relPath) ? relPath : path12.resolve(cwd, relPath);
      if (!nodeFs.existsSync(filePath)) {
        dbg(`session_start lsp-warm: not found: ${relPath}`);
        errors++;
        continue;
      }
      try {
        const content = nodeFs.readFileSync(filePath, "utf-8");
        await lspService.touchFile(filePath, content, {
          diagnostics: "none",
          source: "startup-warm",
          clientScope: "primary",
          maxClientWaitMs: 2e3
        });
        loaded++;
      } catch (err) {
        dbg(`session_start lsp-warm: error ${relPath}: ${err}`);
        errors++;
      }
    }
    dbg(`session_start lsp-warm: ${loaded}/${total} opened (${errors} err)`);
  } catch (err) {
    dbg(`session_start lsp-warm: config/init error: ${err}`);
  }
}
async function igniteDominantLanguageWarm(cwd, runtime, sessionGeneration, dbg) {
  try {
    await initLSPConfig(cwd);
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    const lspService = (await loadLspService()).getLSPService();
    const { collectSourceFilesAsync } = await import("../chunk-E4MAEDPM.js");
    const { CODE_KINDS, detectFileKind: detectFileKind2 } = await import("../chunk-O5WILGMI.js");
    const files = await collectSourceFilesAsync(cwd, {
      inspectGeneratedHeaders: false
    });
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    const counts = /* @__PURE__ */ new Map();
    for (const f of files) {
      const kind = detectFileKind2(f);
      if (kind)
        counts.set(kind, (counts.get(kind) ?? 0) + 1);
    }
    if (counts.size === 0) {
      dbg("session_start lsp-warm: no detected languages to auto-warm");
      return;
    }
    const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
    const warmOrder = [
      ...ranked.filter(([kind]) => CODE_KINDS.has(kind)),
      ...ranked.filter(([kind]) => !CODE_KINDS.has(kind))
    ];
    for (const [kind] of warmOrder) {
      const sample = files.find((f) => detectFileKind2(f) === kind && lspService.supportsLSP(f));
      if (!sample)
        continue;
      const content = await nodeFs.promises.readFile(sample, "utf-8");
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      await lspService.touchFile(sample, content, {
        diagnostics: "none",
        source: "startup-warm-dominant",
        clientScope: "primary",
        maxClientWaitMs: 2e3
      });
      dbg(`session_start lsp-warm: dominant=${kind} via ${path12.basename(sample)}`);
      return;
    }
    dbg("session_start lsp-warm: no dominant-language LSP file found to warm");
  } catch (err) {
    dbg(`session_start lsp-warm: dominant warm error: ${err}`);
  }
}
function firePreinstallDefaults(ensureTool, dbg, startupDefaults) {
  for (const tool of startupDefaults) {
    const startedAt = Date.now();
    dbg(`session_start preinstall ${tool}: start`);
    ensureTool(tool).then((toolPath) => {
      if (toolPath) {
        dbg(`session_start: ${tool} ready at ${toolPath}`);
        dbg(`session_start preinstall ${tool}: success (${Date.now() - startedAt}ms)`);
      } else {
        dbg(`session_start: ${tool} installation unavailable`);
        dbg(`session_start preinstall ${tool}: unavailable (${Date.now() - startedAt}ms)`);
      }
    }).catch((err) => {
      dbg(`session_start: ${tool} pre-install error: ${err}`);
      dbg(`session_start preinstall ${tool}: error (${Date.now() - startedAt}ms)`);
    });
  }
}
var MANAGED_TOOL_REFRESH_DELAY_MS = 3e4;
function scheduleManagedToolRefresh(dbg) {
  const delayMs = Number(process.env.PI_LENS_TOOL_REFRESH_DELAY_MS ?? MANAGED_TOOL_REFRESH_DELAY_MS);
  const timer = setTimeout(() => {
    void (async () => {
      try {
        const refresh = await import("../chunk-T4ERT73Y.js");
        const outcome = await refresh.runManagedToolRefresh();
        if (outcome.skipped) {
          dbg(`session_start tool-refresh: skipped (${outcome.skipped})`);
          return;
        }
        for (const result of outcome.refreshed) {
          dbg(`session_start tool-refresh: ${result.toolId} ${result.ok ? "ok" : "failed"}${result.changed ? ` ${result.previousVersion ?? "unknown"} \u2192 ${result.currentVersion}` : " (unchanged)"}`);
        }
      } catch (err) {
        dbg(`session_start tool-refresh: error ${err}`);
      }
    })();
  }, Number.isFinite(delayMs) ? delayMs : MANAGED_TOOL_REFRESH_DELAY_MS);
  timer.unref?.();
}
async function probePrettierInstall(ensureTool, dbg, analysisRoot) {
  const pkgPath = path12.join(analysisRoot, "package.json");
  try {
    const raw = await nodeFs.promises.readFile(pkgPath, "utf-8");
    const pkg = JSON.parse(raw);
    const usesPrettier = !!pkg.devDependencies?.prettier || !!pkg.dependencies?.prettier || pkg.prettier !== void 0;
    if (usesPrettier) {
      dbg("session_start: project uses prettier, ensuring install...");
      ensureTool("prettier").then((p) => {
        if (p)
          dbg(`session_start: prettier ready at ${p}`);
        else
          dbg("session_start: prettier install failed silently");
      }).catch((err) => dbg(`session_start: prettier install error: ${err}`));
    }
  } catch {
  }
}
function scanOneTodoFile(scanner, filePath, items) {
  if (typeof scanner.scanFile !== "function")
    return;
  try {
    const result = scanner.scanFile(filePath);
    if (Array.isArray(result))
      items.push(...result);
  } catch {
  }
}
async function collectTodoBaselineItems(scanner, analysisRoot, stillCurrent) {
  const items = [];
  try {
    const { getSourceFilesAsync } = await import("../chunk-RPD4ZSB7.js");
    const files = await getSourceFilesAsync(analysisRoot, true);
    if (!stillCurrent())
      return items;
    const deadline = createDeadline(8);
    for (const file of files) {
      if (!stillCurrent())
        return items;
      scanOneTodoFile(scanner, file, items);
      await yieldIfOverBudget(deadline);
    }
  } catch {
    const todoResult = scanner.scanDirectory(analysisRoot);
    items.push(...todoResult.items);
  }
  return items;
}
async function buildOrRefreshWordIndex(args) {
  const { runtime, sessionGeneration, analysisRoot, snapshotRoot, dbg } = args;
  if (!runtime.isCurrentSession(sessionGeneration))
    return;
  const startMs = Date.now();
  let rebuildPreflightFiles;
  const latestSeq = readLatestProjectSequence(snapshotRoot, snapshotSequenceBase(snapshotRoot));
  const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
  const snapshotLoadStartMs = Date.now();
  const snapshot = loadProjectSnapshot(snapshotRoot);
  const snapshotLoadMs = Date.now() - snapshotLoadStartMs;
  if (snapshot?.wordIndex) {
    const { deserializeWordIndex, refreshWordIndexIncrementally, countWordIndexPostingEntries: countWordIndexPostingEntries2, estimateWordIndexResidentBytes: estimateWordIndexResidentBytes2 } = await import("../chunk-XECLWQZN.js");
    const deserializeStartMs = Date.now();
    const index = deserializeWordIndex(snapshot.wordIndex);
    const deserializeMs = Date.now() - deserializeStartMs;
    if (index) {
      try {
        const result = await refreshWordIndexIncrementally(index, analysisRoot, () => runtime.isCurrentSession(sessionGeneration));
        if (!runtime.isCurrentSession(sessionGeneration))
          return;
        if (result.mode === "full-required") {
          rebuildPreflightFiles = result.preflightFiles;
          dbg(`session_start word-index: incremental preflight selected full rebuild (${result.reason})`);
          logWordIndex({
            phase: "incremental_fallback",
            cwd: snapshotRoot,
            trigger: "session_start",
            reason: result.reason,
            phaseDurationsMs: {
              snapshotLoadMs,
              deserializeMs,
              ...result.timings
            }
          });
        } else {
          runtime.wordIndex = index;
          let snapshotSaveSyncMs = 0;
          if (!isProjectSnapshotFresh(snapshot, effectiveSeq, latestSeq.unlockedThrough) || result.refreshed > 0 || result.dropped > 0 || snapshot.wordIndex.truncated !== index.truncated) {
            const serializeSaveEnqueueStartMs = Date.now();
            saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
            snapshotSaveSyncMs = Date.now() - serializeSaveEnqueueStartMs;
          }
          const phases = `snapshot-load=${snapshotLoadMs}ms, deserialize=${deserializeMs}ms, source-walk=${result.timings.sourceWalkMs}ms, stat-walk=${result.timings.statWalkMs}ms, refresh-reads=${result.timings.refreshReadsMs}ms, snapshot-save-sync=${snapshotSaveSyncMs}ms`;
          dbg(`session_start word-index: incremental (seq=${effectiveSeq}, refreshed=${result.refreshed}, dropped=${result.dropped}, skipped=${result.skipped}, reused=${result.reused}, ${Date.now() - startMs}ms, phases: ${phases})`);
          logWordIndex({
            phase: "incremental_refresh",
            cwd: snapshotRoot,
            trigger: "session_start",
            durationMs: Date.now() - startMs,
            indexedFileCount: index.docCount,
            tokens: index.postings.size,
            postingEntries: countWordIndexPostingEntries2(index),
            residentBytes: estimateWordIndexResidentBytes2(index),
            truncated: index.truncated,
            phaseDurationsMs: {
              snapshotLoadMs,
              deserializeMs,
              ...result.timings,
              snapshotSaveSyncMs
            },
            refreshed: result.refreshed,
            dropped: result.dropped,
            skipped: result.skipped,
            reused: result.reused
          });
          return result;
        }
      } catch (err) {
        if (!runtime.isCurrentSession(sessionGeneration)) {
          dbg("session_start word-index: incremental refresh superseded");
          return;
        }
        dbg(`session_start word-index: incremental refresh failed; falling back to full rebuild (${err})`);
        logWordIndex({
          phase: "incremental_fallback",
          cwd: snapshotRoot,
          trigger: "session_start",
          reason: err instanceof Error ? err.message : String(err),
          phaseDurationsMs: { snapshotLoadMs, deserializeMs }
        });
      }
    }
  }
  const { buildWordIndexAsync, collectWordIndexDocs, countWordIndexPostingEntries, estimateWordIndexResidentBytes } = await import("../chunk-XECLWQZN.js");
  const docs = await collectWordIndexDocs(analysisRoot, () => runtime.isCurrentSession(sessionGeneration), rebuildPreflightFiles);
  if (!runtime.isCurrentSession(sessionGeneration))
    return;
  let rebuiltIndex;
  try {
    rebuiltIndex = await buildWordIndexAsync(docs, () => runtime.isCurrentSession(sessionGeneration));
  } catch (err) {
    if (!runtime.isCurrentSession(sessionGeneration)) {
      dbg("session_start word-index: rebuild superseded");
      return;
    }
    throw err;
  }
  if (!runtime.isCurrentSession(sessionGeneration))
    return;
  runtime.wordIndex = rebuiltIndex;
  saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
  dbg(`session_start word-index: rebuilt (absent/stale, seq=${effectiveSeq}) ${runtime.wordIndex.docCount} files, ${runtime.wordIndex.postings.size} tokens (${Date.now() - startMs}ms)`);
  logWordIndex({
    phase: "full_rebuild",
    cwd: snapshotRoot,
    trigger: "session_start",
    durationMs: Date.now() - startMs,
    indexedFileCount: runtime.wordIndex.docCount,
    tokens: runtime.wordIndex.postings.size,
    postingEntries: countWordIndexPostingEntries(runtime.wordIndex),
    residentBytes: estimateWordIndexResidentBytes(runtime.wordIndex),
    truncated: runtime.wordIndex.truncated,
    skipped: docs.skipped
  });
  return {
    mode: "full",
    refreshed: runtime.wordIndex.docCount,
    dropped: 0,
    skipped: docs.skipped,
    reused: 0
  };
}
function scheduleStartupScans(deps, runtime, sessionGeneration, analysisRoot, snapshotRoot, languageProfile, dbg) {
  void (async () => {
    const resolved = await demandBootstrapDeps(deps, "session-start-scans");
    if (!resolved) {
      dbg("session_start scans: analyzer bootstrap unavailable \u2014 skipped");
      return;
    }
    scheduleStartupScansWithClients(resolved, runtime, sessionGeneration, analysisRoot, snapshotRoot, languageProfile, dbg);
  })();
}
function scheduleStartupScansWithClients(deps, runtime, sessionGeneration, analysisRoot, snapshotRoot, languageProfile, dbg) {
  const { todoScanner, cacheManager: cacheManager2, knipClient, jscpdClient, deadCodeClients, govulncheckClient, gitleaksClient, trivyClient, opengrepClient, astGrepClient: astGrepClient2, depChecker } = deps;
  const analyzerEnabled = (flag) => !deps.getFlag(flag);
  if (!analyzerEnabled("no-complexity")) {
    dbg("session_start complexity: skipped (disabled by config)");
    recordDegradationOnce({
      kind: "startup-analyzer-disabled",
      subject: "complexity",
      reason: "skipped (disabled by config)"
    });
  }
  const taskDeferMsByName = {
    "call-graph": 5e3,
    "codebase-model": 5200,
    "ast-grep exports": 5400,
    "project index": 5400
  };
  const runTask = (name, task) => {
    const queuedAt = Date.now();
    dbg(`session_start task ${name}: scheduled`);
    runtime.markStartupScanInFlight(name, sessionGeneration);
    const completion = new Promise((resolve30) => {
      const fire = () => {
        const startedAt = Date.now();
        dbg(`session_start task ${name}: start queuedMs=${startedAt - queuedAt}`);
        void task().then(() => {
          dbg(`session_start task ${name}: success runMs=${Date.now() - startedAt} queuedMs=${startedAt - queuedAt}`);
        }).catch((err) => {
          dbg(`session_start: ${name} background scan failed: ${err}`);
          dbg(`session_start task ${name}: failed runMs=${Date.now() - startedAt} queuedMs=${startedAt - queuedAt}`);
        }).finally(() => {
          runtime.clearStartupScanInFlight(name, sessionGeneration);
          dbg(`session_start task ${name}: end`);
          resolve30();
        });
      };
      const delay = taskDeferMsByName[name] ?? 0;
      if (delay > 0) {
        setTimeout(fire, delay).unref?.();
      } else {
        setImmediate(fire);
      }
    });
    return completion;
  };
  const canRunJsTsHeavyScans = canRunStartupHeavyScans(languageProfile, "jsts");
  const scanNames = ["todo", "dead-code"];
  if (canRunJsTsHeavyScans) {
    scanNames.push("knip", "jscpd", "ast-grep exports", "project index");
  }
  dbg(`session_start: launching background scans (${scanNames.join(", ")})`);
  void runTask("todo", async () => {
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    const items = await collectTodoBaselineItems(todoScanner, analysisRoot, () => runtime.isCurrentSession(sessionGeneration));
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    dbg(`session_start TODO scan: ${items.length} items (baseline stored)`);
    cacheManager2.writeCache("todo-baseline", { items }, analysisRoot);
  });
  if (!canRunJsTsHeavyScans) {
    dbg("session_start: skipping JS/TS startup scans (requires JS/TS language + project config)");
    return;
  }
  const isSubagent = isSubagentSession();
  const skipHeavyweightScans = isSlowFs(analysisRoot) || isSubagent;
  const runHeavyweightTask = (name, task) => {
    const flag = `no-${name}`;
    if (name !== "opengrep" && name !== "trivy" && !analyzerEnabled(flag)) {
      dbg(`session_start ${name}: skipped (disabled by config)`);
      recordDegradationOnce({
        kind: "startup-analyzer-disabled",
        subject: name,
        reason: "skipped (disabled by config)"
      });
      return;
    }
    if (skipHeavyweightScans)
      return;
    void runTask(name, task);
  };
  if (skipHeavyweightScans) {
    dbg(`session_start: skipping knip/jscpd/madge/dead-code/govulncheck/gitleaks/trivy/opengrep (${isSubagent ? "subagent" : "slow-fs"})`);
    deps.notify(`\u23ED\uFE0F Skipped background code-quality scans (knip/jscpd/madge/dead-code/govulncheck/gitleaks/trivy/opengrep): ${isSubagent ? subagentLightModeNotice() : slowFsDegradationNotice()}`, "info");
  }
  runHeavyweightTask("knip", async () => {
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    const cached = cacheManager2.readCache("knip", analysisRoot);
    if (cached) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      dbg(`session_start Knip: cache hit (${Math.round((Date.now() - new Date(cached.meta.timestamp).getTime()) / 1e3)}s ago)`);
    } else {
      const startMs = Date.now();
      const knipResult = await knipClient.analyze(analysisRoot, getKnipIgnorePatterns(), { projectSeq: runtime.projectSeq });
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      if (knipResult.success) {
        cacheManager2.writeCache("knip", knipResult, analysisRoot, {
          scanDurationMs: Date.now() - startMs
        });
      }
      dbg(`session_start Knip scan done (${Date.now() - startMs}ms)`);
    }
  });
  runHeavyweightTask("jscpd", async () => {
    if (await jscpdClient.ensureAvailable()) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      const isTsProject = nodeFs.existsSync(path12.join(analysisRoot, "tsconfig.json"));
      const scannerKey = isTsProject ? "jscpd-ts" : "jscpd";
      const cached = cacheManager2.readCache(scannerKey, analysisRoot);
      if (cached) {
        if (!runtime.isCurrentSession(sessionGeneration))
          return;
        dbg(`session_start jscpd: cache hit (${scannerKey})`);
      } else {
        const startMs = Date.now();
        const jscpdResult = await jscpdClient.scan(analysisRoot, void 0, void 0, isTsProject);
        if (!runtime.isCurrentSession(sessionGeneration))
          return;
        if (jscpdResult.success) {
          cacheManager2.writeCache(scannerKey, jscpdResult, analysisRoot, {
            scanDurationMs: Date.now() - startMs
          });
        }
        dbg(`session_start jscpd scan done (${Date.now() - startMs}ms, isTsProject=${isTsProject})`);
      }
    } else {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      dbg("session_start jscpd: not available");
    }
  });
  runHeavyweightTask("dead-code", async () => {
    const applicable = deadCodeClients.filter((c) => c.detect(analysisRoot));
    if (applicable.length === 0)
      return;
    await Promise.all(applicable.map(async (client) => {
      if (!runtime.isCurrentSession(sessionGeneration))
        return void 0;
      const cacheKey = `dead-code-${client.id}`;
      const cached = cacheManager2.readCache(cacheKey, analysisRoot);
      if (cached) {
        dbg(`session_start dead-code(${client.id}): cache hit`);
        return void 0;
      }
      const startMs = Date.now();
      const result = await client.analyze(analysisRoot);
      if (!runtime.isCurrentSession(sessionGeneration))
        return void 0;
      if (result.success) {
        cacheManager2.writeCache(cacheKey, result, analysisRoot, {
          scanDurationMs: Date.now() - startMs
        });
      }
      logDeadCodeScan({
        language: client.language,
        success: result.success,
        cached: false,
        unusedExports: result.unusedExports.length,
        unusedFiles: result.unusedFiles.length,
        unusedDeps: result.unusedDeps.length,
        unlistedDeps: result.unlistedDeps.length,
        durationMs: result.durationMs ?? Date.now() - startMs,
        ...!result.success && { reason: result.summary }
      });
      dbg(`session_start dead-code(${client.id}) done (${Date.now() - startMs}ms, ${deadCodeIssueCount(result)} issues)`);
    }));
  });
  runHeavyweightTask("govulncheck", async () => {
    if (!GovulncheckClient.hasGoModule(analysisRoot)) {
      dbg("session_start govulncheck: no go.mod \u2014 skipped");
      return;
    }
    if (!await govulncheckClient.ensureAvailable()) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      dbg("session_start govulncheck: not installed (go install golang.org/x/vuln/cmd/govulncheck@latest)");
      return;
    }
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    const cached = cacheManager2.readCache("govulncheck", analysisRoot);
    if (cached) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      dbg(`session_start govulncheck: cache hit (${cached.data.findings.length} findings)`);
      return;
    }
    const startMs = Date.now();
    const result = await govulncheckClient.analyze(analysisRoot);
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    if (result.success) {
      cacheManager2.writeCache("govulncheck", result, analysisRoot, {
        scanDurationMs: Date.now() - startMs
      });
    }
    dbg(`session_start govulncheck: ${result.findings.length} reachable findings (${Date.now() - startMs}ms)`);
  });
  runHeavyweightTask("gitleaks", async () => {
    if (!GitleaksClient.hasGitleaksSignal(analysisRoot)) {
      dbg("session_start gitleaks: no opt-in signal \u2014 skipped");
      return;
    }
    if (!await gitleaksClient.ensureAvailable()) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      dbg("session_start gitleaks: not available (install failed?)");
      return;
    }
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    const cached = cacheManager2.readCache("gitleaks", analysisRoot);
    if (cached) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      dbg(`session_start gitleaks: cache hit (${cached.data.findings.length} findings)`);
      return;
    }
    const startMs = Date.now();
    const result = await gitleaksClient.scan(analysisRoot);
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    if (result.success) {
      cacheManager2.writeCache("gitleaks", result, analysisRoot, {
        scanDurationMs: Date.now() - startMs
      });
    }
    dbg(`session_start gitleaks: ${result.findings.length} findings (${Date.now() - startMs}ms)`);
  });
  runHeavyweightTask("opengrep", async () => {
    if (!await opengrepClient.ensureAvailable()) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      dbg("session_start opengrep: not available (install failed?)");
      return;
    }
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    const cached = cacheManager2.readCache("opengrep", analysisRoot);
    if (cached) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      dbg(`session_start opengrep: cache hit (${cached.data.findings.length} findings)`);
      return;
    }
    const startMs = Date.now();
    const result = await opengrepClient.scan(analysisRoot);
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    if (result.success) {
      cacheManager2.writeCache("opengrep", result, analysisRoot, {
        scanDurationMs: Date.now() - startMs
      });
    }
    dbg(`session_start opengrep: ${result.findings.length} findings (${Date.now() - startMs}ms)`);
  });
  runHeavyweightTask("madge", async () => {
    if (!await depChecker.ensureAvailable()) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      dbg("session_start madge: not available");
      return;
    }
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    const cached = cacheManager2.readCache("madge", analysisRoot);
    if (cached) {
      dbg(`session_start madge: cache hit (${cached.data.circular.length} cycles)`);
      return;
    }
    const startMs = Date.now();
    const result = await depChecker.scanProject(analysisRoot);
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    cacheManager2.writeCache("madge", result, analysisRoot, {
      scanDurationMs: Date.now() - startMs
    });
    dbg(`session_start madge: ${result.circular.length} circular dependency chain(s) (${Date.now() - startMs}ms)`);
  });
  runHeavyweightTask("trivy", async () => {
    if (!TrivyClient.shouldScan(analysisRoot)) {
      dbg("session_start trivy: not enabled / no dependency manifest \u2014 skipped");
      return;
    }
    if (!await trivyClient.ensureAvailable()) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      dbg("session_start trivy: not available (install failed?)");
      return;
    }
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    const cached = cacheManager2.readCache("trivy", analysisRoot);
    if (cached) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      dbg(`session_start trivy: cache hit (${cached.data.findings.length} findings)`);
      return;
    }
    const startMs = Date.now();
    const result = await trivyClient.scan(analysisRoot);
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    if (result.success) {
      cacheManager2.writeCache("trivy", result, analysisRoot, {
        scanDurationMs: Date.now() - startMs
      });
    }
    dbg(`session_start trivy: ${result.findings.length} CVE findings (${Date.now() - startMs}ms)`);
  });
  let callGraphIdentity;
  const callGraphTask = runTask("call-graph", async () => {
    const { FactStore: FactStore2 } = await import("../chunk-YSNFMJKN.js");
    const { buildOrUpdateGraph, extractSymbolsAndRefsFromGraph, getReviewGraphCacheIdentity } = await import("../chunk-VD4JQCQC.js");
    const { buildCallGraph, saveCallGraph, loadCallGraph: loadCallGraph2 } = await import("../chunk-FJNKANOS.js");
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    const startMs = Date.now();
    const sessionFacts = new FactStore2("runtime-session-call-graph");
    const graph = await buildOrUpdateGraph(analysisRoot, [], sessionFacts);
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    const identity = getReviewGraphCacheIdentity(analysisRoot, graph);
    if (!identity) {
      dbg("session_start call-graph: canonical review-graph identity unavailable");
      return;
    }
    callGraphIdentity = {
      reviewGraphVersion: identity.version,
      reviewGraphSignature: identity.signature
    };
    const cached = loadCallGraph2(snapshotRoot, {
      reviewGraphVersion: identity.version,
      reviewGraphSignature: identity.signature
    });
    if (cached) {
      runtime.callGraph = cached.graph;
      dbg(`session_start call-graph: loaded from cache (${cached.graph.edges.length} edges, ${cached.graph.callers.size} callee entries, ${Date.now() - startMs}ms)`);
      return;
    }
    const { allSymbols, allRefs, coverage } = extractSymbolsAndRefsFromGraph(graph);
    const callGraph = buildCallGraph(allSymbols, allRefs, coverage);
    runtime.callGraph = callGraph;
    saveCallGraph(snapshotRoot, callGraph, {
      reviewGraphVersion: identity.version,
      reviewGraphSignature: identity.signature
    });
    dbg(`session_start call-graph: built ${callGraph.edges.length} edges, ${callGraph.callers.size} callee entries (${Date.now() - startMs}ms)`);
  });
  void runTask("codebase-model", async () => {
    await callGraphTask;
    if (!runtime.isCurrentSession(sessionGeneration))
      return;
    if (!runtime.callGraph)
      return;
    const { buildCodebaseModel, saveCodebaseModel, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET } = await import("../chunk-K6VYLB2U.js");
    if (!callGraphIdentity)
      return;
    const model = buildCodebaseModel(runtime.callGraph, analysisRoot, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET, callGraphIdentity);
    saveCodebaseModel(snapshotRoot, model);
    const top3 = model.entries.slice(0, 3).map((e) => e.name).join(", ");
    dbg(`session_start codebase-model: ${model.entries.length} entries, ${model.totalTokens} tokens, top symbols: ${top3 || "(none)"}`);
  });
  void runTask("ast-grep-exports", async () => {
    if (await astGrepClient2.ensureAvailable()) {
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      const exports = await astGrepClient2.scanExports(analysisRoot, "typescript");
      if (!runtime.isCurrentSession(sessionGeneration))
        return;
      dbg(`session_start exports scan: ${exports.size} functions found`);
      for (const [name, file] of exports) {
        runtime.cachedExports.set(name, file);
      }
      saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
    }
  });
  void runTask("word-index", async () => {
    await buildOrRefreshWordIndex({
      runtime,
      sessionGeneration,
      analysisRoot,
      snapshotRoot,
      dbg
    });
  });
}
function scheduleDeferredToolProbes(deps, languageProfile, startupDefaults, startupScansWillRun, dbg) {
  void (async () => {
    const resolved = await demandBootstrapDeps(deps, "session-start-tool-probes");
    if (!resolved) {
      dbg("session_start tools: analyzer bootstrap unavailable \u2014 no probes");
      return;
    }
    scheduleDeferredToolProbesWithClients(resolved, languageProfile, startupDefaults, startupScansWillRun, dbg);
  })();
}
function scheduleDeferredToolProbesWithClients(deps, languageProfile, startupDefaults, startupScansWillRun, dbg) {
  const { biomeClient, ruffClient, depChecker } = deps;
  const defaultTools = new Set(startupDefaults);
  const probes = [];
  if (languageProfile.present.jsts && !defaultTools.has("biome")) {
    probes.push(["biome", () => biomeClient.ensureAvailable()]);
  }
  if (languageProfile.present.python && !defaultTools.has("ruff")) {
    probes.push(["ruff", () => ruffClient.ensureAvailable()]);
  }
  if (startupScansWillRun) {
    probes.push(["madge", () => depChecker.ensureAvailable()]);
  }
  if (probes.length === 0) {
    dbg("session_start tools: no deferred availability probes needed");
    return;
  }
  void (async () => {
    const warmStart = Date.now();
    const results = await Promise.all(probes.map(async ([name, run]) => {
      try {
        return [name, await run()];
      } catch (err) {
        dbg(`session_start: ${name} availability check failed: ${err}`);
        return [name, false];
      }
    }));
    const summary = results.map(([name, ready]) => `${name}=${ready}`).join(" ");
    dbg(`session_start tools (deferred probes complete, ${Date.now() - warmStart}ms): ${summary}`);
  })();
}
var SESSION_START_GUIDANCE = [
  "\u{1F4CC} pi-lens active \u2014 automated checks run on every edit/write; blocking errors (including pre-existing) show inline and must be fixed.\nKey tools (see each tool's own description for args):\n\u2022 lens_diagnostics \u2014 source=session reads cache; empty cache \u2260 clean; use source=lsp scope=paths for changed files with absent or stale findings (aggregate hosts: lens(action=diagnostics)).\n\u2022 symbol_search \u2192 module_report \u2192 read_symbol/read_enclosing \u2014 ranked identifier search, then navigable outline/callback handles + exact body reads; cheaper than reading a whole file before editing.\n\u2022 Situational (activate via pi_lens_activate_tools): lsp_navigation, ast_grep_search, ast_grep_replace. Use ast_grep_search with dump=true to inspect AST nodes."
];
async function handleSessionStart(deps) {
  resetDegradationLedger();
  resetAnalyzerBootstrapSessionState();
  resetProjectDataDirSessionState();
  resetTestRunnerDelivery();
  resetLspMutationNoBridgeDbgLatch();
  resetBoundedTelemetry();
  rotateMessageEndAttribution();
  const handlerEnteredAt = Date.now();
  const sessionStartMs = deps.sessionStartFiredAt ?? handlerEnteredAt;
  const cwdForTelemetry = deps.ctxCwd ?? process.cwd();
  if (deps.sessionStartFiredAt !== void 0) {
    logLatency({
      type: "phase",
      filePath: cwdForTelemetry,
      phase: "session_start_prehandler",
      startedAt: new Date(deps.sessionStartFiredAt).toISOString(),
      durationMs: (deps.handlerEnteredAt ?? handlerEnteredAt) - deps.sessionStartFiredAt,
      metadata: { reason: deps.sessionReason }
    });
  }
  let startupMode = resolveStartupMode(deps.projectConfig, deps.globalConfig);
  const processGlobals = globalThis;
  const isFirstSessionOfProcess = !processGlobals.__piLensFirstSessionDone;
  if (isFirstSessionOfProcess && process.env.PI_LENS_COLD_START_QUICK !== "0" && !process.env.PI_LENS_STARTUP_MODE && !deps.projectConfig?.startup?.mode && !deps.globalConfig?.startup?.mode) {
    startupMode = deps.startupModeOverride ?? "quick";
  }
  processGlobals.__piLensFirstSessionDone = true;
  let warmupOwnSnapshotRead;
  if (startupMode === "quick" && !isPrintMode() && process.env.PI_LENS_COLD_START_QUICK !== "0" && !processGlobals.__piLensWarmupScheduled) {
    processGlobals.__piLensWarmupScheduled = true;
    const warmupDelayMs = Number(process.env.PI_LENS_WARMUP_DELAY_MS ?? 2e3);
    const warmupCwd = deps.ctxCwd ?? process.cwd();
    const warmupDbg = deps.dbg;
    const warmupTimer = setTimeout(() => {
      const warmupStartedAt = Date.now();
      void (async () => {
        try {
          warmupDbg("warmup: starting background warmup");
          const scanContextStartedAt = Date.now();
          const startupScanModule = await import("../chunk-ODEEOXPH.js");
          const languageProfileModule = await import("../chunk-AWE5TMPO.js");
          const warmupSnapshotRoot = resolveSnapshotRoot(warmupCwd);
          const cachedSnapshot = loadProjectSnapshot(warmupSnapshotRoot);
          warmupOwnSnapshotRead = cachedSnapshot ? {
            version: cachedSnapshot.version,
            seq: cachedSnapshot.seq,
            cachedExports: cachedSnapshot.cachedExports,
            projectRulesScan: cachedSnapshot.projectRulesScan
          } : null;
          const cachedVerdict = cachedSnapshot?.startupScan;
          let scan;
          if (cachedVerdict && startupScanModule.isStartupScanVerdictFresh(cachedVerdict)) {
            scan = { ...cachedVerdict, cwd: path12.resolve(warmupCwd) };
            warmupDbg(`warmup: scan-context reused from cache (canWarm=${scan.canWarmCaches}${scan.reason ? `, reason=${scan.reason}` : ""})`);
          } else {
            scan = await startupScanModule.resolveStartupScanContextAsync(warmupCwd);
            logLatency({
              type: "phase",
              phase: "session_start_scan_context_compute",
              filePath: warmupCwd,
              startedAt: new Date(scanContextStartedAt).toISOString(),
              durationMs: Date.now() - scanContextStartedAt,
              metadata: { mode: "quick-background" }
            });
            warmupDbg(`warmup: scan-context done in ${Date.now() - warmupStartedAt}ms (canWarm=${scan.canWarmCaches})`);
            saveRuntimeProjectSnapshot({
              cwd: warmupSnapshotRoot,
              runtime: deps.runtime,
              startupScan: scan,
              dbg: warmupDbg
            });
          }
          logLatency({
            type: "phase",
            phase: "warmup_scan_context",
            filePath: warmupCwd,
            startedAt: new Date(scanContextStartedAt).toISOString(),
            durationMs: Date.now() - scanContextStartedAt
          });
          if (!scan.canWarmCaches) {
            warmupDbg(`warmup: skipping language-profile (canWarm=false, reason=${scan.reason ?? "unknown"})`);
            return;
          }
          const languageRoot = scan.projectRoot ?? warmupCwd;
          const languageProfileStartedAt = Date.now();
          await languageProfileModule.detectProjectLanguageProfileAsync(languageRoot);
          warmupDbg(`warmup: language-profile done in ${Date.now() - languageProfileStartedAt}ms`);
          logLatency({
            type: "phase",
            phase: "warmup_language_profile",
            filePath: warmupCwd,
            startedAt: new Date(languageProfileStartedAt).toISOString(),
            durationMs: Date.now() - languageProfileStartedAt
          });
          const wordIndexStartedAt = Date.now();
          const wordIndexResult = await buildOrRefreshWordIndex({
            runtime: deps.runtime,
            sessionGeneration: deps.runtime.sessionGeneration,
            analysisRoot: languageRoot,
            snapshotRoot: warmupSnapshotRoot,
            dbg: warmupDbg
          });
          warmupDbg(`warmup: word-index done in ${Date.now() - wordIndexStartedAt}ms`);
          logLatency({
            type: "phase",
            phase: "warmup_word_index",
            filePath: warmupCwd,
            startedAt: new Date(wordIndexStartedAt).toISOString(),
            durationMs: Date.now() - wordIndexStartedAt,
            metadata: wordIndexResult ? { ...wordIndexResult } : void 0
          });
          const lspPrewarmStartedAt = Date.now();
          if (deps.getFlag("no-lsp")) {
            warmupDbg("warmup: skipping LSP pre-warm (no-lsp)");
          } else if (isSubagentSession()) {
            warmupDbg("warmup: skipping LSP pre-warm (subagent session)");
          } else if (isWarmAttached()) {
            warmupDbg("warmup: skipping LSP pre-warm (attached to incumbent)");
          } else {
            const lspConfig = await loadLSPConfig(warmupCwd).catch(() => ({
              warmFiles: []
            }));
            const warmFiles = lspConfig.warmFiles ?? [];
            if (warmFiles.length > 0) {
              await igniteWarmFiles(warmupCwd, warmFiles, deps.runtime, deps.runtime.sessionGeneration, warmupDbg);
            } else {
              await igniteDominantLanguageWarm(languageRoot, deps.runtime, deps.runtime.sessionGeneration, warmupDbg);
            }
            logLatency({
              type: "phase",
              phase: "warmup_lsp_prewarm",
              filePath: warmupCwd,
              startedAt: new Date(lspPrewarmStartedAt).toISOString(),
              durationMs: Date.now() - lspPrewarmStartedAt
            });
            warmupDbg(`warmup: lsp-prewarm done in ${Date.now() - lspPrewarmStartedAt}ms`);
          }
          warmupDbg(`warmup: total ${Date.now() - warmupStartedAt}ms`);
        } catch (err) {
          warmupDbg(`warmup: error ${err}`);
          processGlobals.__piLensWarmupScheduled = false;
        } finally {
          logLatency({
            type: "phase",
            phase: "warmup_total",
            filePath: warmupCwd,
            startedAt: new Date(warmupStartedAt).toISOString(),
            durationMs: Date.now() - warmupStartedAt
          });
        }
      })();
    }, warmupDelayMs);
    warmupTimer.unref?.();
  }
  const allowBootstrapTasks = startupMode === "full";
  const quickMode = startupMode === "quick";
  const { ctxCwd, getFlag, notify, dbg, log, runtime, cacheManager: cacheManager2, ensureTool, cleanStaleTsBuildInfo, resetDispatchBaselines: resetDispatchBaselines2, resetLSPService: resetLSPService2 } = deps;
  let _phaseT = Date.now();
  const phase = (name) => {
    dbg(`session_start phase ${name}: ${Date.now() - _phaseT}ms`);
    _phaseT = Date.now();
  };
  deps.bootstrap.peek()?.metricsClient.reset();
  getDiagnosticTracker().reset();
  clearAllSessions();
  runtime.complexityBaselines.clear();
  resetDispatchBaselines2(ctxCwd);
  resetWorkspaceTopology();
  resetOpaqueMutationState();
  resetObservedMutationNet();
  resetMutationAttribution();
  primePersistedMutationAttribution(ctxCwd);
  resetPendingAuxiliaryCoverage();
  resetSafeSpawnWindowsCommandCache();
  resetDispatchAvailabilityState();
  resetObservedRunnerLatency();
  resetPendingRunnerFindings();
  resetSpawnTimeoutCooldowns();
  clearFormatterCache();
  resetZizmorTokenAvailability();
  resetGoAvailability();
  resetRustAvailability();
  resetPsScriptAnalyzerAvailability();
  resetInstallRetryLatches();
  resetLazyInstallAttempts();
  resetDirectLspCommandAvailability();
  resetLSPCaseSensitivityState();
  resetResolvedPathCache();
  _resetPackageManagerCache();
  resetManagedToolRefreshSession();
  resetSmellsSessionState();
  resetSituationalToolTelemetry();
  resetWorkspaceDiagnosticsCacheSession(sessionStartMs);
  deps.bootstrap.peek()?.knipClient.resetSessionState?.();
  resetCascadeTierSessionState();
  runtime.resetForSession(sessionStartMs);
  logLatency({
    type: "phase",
    phase: "session_start_runtime_reset",
    filePath: ctxCwd ?? process.cwd(),
    startedAt: new Date(handlerEnteredAt).toISOString(),
    durationMs: Date.now() - handlerEnteredAt,
    metadata: { mode: startupMode, reason: deps.sessionReason }
  });
  const cleanupCwd = ctxCwd ?? process.cwd();
  setImmediate(() => {
    const logCleanupStartedAt = Date.now();
    try {
      const logCleanup = runLogCleanup(dbg);
      logLatency({
        type: "phase",
        phase: "session_start_log_cleanup",
        filePath: cleanupCwd,
        startedAt: new Date(logCleanupStartedAt).toISOString(),
        durationMs: Date.now() - logCleanupStartedAt,
        metadata: {
          mode: startupMode,
          reason: deps.sessionReason,
          deferred: true
        }
      });
      if (logCleanup.cleaned > 0 || logCleanup.rotated > 0) {
        notify(`\u{1F9F9} ${logCleanup.report}`, "info");
      }
    } catch (err) {
      dbg(`session_start: deferred log cleanup failed: ${err}`);
    }
  });
  dbg(`session_start startup mode: ${startupMode}`);
  if (!getFlag("no-lsp")) {
    resetLSPService2({ fast: true, reason: "session_start" });
    dbg("session_start: LSP service reset");
    dbg("session_start: phase0 workspace diagnostics observation enabled (capability probe only)");
  }
  const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
  const cwd = ctxCwd ?? process.cwd();
  const projectDataDir = getProjectDataDir(cwd);
  for (const migration of drainProjectDataDirMigrations()) {
    const targetName = path12.basename(migration.to);
    const hash = targetName.match(/([0-9a-f]{8})$/)?.[1] ?? "unknown";
    if (migration.outcome === "ephemeral") {
      recordDegradationOnce({
        kind: "data-dir-ephemeral",
        subject: hash,
        reason: "temporary checkout: project data lives in a process-owned directory and is not kept after exit"
      });
      continue;
    }
    recordDegradationOnce({
      kind: "data_dir_migrated",
      subject: hash,
      reason: migration.outcome === "renamed" ? "using hashed directory after renaming legacy directory" : migration.outcome === "coexisting" ? "using hashed directory because legacy and hashed directories both exist" : migration.outcome === "rename-failed" ? "using legacy directory after hashed-directory rename failure" : "using resolved directory after realpath fallback"
    });
  }
  const globalDir = getGlobalPiLensDir();
  void sweepAtomicWriteStages([
    projectDataDir,
    path12.join(projectDataDir, "cache"),
    path12.join(projectDataDir, "sessions"),
    globalDir,
    path12.join(globalDir, "bin"),
    path12.join(globalDir, "tools")
  ]).catch(() => {
  });
  void sweepDeadEphemeralDataDirs({ isPidAlive: realIsPidAlive }).catch(() => {
  });
  if (quickMode) {
    runtime.projectRoot = cwd;
    const sequenceReadStartedAt2 = Date.now();
    const snapshotRoot2 = resolveSnapshotRoot(cwd);
    const { latestSeq: latestSeq2, timedOut: timedOut2 } = await readSequenceWithBudget({
      snapshotRoot: snapshotRoot2,
      base: snapshotSequenceBase(snapshotRoot2),
      cwd,
      runtime,
      sessionGeneration: runtime.sessionGeneration,
      dbg,
      onDeferredSequenceResolved: retroactivelyHydrateAfterDeferredSequence({
        getWarmupOwnSnapshotRead: () => warmupOwnSnapshotRead,
        snapshotRoot: snapshotRoot2,
        runtime,
        dbg
      })
    });
    logLatency({
      type: "phase",
      phase: "session_start_sequence_read",
      filePath: cwd,
      startedAt: new Date(sequenceReadStartedAt2).toISOString(),
      durationMs: Date.now() - sequenceReadStartedAt2,
      metadata: { entries: latestSeq2.fileSeqByPath.size, timedOut: timedOut2 }
    });
    if (timedOut2) {
      recordSnapshotSequenceTimeout({
        snapshotRoot: snapshotRoot2,
        snapshotPath: getProjectSnapshotPath(snapshotRoot2)
      });
    }
    runtime.seedProjectSequence?.(latestSeq2.projectSeq, latestSeq2.fileSeqByPath, latestSeq2.logEntries);
    const effectiveSeq2 = runtime.projectSeq ?? latestSeq2.projectSeq;
    dbg(`session_start sequence: projectSeq=${effectiveSeq2} fileSeqEntries=${latestSeq2.fileSeqByPath.size}`);
    const freshnessSeq2 = timedOut2 ? UNKNOWN_PROJECT_SEQ : effectiveSeq2;
    const snapshotLoadStartedAt2 = Date.now();
    const snapshotPath2 = getProjectSnapshotPath(snapshotRoot2);
    let snapshotBytes2 = 0;
    try {
      snapshotBytes2 = nodeFs.statSync(snapshotPath2).size;
    } catch {
    }
    const snapshotBodyPresent2 = nodeFs.existsSync(snapshotPath2) || nodeFs.existsSync(getProjectSnapshotLegacyPath(snapshotRoot2));
    const snapshotGate2 = loadSnapshotBodyUnlessStale({
      root: snapshotRoot2,
      currentProjectSeq: freshnessSeq2,
      unlockedThrough: latestSeq2.unlockedThrough ?? 0,
      dbg
    });
    const snapshot2 = snapshotGate2.snapshot;
    const snapshotFresh2 = isProjectSnapshotFresh(snapshot2, freshnessSeq2, latestSeq2.unlockedThrough);
    const snapshotMissReason2 = describeSnapshotMiss(snapshot2, freshnessSeq2, {
      skippedStale: snapshotGate2.skippedStale,
      bodyPresent: snapshotBodyPresent2,
      unlockedThrough: latestSeq2.unlockedThrough ?? 0
    });
    logLatency({
      type: "phase",
      phase: "session_start_snapshot_load",
      filePath: cwd,
      startedAt: new Date(snapshotLoadStartedAt2).toISOString(),
      durationMs: Date.now() - snapshotLoadStartedAt2,
      metadata: {
        bytes: snapshotBytes2,
        fresh: snapshotFresh2,
        seq: snapshot2?.seq ?? null,
        reason: snapshotFresh2 ? void 0 : snapshotMissReason2,
        ...snapshotGate2.skippedStale ? { skippedStale: true } : {},
        ...timedOut2 ? { sequenceUnknown: true } : {}
      }
    });
    logProjectSnapshotProbe({
      dbg,
      root: snapshotRoot2,
      currentProjectSeq: freshnessSeq2,
      unlockedThrough: latestSeq2.unlockedThrough ?? 0,
      snapshot: snapshot2,
      missReason: snapshotMissReason2
    });
    if (snapshotFresh2) {
      hydrateRuntimeFromProjectSnapshot(runtime, snapshot2);
    }
    const quickTools = [];
    if (!getFlag("no-lsp")) {
      quickTools.push("LSP Service");
    }
    log(`Active tools: ${quickTools.join(", ")}`);
    dbg(`session_start tools: ${quickTools.join(", ") || "deferred (quick mode)"}`);
    dbg("session_start: quick mode active - skipping slow tool probes, language profiling, preinstall, scans, and error debt baseline");
    logLatency({
      type: "phase",
      phase: "session_start_skipped_steps",
      filePath: cwd,
      durationMs: 0,
      metadata: {
        mode: startupMode,
        reason: deps.sessionReason,
        // #1911 review F3: this `if (quickMode)` branch is an early return —
        // the whole quick path — so there is no enumerable list of "steps
        // run vs. skipped" this array could be derived from mechanically. It
        // is DESCRIPTIVE documentation of what the early return skips,
        // matching the `dbg()` line above word for word; keep both in sync by
        // hand if either changes. It also does NOT narrow under
        // `getFlag("no-lsp")` — quick mode skips the same five steps either
        // way; the LSP flag instead affects `quickTools` above, a separate
        // record.
        steps: [
          "slow_tool_probes",
          "language_profiling",
          "tool_preinstall",
          "startup_scans",
          "error_debt_baseline"
        ]
      }
    });
    const totalDurationMs2 = Date.now() - sessionStartMs;
    dbg(`session_start total: ${totalDurationMs2}ms (interactive path)`);
    logLatency({
      type: "phase",
      phase: "session_start_total",
      filePath: cwd,
      startedAt: new Date(sessionStartMs).toISOString(),
      durationMs: totalDurationMs2,
      metadata: {
        mode: startupMode,
        reason: deps.sessionReason,
        // #2129: the root-identity input decideSessionStart consulted,
        // alongside `mode` — every start reaching this line already
        // classified `primary`/`sequential-replacement` (a `secondary-root`
        // start returns before `handleSessionStart` is ever called).
        ...sessionStartDecisionMetadata(deps)
      }
    });
    logHostReadyDelay(deps, cwd);
    emitSmellsSessionStartLine(dbg, sessionStartMs);
    return;
  }
  const tools = [];
  if (!getFlag("no-lsp"))
    tools.push("LSP Service");
  if (allowBootstrapTasks && !getFlag("no-lsp")) {
    const cleaned = cleanStaleTsBuildInfo(ctxCwd ?? process.cwd());
    if (cleaned.length > 0) {
      notify(`\u{1F9F9} Deleted stale TypeScript build cache (${cleaned.map((f) => path12.basename(f)).join(", ")}) \u2014 phantom errors suppressed.`, "warning");
      dbg(`session_start: cleaned stale tsbuildinfo: ${cleaned.join(", ")}`);
    }
  }
  const sessionGeneration = runtime.sessionGeneration;
  const sequenceReadStartedAt = Date.now();
  const snapshotRoot = resolveSnapshotRoot(cwd);
  const { latestSeq, timedOut } = await readSequenceWithBudget({
    snapshotRoot,
    base: snapshotSequenceBase(snapshotRoot),
    cwd,
    runtime,
    sessionGeneration,
    dbg
  });
  logLatency({
    type: "phase",
    phase: "session_start_sequence_read",
    filePath: cwd,
    startedAt: new Date(sequenceReadStartedAt).toISOString(),
    durationMs: Date.now() - sequenceReadStartedAt,
    metadata: { entries: latestSeq.fileSeqByPath.size, timedOut }
  });
  if (timedOut) {
    recordSnapshotSequenceTimeout({
      snapshotRoot,
      snapshotPath: getProjectSnapshotPath(snapshotRoot)
    });
  }
  runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath, latestSeq.logEntries);
  const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
  dbg(`session_start sequence: projectSeq=${effectiveSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
  const freshnessSeq = timedOut ? UNKNOWN_PROJECT_SEQ : effectiveSeq;
  const snapshotLoadStartedAt = Date.now();
  const snapshotPath = getProjectSnapshotPath(snapshotRoot);
  let snapshotBytes = 0;
  try {
    snapshotBytes = nodeFs.statSync(snapshotPath).size;
  } catch {
  }
  const snapshotBodyPresent = nodeFs.existsSync(snapshotPath) || nodeFs.existsSync(getProjectSnapshotLegacyPath(snapshotRoot));
  const snapshotGate = loadSnapshotBodyUnlessStale({
    root: snapshotRoot,
    currentProjectSeq: freshnessSeq,
    unlockedThrough: latestSeq.unlockedThrough ?? 0,
    dbg
  });
  const snapshot = snapshotGate.snapshot;
  const snapshotFresh = isProjectSnapshotFresh(snapshot, freshnessSeq, latestSeq.unlockedThrough);
  const snapshotMissReason = describeSnapshotMiss(snapshot, freshnessSeq, {
    skippedStale: snapshotGate.skippedStale,
    bodyPresent: snapshotBodyPresent,
    unlockedThrough: latestSeq.unlockedThrough ?? 0
  });
  logLatency({
    type: "phase",
    phase: "session_start_snapshot_load",
    filePath: cwd,
    startedAt: new Date(snapshotLoadStartedAt).toISOString(),
    durationMs: Date.now() - snapshotLoadStartedAt,
    metadata: {
      bytes: snapshotBytes,
      fresh: snapshotFresh,
      seq: snapshot?.seq ?? null,
      reason: snapshotFresh ? void 0 : snapshotMissReason,
      ...snapshotGate.skippedStale ? { skippedStale: true } : {},
      ...timedOut ? { sequenceUnknown: true } : {}
    }
  });
  logProjectSnapshotProbe({
    dbg,
    root: snapshotRoot,
    currentProjectSeq: freshnessSeq,
    unlockedThrough: latestSeq.unlockedThrough ?? 0,
    snapshot,
    missReason: snapshotMissReason
  });
  const freshSnapshot = snapshotFresh ? snapshot : null;
  if (freshSnapshot) {
    hydrateRuntimeFromProjectSnapshot(runtime, freshSnapshot);
  }
  const cachedStartupScan = freshSnapshot?.startupScan && isStartupScanVerdictFresh(freshSnapshot.startupScan) ? freshSnapshot.startupScan : void 0;
  const startupScanSource = cachedStartupScan ? "snapshot" : "computed";
  let startupScan;
  if (cachedStartupScan) {
    startupScan = { ...cachedStartupScan, cwd: path12.resolve(cwd) };
  } else {
    const scanContextStartedAt = Date.now();
    startupScan = resolveStartupScanContext(cwd);
    logLatency({
      type: "phase",
      phase: "session_start_scan_context_compute",
      filePath: cwd,
      startedAt: new Date(scanContextStartedAt).toISOString(),
      durationMs: Date.now() - scanContextStartedAt,
      metadata: { mode: startupMode }
    });
  }
  phase("scan-context");
  dbg(`session_start scan-context source=${startupScanSource}`);
  const scanRoot = startupScan.projectRoot ?? cwd;
  const useScanRootForSignals = startupScan.canWarmCaches || startupScan.reason === "too-many-source-files" || startupScan.reason === "too-many-entries";
  const analysisRoot = useScanRootForSignals ? scanRoot : cwd;
  runtime.projectRoot = cwd;
  const languageProfileSource = freshSnapshot?.languageProfile ? "snapshot" : "computed";
  const languageProfile = freshSnapshot?.languageProfile ? freshSnapshot.languageProfile : detectProjectLanguageProfile(analysisRoot, startupScan.canWarmCaches ? void 0 : []);
  phase("language-profile");
  dbg(`session_start language-profile source=${languageProfileSource}`);
  dbg(`session_start cwd: ${cwd}`);
  dbg(`session_start scan root: ${scanRoot} (warmCaches=${startupScan.canWarmCaches}${startupScan.reason ? `, reason=${startupScan.reason}` : ""})`);
  dbg(`session_start analysis root: ${analysisRoot}`);
  dbg(`session_start workspace root: ${runtime.projectRoot}`);
  dbg(`session_start language profile: ${languageProfile.detectedKinds.join(", ") || "none"}`);
  dbg(`session_start language counts: ${JSON.stringify(languageProfile.counts)} configured=${JSON.stringify(languageProfile.configured)}`);
  dbg(`session_start workspace cwd available: ${hasWorkspaceCwd}`);
  if (useScanRootForSignals && analysisRoot !== cwd) {
    dbg(`session_start: monorepo analysis root override -> ${analysisRoot}`);
  }
  const slowFsVerdict = getSlowFsVerdict(analysisRoot);
  logLatency({
    type: "phase",
    phase: "slow_fs_probe",
    filePath: analysisRoot,
    durationMs: 0,
    metadata: {
      slow: slowFsVerdict.slow,
      medianStatMicros: slowFsVerdict.medianStatMicros,
      samples: slowFsVerdict.samples
    }
  });
  dbg(`session_start slow-fs probe: slow=${slowFsVerdict.slow} medianStatMicros=${slowFsVerdict.medianStatMicros.toFixed(1)} samples=${slowFsVerdict.samples}`);
  if (slowFsVerdict.slow) {
    notify(`\u{1F422} Slow filesystem detected (median ${slowFsVerdict.medianStatMicros.toFixed(0)}\xB5s/stat) \u2014 reduced-scan mode engaged (set PI_LENS_ALLOW_SLOW_FS_SCAN=1 to override).`, "warning");
  }
  const subagentSession = isSubagentSession();
  if (isWarmAttached()) {
    dbg("session_start lsp-warm: skipping pre-warm (attached to incumbent)");
  } else if (subagentSession) {
    const identity = getSubagentIdentity();
    logLatency({
      type: "phase",
      phase: "subagent_light_mode",
      filePath: analysisRoot,
      durationMs: 0,
      metadata: {
        runId: identity?.runId,
        agentName: identity?.agentName,
        marker: identity?.marker
      }
    });
    dbg(`session_start subagent light mode: engaged (marker=${identity?.marker ?? "unknown"} runId=${identity?.runId ?? "unknown"} agentName=${identity?.agentName ?? "unknown"})`);
  }
  const lensLspEnabled = !getFlag("no-lsp");
  const startupDefaults = getDefaultStartupTools(languageProfile).filter((tool) => {
    if ((tool === "typescript-language-server" || tool === "pyright") && !lensLspEnabled) {
      return false;
    }
    return true;
  });
  if (!allowBootstrapTasks) {
    dbg("session_start: skipping tool preinstall (startup mode)");
  } else if (startupDefaults.length > 0) {
    dbg(`session_start: pre-install defaults -> ${startupDefaults.join(", ")}`);
    firePreinstallDefaults(ensureTool, dbg, startupDefaults);
  } else {
    dbg("session_start: no language defaults selected for pre-install");
  }
  const startupScansEnabled = deps.projectConfig?.startup?.scans?.enabled ?? deps.globalConfig?.startup?.scans?.enabled ?? true;
  const startupScansWillRun = allowBootstrapTasks && startupScan.canWarmCaches && startupScansEnabled;
  const jstsHeavyScansWillRun = startupScansWillRun && canRunStartupHeavyScans(languageProfile, "jsts");
  if (allowBootstrapTasks) {
    scheduleDeferredToolProbes(deps, languageProfile, startupDefaults, jstsHeavyScansWillRun, dbg);
    scheduleManagedToolRefresh(dbg);
  }
  if (allowBootstrapTasks) {
    void probePrettierInstall(ensureTool, dbg, analysisRoot);
  } else {
    dbg("session_start: skipping prettier preinstall probe (startup mode)");
  }
  const summaryClients = await deps.bootstrap.request("session-start-tools");
  const detectedRunner = summaryClients?.testRunnerClient.detectRunner(analysisRoot);
  phase("test-runner-detect");
  if (detectedRunner)
    tools.push(`Test runner (${detectedRunner.runner})`);
  if (await summaryClients?.goClient.isGoAvailableAsync())
    tools.push("Go (go vet)");
  if (await summaryClients?.rustClient.isAvailableAsync())
    tools.push("Rust (cargo)");
  log(`Active tools: ${tools.join(", ")}`);
  dbg(`session_start tools: ${tools.join(", ")}`);
  const agentStartupGuidance = SESSION_START_GUIDANCE;
  runtime.projectRulesScan = scanProjectRules(analysisRoot);
  saveRuntimeProjectSnapshot({
    cwd: snapshotRoot,
    runtime,
    startupScan,
    languageProfile,
    dbg
  });
  phase("project-rules");
  if (runtime.projectRulesScan.hasCustomRules) {
    const ruleCount = runtime.projectRulesScan.rules.length;
    const sources = [
      ...new Set(runtime.projectRulesScan.rules.map((r) => r.source))
    ];
    dbg(`session_start: found ${ruleCount} project rule(s) from ${sources.join(", ")}`);
  } else {
    dbg("session_start: no project rules found");
  }
  cacheManager2.writeCache("session-start-guidance", { content: agentStartupGuidance.join("\n") }, analysisRoot);
  if (!allowBootstrapTasks) {
    dbg("session_start: skipping startup background scans (startup mode)");
  } else if (!startupScansEnabled) {
    dbg("session_start: skipping startup background scans (disabled by config)");
    recordDegradationOnce({
      kind: "startup-analyzer-disabled",
      subject: "startup-scans",
      reason: "skipped (disabled by config)"
    });
  } else if (!startupScan.canWarmCaches) {
    dbg(`session_start: skipping heavy scans (${startupScan.reason ?? "unknown"})`);
    dbg(`session_start: skipping TODO scan (${startupScan.reason ?? "unknown"})`);
    if (startupScan.reason === "too-many-source-files" || startupScan.reason === "too-many-entries") {
      const overrideHint = startupScan.reason === "too-many-entries" ? ` (set PI_LENS_STARTUP_SCAN_MAX_ENTRIES=<n> to override the ${getStartupScanMaxEntries()}-entry cap)` : "";
      notify(`\u{1F4E6} Project-size limits disabled background warm scans (heavy scans, TODO scan, LSP pre-warm)${overrideHint}.`, "warning");
    }
  } else {
    scheduleStartupScans(deps, runtime, sessionGeneration, analysisRoot, snapshotRoot, languageProfile, dbg);
  }
  if (subagentSession) {
    dbg("session_start lsp-warm: skipping pre-warm (subagent session)");
  } else if (!getFlag("no-lsp") && allowBootstrapTasks) {
    setImmediate(() => {
      void loadLSPConfig(cwd).then((lspConfig) => {
        const warmFiles = lspConfig.warmFiles ?? [];
        dbg(`session_start lsp-config: loaded (${warmFiles.length} warm file(s) configured)`);
        if (warmFiles.length > 0) {
          igniteWarmFiles(cwd, warmFiles, runtime, sessionGeneration, dbg).catch((err) => dbg(`session_start lsp-warm: unhandled error: ${err}`));
        } else if (startupScan.canWarmCaches) {
          igniteDominantLanguageWarm(analysisRoot, runtime, sessionGeneration, dbg).catch((err) => dbg(`session_start lsp-warm: unhandled dominant error: ${err}`));
        } else {
          dbg(`session_start lsp-warm: skipping dominant-language auto-warm (${startupScan.reason ?? "unknown"})`);
        }
      });
    });
    phase("lsp-config");
  }
  setSessionLanguages(languageProfile.detectedKinds);
  const totalDurationMs = Date.now() - sessionStartMs;
  dbg(`session_start total: ${totalDurationMs}ms (interactive path; background tasks may continue)`);
  logLatency({
    type: "phase",
    phase: "session_start_total",
    filePath: cwd,
    startedAt: new Date(sessionStartMs).toISOString(),
    durationMs: totalDurationMs,
    metadata: {
      mode: startupMode,
      reason: deps.sessionReason,
      // #2129: see the quick-mode session_start_total record above.
      // Keep the full path's durable shape identical to quick mode.
      ...sessionStartDecisionMetadata(deps)
    }
  });
  logHostReadyDelay(deps, cwd);
  emitSmellsSessionStartLine(dbg, sessionStartMs);
}
function sessionStartDecisionMetadata(deps) {
  return {
    classification: deps.sessionStartClassification,
    sameRoot: deps.sessionStartSameRoot ?? "unknown",
    basis: deps.sessionStartBasis,
    gapMs: deps.sessionStartGapMs,
    lineageMatch: deps.sessionStartLineageMatch
  };
}
function emitSmellsSessionStartLine(dbg, sessionStartMs) {
  try {
    const line = formatSmellsSessionStartLine(countRecentSmells(void 0, sessionStartMs));
    if (line)
      dbg(line);
  } catch {
  }
}

// dist/clients/runtime-turn.js
import * as fs17 from "node:fs";
import * as path23 from "node:path";

// dist/clients/actionable-warnings.js
import { createHash as createHash2 } from "node:crypto";
import * as fs12 from "node:fs";
import * as path14 from "node:path";

// dist/clients/actionable-warnings-logger.js
import * as path13 from "node:path";
var AW_LOG_DIR = getGlobalPiLensLogDir();
var AW_LOG_FILE = path13.join(AW_LOG_DIR, "actionable-warnings.log");
var AW_LOG_BACKUP_FILE = path13.join(AW_LOG_DIR, "actionable-warnings.log.1");
var MAX_LOG_BYTES2 = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_AW_LOG_MAX_BYTES ?? "1048576", 10) || 1048576);
var writer2 = createNdjsonLogger({
  filePath: AW_LOG_FILE,
  maxBytes: MAX_LOG_BYTES2,
  backupPath: AW_LOG_BACKUP_FILE
});
function logActionableWarningsEvent(entry) {
  if (isTestMode()) {
    return;
  }
  writer2.log({
    ts: (/* @__PURE__ */ new Date()).toISOString(),
    ...entry,
    ...entry.filePath !== void 0 ? { filePath: normalizeFilePath(entry.filePath) } : {}
  });
}

// dist/clients/actionable-warnings.js
var beforeWarningStateLockForTests = null;
function createActionableWarningId(args) {
  return stableFindingId("aw:", {
    cwd: args.cwd,
    filePath: args.filePath,
    parts: [
      args.tool,
      args.source,
      args.code,
      args.rule,
      normalizeMessage(args.message),
      args.line
    ]
  });
}
function legacyActionableWarningId(args) {
  const rel = path14.relative(args.cwd, args.filePath).replace(/\\/g, "/");
  const legacyRelativeFile = rel && !rel.startsWith("..") ? rel : normalizeMapKey(args.filePath);
  const parts = [
    legacyRelativeFile,
    args.tool ?? "",
    args.source ?? "",
    String(args.code ?? ""),
    args.rule ?? "",
    normalizeMessage(args.message),
    String(args.line ?? "")
  ];
  return `aw:${hashText(parts.join("|"), 10)}`;
}
function actionSafety(action) {
  const kind = action.kind ?? "";
  if (!kind.startsWith("quickfix"))
    return { eligible: false, reason: "not_quickfix" };
  if (!action.isPreferred)
    return { eligible: false, reason: "not_preferred" };
  if (!action.edit)
    return { eligible: false, reason: "no_edit" };
  if (action.command)
    return { eligible: false, reason: "has_command" };
  return { eligible: true };
}
function serializeAction(action) {
  const safety = actionSafety(action);
  return {
    title: action.title,
    kind: action.kind,
    isPreferred: action.isPreferred,
    hasEdit: Boolean(action.edit),
    hasCommand: Boolean(action.command),
    autoFixEligible: safety.eligible,
    skipReason: safety.reason
  };
}
function deserializeSuppressionState(contents) {
  try {
    const parsed = JSON.parse(contents ?? "");
    return parsed && typeof parsed === "object" ? parsed : {};
  } catch {
    return {};
  }
}
function readSuppressionState(cwd) {
  const statePath = path14.join(getProjectDataDir(cwd), "cache", "actionable-warning-state.json");
  try {
    return deserializeSuppressionState(fs12.readFileSync(statePath, "utf8"));
  } catch {
    return {};
  }
}
function updateWarningState(cwd, warnings) {
  const statePath = path14.join(getProjectDataDir(cwd), "cache", "actionable-warning-state.json");
  fs12.mkdirSync(path14.dirname(statePath), { recursive: true });
  const hook = beforeWarningStateLockForTests;
  beforeWarningStateLockForTests = null;
  hook?.();
  commitDurableStore({
    path: statePath,
    deserialize: deserializeSuppressionState,
    merge: (state) => {
      const now = (/* @__PURE__ */ new Date()).toISOString();
      state.warnings ??= {};
      for (const warning of warnings) {
        const legacyId = warning.legacyId;
        const legacyEntry = legacyId && legacyId !== warning.id ? state.warnings[legacyId] : void 0;
        const existing = state.warnings[warning.id] ?? legacyEntry ?? {};
        state.warnings[warning.id] = {
          ...existing,
          status: existing.status ?? "active",
          firstSeenAt: existing.firstSeenAt ?? now,
          lastSeenAt: now,
          seenCount: (existing.seenCount ?? 0) + 1
        };
        if (legacyEntry && legacyId)
          delete state.warnings[legacyId];
      }
      return state;
    },
    serialize: (state) => JSON.stringify(state, null, 2),
    waitMs: 2e3,
    retryMs: 10,
    timeoutMessage: "timed out acquiring actionable warning store lock",
    onContention: "skip-log",
    logContention: () => logActionableWarningsEvent({
      event: "warning_state_write_dropped",
      metadata: { reason: "lock_contention" }
    })
  });
}
function suppressionFor(cwd, id, args) {
  const state = readSuppressionState(cwd);
  const legacyId = legacyActionableWarningId({ cwd, ...args });
  const entry = state.warnings?.[id] ?? (legacyId !== id ? state.warnings?.[legacyId] : void 0);
  return {
    suppressed: entry?.status === "suppressed",
    reason: entry?.reason,
    legacyId
  };
}
function lineInModifiedRanges(line, ranges) {
  if (line === void 0)
    return true;
  if (ranges.length === 0)
    return true;
  return ranges.some((range) => line >= range.start - 2 && line <= range.end + 2);
}
function recordFromLspDiagnostic(diag, filePath, cwd) {
  const line = diag.range.start.line + 1;
  const column = diag.range.start.character + 1;
  const producer = diag.source && diag.source !== "lsp" ? diag.source : diag.serverId;
  const source = diag.source ?? "lsp";
  const code = diag.code === void 0 ? void 0 : String(diag.code);
  const identityArgs = {
    filePath,
    tool: producer ?? "lsp",
    source,
    code,
    message: diag.message,
    line
  };
  const id = createActionableWarningId({ cwd, ...identityArgs });
  const suppression = suppressionFor(cwd, id, identityArgs);
  return {
    id,
    filePath,
    displayPath: toRunnerDisplayPath(cwd, filePath),
    line,
    column,
    severity: "warning",
    tool: producer ?? "lsp",
    source,
    code,
    rule: code ? `${source}:${code}` : source,
    message: diag.message,
    actions: [],
    suppressed: suppression.suppressed,
    suppressionReason: suppression.reason,
    legacyId: suppression.legacyId,
    origin: "lsp"
  };
}
function mergeWarnings(records) {
  const byId = /* @__PURE__ */ new Map();
  for (const record of records) {
    const existing = byId.get(record.id);
    if (!existing) {
      byId.set(record.id, { ...record, actions: [...record.actions] });
      continue;
    }
    existing.origin = existing.origin === record.origin ? existing.origin : "merged";
    existing.fixSuggestion ??= record.fixSuggestion;
    existing.fixKind ??= record.fixKind;
    existing.autoFixAvailable ||= record.autoFixAvailable;
    const seenActions = new Set(existing.actions.map((a) => `${a.kind ?? ""}|${a.title}`));
    for (const action of record.actions) {
      const key = `${action.kind ?? ""}|${action.title}`;
      if (!seenActions.has(key)) {
        existing.actions.push(action);
        seenActions.add(key);
      }
    }
  }
  return [...byId.values()].sort((a, b) => a.displayPath.localeCompare(b.displayPath) || (a.line ?? 0) - (b.line ?? 0));
}
var ACTIONABLE_WARNINGS_LSP_FILE_CAP = 25;
var ACTIONABLE_WARNINGS_LSP_BUDGET_MS = 2500;
var ACTIONABLE_WARNINGS_DEFERRED_BUDGET_MS = 6e4;
var ACTIONABLE_WARNINGS_LSP_PULL_TIMEOUT_MS = 1e4;
var ACTIONABLE_WARNINGS_MAX_CODE_ACTIONS_PER_FILE = 25;
async function boundedLspCall(call, deps) {
  if (deps.signal?.aborted)
    return void 0;
  const remainingMs = deps.deadlineAt !== void 0 ? deps.deadlineAt - Date.now() : void 0;
  if (remainingMs !== void 0 && remainingMs <= 0)
    return void 0;
  const inner = bounded(call().then((value) => ({ value })), {
    // Always the FULL per-trip budget -- never clamped to the loop's own
    // residual. The residual is enforced below, off the ledger.
    ms: deps.pullTimeoutMs,
    // Optional for the in-band caller; `bounded()` reads a missing signal
    // as one that never aborts, so the deadline half stays live.
    signal: deps.signal,
    // From the deps, never a literal: the in-band and deferred loops must
    // key separately (#2557 review F3).
    hook: deps.site.hook,
    label: deps.site.label
  });
  const boxed = deps.deadlineAt !== void 0 ? await withDeadline(inner, {
    deadlineAt: deps.deadlineAt,
    onTimeout: "undefined"
  }) : await inner;
  return boxed?.value;
}
function boundedNumber(value, fallback) {
  return value !== void 0 && Number.isFinite(value) && value > 0 ? value : fallback;
}
async function buildActionableWarningsReport(args) {
  const cwd = path14.resolve(args.cwd);
  const records = [...args.dispatchWarnings];
  const lspService = getLSPService();
  const observedAtByPath = /* @__PURE__ */ new Map();
  const buildStartedAt = (/* @__PURE__ */ new Date()).toISOString();
  logActionableWarningsEvent({
    event: "report_started",
    sessionId: args.sessionId,
    metadata: {
      turnIndex: args.turnIndex,
      filesCount: args.files.length,
      dispatchWarningsCount: args.dispatchWarnings.length,
      deltaOnly: args.deltaOnly !== false,
      includeLspCodeActions: args.includeLspCodeActions
    }
  });
  if (args.includeLspCodeActions) {
    const fileCap = Math.floor(boundedNumber(args.lspFileCap, ACTIONABLE_WARNINGS_LSP_FILE_CAP));
    const budgetMs = boundedNumber(args.lspBudgetMs, ACTIONABLE_WARNINGS_LSP_BUDGET_MS);
    const eligible = [];
    let outsideRoot = 0;
    for (const file of args.files) {
      const filePath = path14.resolve(cwd, file);
      if (normalizeMapKey(filePath) === normalizeMapKey(cwd) || !isUnderDir(filePath, cwd)) {
        outsideRoot++;
        logActionableWarningsEvent({
          event: "lsp_file_skipped",
          sessionId: args.sessionId,
          filePath,
          metadata: { reason: "outside_project_root" }
        });
        continue;
      }
      if (!lspService.supportsLSP(filePath)) {
        logActionableWarningsEvent({
          event: "lsp_file_skipped",
          sessionId: args.sessionId,
          filePath,
          metadata: { reason: "no_lsp_support" }
        });
        continue;
      }
      eligible.push(filePath);
    }
    if (outsideRoot > 0) {
      args.dbg?.(`actionable_warnings: skipped ${outsideRoot} file(s) outside the project root`);
    }
    const capped = eligible.slice(0, fileCap);
    if (capped.length < eligible.length) {
      recordDegradationOnce({
        kind: "actionable-warnings-cap",
        subject: `${cwd}:file-cap`,
        reason: `LSP enrichment capped at ${fileCap} file(s); ${eligible.length - capped.length} modified file(s) were not checked for code actions this turn`
      });
    }
    const primed = [];
    const cold = [];
    for (const filePath of capped) {
      const content = fs12.existsSync(filePath) ? fs12.readFileSync(filePath, "utf-8") : void 0;
      const contentHash = content !== void 0 ? createHash2("sha256").update(content).digest("hex") : void 0;
      const cached = contentHash !== void 0 ? lspService.getLastKnownDiagnostics(filePath, contentHash) : void 0;
      (cached !== void 0 ? primed : cold).push({
        filePath,
        content,
        cached
      });
    }
    const deadline = Date.now() + budgetMs;
    const exhausted = () => args.signal?.aborted === true || Date.now() >= deadline;
    const pullTimeoutMs = boundedNumber(args.lspPullTimeoutMs, ACTIONABLE_WARNINGS_LSP_PULL_TIMEOUT_MS);
    const inBandDeps = {
      lspService,
      pullTimeoutMs,
      // #2504 review round 4 (F2). This is the AWAITED hook: the batch
      // budget has to bound what happens INSIDE a file, not only how many
      // files get started.
      deadlineAt: deadline,
      signal: args.signal,
      // This loop IS on the awaited hook, so its exceedances are turn_end's.
      site: { hook: "turn_end", label: "lspEnrichmentRoundTrip" }
    };
    let unchecked = 0;
    const runInBand = async (targets) => {
      for (const target of targets) {
        if (exhausted()) {
          unchecked += targets.length - targets.indexOf(target);
          break;
        }
        records.push(...await enrichFileFromLsp(cwd, args, target, inBandDeps));
        observedAtByPath.set(normalizeMapKey(target.filePath), (/* @__PURE__ */ new Date()).toISOString());
      }
    };
    await runInBand(primed);
    if (cold.length > 0 && primed.length === 0) {
      const deferredSignal = armDeferredLspWork();
      if (deferredSignal === void 0) {
        incrementDegradationCount({
          kind: "actionable-warnings-cap",
          subject: `${cwd}:deferral-declined`,
          // #2504 review round 4 (F1): the old wording ended "rather than
          // cancel it", which asserted a preservation that did not
          // happen -- the incumbent's report was then discarded whole by
          // the persisted-newer guard. With the per-file merge below it
          // is true, so it now says exactly WHAT survives and what does
          // not.
          reason: `an earlier deferred LSP pull is still running; ${cold.length} file(s) went unchecked for code actions this turn. What IS preserved is the incumbent loop's work: when it lands, its per-file entries are merged into whatever report is persisted then, for every file whose fileSeq has not advanced. This turn's own cold files are the loss, and nothing re-derives them -- the next report is a delta over a different file set`
        });
        logActionableWarningsEvent({
          event: "lsp_pull_deferral_declined",
          sessionId: args.sessionId,
          metadata: { turnIndex: args.turnIndex, files: cold.length }
        });
        args.dbg?.(`actionable_warnings: an earlier deferred LSP pull still holds the slot \u2014 skipping ${cold.length} fresh pull(s) this turn`);
      } else {
        const carried = [...records];
        const deferredArgs = args;
        const loopSignal = combineAbortSignals(args.signal, deferredSignal) ?? deferredSignal;
        const deferredDeps = {
          lspService,
          pullTimeoutMs,
          signal: loopSignal,
          // OFF the hook by construction: this loop is the answer to
          // turn_end's budget, not a spender of it, and it runs on its
          // own ACTIONABLE_WARNINGS_DEFERRED_BUDGET_MS deadline. Charging
          // its exceedances to `turn_end` would make the hook look over
          // budget for having correctly deferred the work (#2557 F3).
          site: {
            hook: "off_hook",
            label: "deferredLspEnrichmentRoundTrip"
          }
        };
        const deferredWork = (async () => {
          await new Promise((resolve30) => setTimeout(resolve30, 0));
          const deferredRecords = [...carried];
          const deferredDeadline = Date.now() + ACTIONABLE_WARNINGS_DEFERRED_BUDGET_MS;
          deferredDeps.deadlineAt = deferredDeadline;
          let deferredUnchecked = 0;
          let abortedMidLoop = false;
          for (const target of cold) {
            if (loopSignal.aborted || Date.now() >= deferredDeadline) {
              abortedMidLoop = loopSignal.aborted;
              deferredUnchecked += cold.length - cold.indexOf(target);
              break;
            }
            deferredRecords.push(...await enrichFileFromLsp(cwd, deferredArgs, target, deferredDeps));
            observedAtByPath.set(normalizeMapKey(target.filePath), (/* @__PURE__ */ new Date()).toISOString());
          }
          if (deferredUnchecked > 0) {
            recordDegradationOnce({
              kind: "actionable-warnings-cap",
              subject: `${cwd}:deferred-budget`,
              reason: `deferred LSP enrichment stopped early; ${deferredUnchecked} file(s) were not checked for code actions`
            });
          }
          if (abortedMidLoop || loopSignal.aborted) {
            logActionableWarningsEvent({
              event: "lsp_pull_aborted",
              sessionId: deferredArgs.sessionId,
              metadata: {
                turnIndex: deferredArgs.turnIndex,
                unchecked: deferredUnchecked
              }
            });
            return;
          }
          deferredArgs.onDeferredReport?.(assembleReport(cwd, deferredArgs, deferredRecords, {
            observedAtByPath,
            fallbackObservedAt: buildStartedAt
          }));
        })().catch((err) => {
          args.dbg?.(`actionable_warnings: deferred LSP pull failed: ${err}`);
        });
        registerDeferredLspWork(deferredSignal, deferredWork);
        logActionableWarningsEvent({
          event: "lsp_pull_deferred",
          sessionId: args.sessionId,
          metadata: { turnIndex: args.turnIndex, files: cold.length }
        });
        args.dbg?.(`actionable_warnings: no LSP cache primed this turn \u2014 deferring ${cold.length} fresh pull(s) off the turn_end hook`);
      }
    } else {
      await runInBand(cold);
    }
    if (unchecked > 0) {
      recordDegradationOnce({
        kind: "actionable-warnings-cap",
        subject: `${cwd}:wall-budget`,
        reason: `LSP enrichment hit its ${Math.round(budgetMs)}ms turn budget; ${unchecked} file(s) were not checked for code actions this turn`
      });
    }
  }
  return assembleReport(cwd, args, records, {
    observedAtByPath,
    fallbackObservedAt: buildStartedAt
  });
}
async function enrichFileFromLsp(cwd, args, target, deps) {
  const { lspService } = deps;
  const { filePath } = target;
  const out = [];
  let diags;
  let lspSource = "cache";
  if (target.cached !== void 0) {
    diags = target.cached;
  } else {
    if (deps.signal?.aborted) {
      logActionableWarningsEvent({
        event: "lsp_file_skipped",
        sessionId: args.sessionId,
        filePath,
        metadata: { reason: "aborted" }
      });
      return out;
    }
    try {
      if (target.content) {
        const opened = await boundedLspCall(async () => {
          await lspService.openFile(filePath, target.content);
          return true;
        }, deps);
        if (opened === void 0) {
          logActionableWarningsEvent({
            event: "lsp_file_skipped",
            sessionId: args.sessionId,
            filePath,
            metadata: {
              reason: deps.signal?.aborted ? "aborted" : "open_timeout",
              pullTimeoutMs: deps.pullTimeoutMs
            }
          });
          return out;
        }
      }
      const pulled = await boundedLspCall(() => lspService.getDiagnostics(filePath), deps);
      if (pulled === void 0) {
        logActionableWarningsEvent({
          event: "lsp_file_skipped",
          sessionId: args.sessionId,
          filePath,
          metadata: {
            reason: deps.signal?.aborted ? "aborted" : "pull_timeout",
            pullTimeoutMs: deps.pullTimeoutMs
          }
        });
        return out;
      }
      diags = pulled;
      lspSource = "fresh";
    } catch (err) {
      args.dbg?.(`actionable_warnings: LSP diagnostics failed for ${filePath}: ${err}`);
      logActionableWarningsEvent({
        event: "lsp_file_skipped",
        sessionId: args.sessionId,
        filePath,
        metadata: { reason: "lsp_error", error: String(err) }
      });
      return out;
    }
  }
  const ranges = args.modifiedRangesByFile.get(normalizeMapKey(filePath)) ?? [];
  const diagsWarning = diags.filter((d) => d.severity === 2);
  let deltaFiltered = 0;
  let enriched = 0;
  let actionPulls = 0;
  let actionCapped = 0;
  let budgetStopped = 0;
  for (const diag of diagsWarning) {
    if (deps.deadlineAt !== void 0 && Date.now() >= deps.deadlineAt) {
      budgetStopped = diagsWarning.length - diagsWarning.indexOf(diag);
      break;
    }
    const line = diag.range.start.line + 1;
    if (args.deltaOnly !== false && !lineInModifiedRanges(line, ranges)) {
      deltaFiltered++;
      continue;
    }
    if (actionPulls >= ACTIONABLE_WARNINGS_MAX_CODE_ACTIONS_PER_FILE) {
      actionCapped++;
      continue;
    }
    actionPulls++;
    const record = recordFromLspDiagnostic(diag, filePath, cwd);
    try {
      const actions = await boundedLspCall(() => lspService.codeAction(filePath, diag.range.start.line, diag.range.start.character, diag.range.end.line, diag.range.end.character), deps);
      record.actions = (actions ?? []).map(serializeAction).slice(0, 5);
    } catch (err) {
      args.dbg?.(`actionable_warnings: LSP codeAction failed for ${filePath}: ${err}`);
    }
    if (record.actions.length > 0) {
      out.push(record);
      enriched++;
    }
  }
  if (actionCapped > 0) {
    incrementDegradationCount({
      kind: "actionable-warnings-cap",
      subject: `${cwd}:code-action-fanout`,
      // #2504 review round 4 (S-2): "reported without fix actions" was
      // wrong in both halves. A capped warning is skipped BEFORE its record
      // is built, and a record with no action is dropped below anyway, so
      // it is not reported at all -- it never reaches the agent on this
      // channel.
      reason: `a file exceeded the ${ACTIONABLE_WARNINGS_MAX_CODE_ACTIONS_PER_FILE}-warning code-action cap; ${actionCapped} warning(s) in ${toRunnerDisplayPath(cwd, filePath)} were NOT reported this turn`
    });
  }
  if (budgetStopped > 0) {
    incrementDegradationCount({
      kind: "actionable-warnings-cap",
      subject: `${cwd}:in-file-budget`,
      reason: `the LSP enrichment wall budget expired part-way through a file; ${budgetStopped} warning(s) in ${toRunnerDisplayPath(cwd, filePath)} were NOT checked for fix actions`
    });
  }
  logActionableWarningsEvent({
    event: "lsp_file_checked",
    sessionId: args.sessionId,
    filePath,
    metadata: {
      diagsTotal: diags.length,
      diagsWarning: diagsWarning.length,
      deltaFiltered,
      enriched,
      actionCapped,
      budgetStopped,
      modifiedRangesCount: ranges.length,
      lspSource
    }
  });
  return out;
}
function assembleReport(cwd, args, records, stamps) {
  const merged = mergeWarnings(records);
  updateWarningState(cwd, merged);
  const reportWarnings = merged.map(({ legacyId: _legacyId, ...rest }) => rest);
  const byFile = /* @__PURE__ */ new Map();
  for (const warning of reportWarnings) {
    const arr = byFile.get(warning.filePath) ?? [];
    arr.push(warning);
    byFile.set(warning.filePath, arr);
  }
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
  const files = [...byFile.entries()].map(([filePath, warnings]) => ({
    filePath,
    displayPath: toRunnerDisplayPath(cwd, filePath),
    fileSeq: args.fileSeqByPath?.get(normalizeMapKey(filePath)),
    // #2504 review round 5 (F2): the moment THIS file was observed. The
    // report-level stamp remains the assembly moment and is only the
    // last-resort fallback, for a caller that supplied no observations
    // at all.
    generatedAt: stamps?.observedAtByPath.get(normalizeMapKey(filePath)) ?? stamps?.fallbackObservedAt ?? generatedAt,
    branchEpoch: args.branchEpoch,
    branchScope: args.branchScope,
    warnings
  }));
  const summary = summarizeReportFiles(files);
  logActionableWarningsEvent({
    event: "report_complete",
    sessionId: args.sessionId,
    metadata: { turnIndex: args.turnIndex, summary }
  });
  return {
    generatedAt,
    scope: "turn_delta",
    sessionId: args.sessionId,
    turnIndex: args.turnIndex,
    projectSeqStart: args.projectSeqStart,
    projectSeqEnd: args.projectSeqEnd,
    deltaOnly: args.deltaOnly !== false,
    includeLspCodeActions: args.includeLspCodeActions,
    files,
    summary
  };
}
function summarizeReportFiles(files) {
  const warnings = files.flatMap((file) => file.warnings);
  const unsuppressed = warnings.filter((warning) => !warning.suppressed);
  const allActions = warnings.flatMap((warning) => warning.actions);
  const countTier = (tier) => unsuppressed.filter((warning) => warning.severity === tier).length;
  return {
    warnings: warnings.length,
    unsuppressed: unsuppressed.length,
    byTier: {
      warning: countTier("warning"),
      info: countTier("info"),
      hint: countTier("hint")
    },
    suppressed: warnings.filter((warning) => warning.suppressed).length,
    files: files.length,
    actions: allActions.length,
    autoFixEligible: allActions.filter((action) => action.autoFixEligible).length
  };
}
function writeActionableWarningsReport(cacheManager2, cwd, report) {
  cacheManager2.writeCache("actionable-warnings", report, cwd);
}
function mergeActionableWarningsReports(args) {
  const { persisted, incoming, origin } = args;
  const deferredPublish = origin === "deferred";
  const sessionMismatch = !deferredPublish && persisted?.sessionId !== void 0 && persisted.sessionId !== incoming.sessionId;
  const newerHalf = deferredPublish ? persisted?.files ?? [] : incoming.files;
  const olderHalf = deferredPublish ? incoming.files : persisted?.files ?? [];
  const newerReport = deferredPublish ? persisted : incoming;
  const olderReport = deferredPublish ? incoming : persisted;
  const byPath = /* @__PURE__ */ new Map();
  for (const entry of newerHalf) {
    byPath.set(normalizeMapKey(entry.filePath), entry);
  }
  const droppedFiles = [];
  let mergedFiles = 0;
  for (const entry of olderHalf) {
    if (!deferredPublish && entry.origin !== "deferred") {
      args.dbg?.(`actionable_warnings: in-band publish dropped ${entry.displayPath || entry.filePath} -- its carry-forward window (from an earlier deferred publish) already closed`);
      continue;
    }
    const key = normalizeMapKey(entry.filePath);
    const incumbent = byPath.get(key);
    const liveBaseline = args.getFileSeq !== void 0;
    const baselineSeq = args.getFileSeq?.(entry.filePath) ?? incumbent?.fileSeq;
    const stale = !deferredPublish && liveBaseline ? sessionMismatch || typeof entry.fileSeq === "number" && baselineSeq !== entry.fileSeq : typeof baselineSeq === "number" && typeof entry.fileSeq === "number" && baselineSeq > entry.fileSeq;
    if (stale) {
      droppedFiles.push(entry.displayPath || entry.filePath);
      continue;
    }
    mergedFiles++;
    const merged = incumbent ? incumbent.reVerified || incumbent.reVerifyIncomplete ? (
      // #3170: the runtime just RE-OBSERVED this file (the bounded
      // persistent-reverify pass) — the fresh observation supersedes
      // the carried entry instead of unioning with it: a converged
      // finding would otherwise survive its own supersession via the
      // id-union in `mergeWarnings`.
      {
        ...incumbent,
        fileSeq: incumbent.fileSeq ?? entry.fileSeq,
        generatedAt: olderStamp(incumbent.generatedAt ?? newerReport?.generatedAt, entry.generatedAt ?? olderReport?.generatedAt),
        warnings: incumbent.warnings
      }
    ) : {
      ...incumbent,
      fileSeq: incumbent.fileSeq ?? entry.fileSeq,
      generatedAt: olderStamp(incumbent.generatedAt ?? newerReport?.generatedAt, entry.generatedAt ?? olderReport?.generatedAt),
      branchEpoch: olderBranchEpoch(incumbent.branchEpoch, entry.branchEpoch),
      branchScope: incumbent.branchScope === entry.branchScope ? incumbent.branchScope : void 0,
      warnings: mergeWarnings([...incumbent.warnings, ...entry.warnings])
    } : { ...entry };
    if (deferredPublish) {
      merged.origin = "deferred";
    } else {
      merged.origin = void 0;
    }
    byPath.set(key, merged);
  }
  const files = [...byPath.values()];
  if (!deferredPublish) {
    return {
      report: { ...incoming, files, summary: summarizeReportFiles(files) },
      mergedFiles,
      droppedFiles,
      // sessionMismatch is a single flag for the whole call, not per-entry:
      // once true, every stale check above is forced true by the `||`, so
      // EITHER every drop in this batch is a session-boundary drop, OR none
      // are (droppedFiles is then purely fileSeq-mismatch drops).
      droppedForSessionMismatch: sessionMismatch && droppedFiles.length > 0
    };
  }
  const base = persisted ?? incoming;
  return {
    report: {
      ...base,
      // The report-level stamp is only the fallback for entries with none
      // of their own (a cache file from an older build), so it takes the
      // NEWER of the two; per-entry stamps carry the real ages.
      generatedAt: newerStamp(persisted?.generatedAt, incoming.generatedAt) ?? base.generatedAt,
      turnIndex: Math.max(base.turnIndex, incoming.turnIndex),
      projectSeqStart: minDefined(persisted?.projectSeqStart, incoming.projectSeqStart),
      // max, so a merged report never claims to be fresher OR staler than
      // the newest part it holds. checkActionableWarningsReportFresh still
      // demands exact equality with the live projectSeq and re-checks every
      // entry's fileSeq, so this widens nothing.
      projectSeqEnd: maxDefined(persisted?.projectSeqEnd, incoming.projectSeqEnd),
      includeLspCodeActions: base.includeLspCodeActions || incoming.includeLspCodeActions,
      files,
      summary: summarizeReportFiles(files)
    },
    mergedFiles,
    droppedFiles,
    // The deferred-publish path never sets sessionMismatch (gated by
    // `!deferredPublish` above), so its drops are always fileSeq-mismatch.
    droppedForSessionMismatch: false
  };
}
function publishActionableWarningsReport(cacheManager2, cwd, report, opts = {}) {
  const origin = opts.origin ?? "in-band";
  const persisted = cacheManager2.readCache("actionable-warnings", cwd, Number.MAX_SAFE_INTEGER)?.data;
  const merged = mergeActionableWarningsReports({
    persisted,
    incoming: report,
    origin,
    getFileSeq: opts.getFileSeq,
    dbg: opts.dbg
  });
  writeActionableWarningsReport(cacheManager2, cwd, merged.report);
  appendActionableWarningsHistory(cwd, report);
  if (merged.mergedFiles > 0) {
    opts.dbg?.(`actionable_warnings: ${origin} publish merged ${merged.mergedFiles} persisted file entry/entries`);
  }
  if (origin === "in-band" && merged.droppedFiles.length > 0) {
    const cause = merged.droppedForSessionMismatch ? "the publish crossed a session boundary" : "changed before this turn's in-band publish could keep them";
    const reason = `${merged.droppedFiles.length} carried-forward entries: findings are LOST on this channel; ${cause} (files: ${merged.droppedFiles.slice(0, 3).join(", ")}${merged.droppedFiles.length > 3 ? ", ..." : ""})`;
    incrementDegradationCount({
      kind: "actionable-warnings-inband-superseded",
      subject: `${path14.resolve(cwd)}:inband-carry-superseded`,
      reason
    });
    opts.dbg?.(`actionable_warnings: in-band publish dropped ${merged.droppedFiles.length} superseded carried-forward file entry/entries (${merged.droppedFiles.slice(0, 3).join(", ")}${merged.droppedFiles.length > 3 ? ", ..." : ""})`);
  }
  return merged;
}
function olderStamp(a, b) {
  if (a === void 0)
    return b;
  if (b === void 0)
    return a;
  const at = Date.parse(a);
  const bt = Date.parse(b);
  if (!Number.isFinite(at))
    return b;
  if (!Number.isFinite(bt))
    return a;
  return at <= bt ? a : b;
}
function newerStamp(a, b) {
  const older = olderStamp(a, b);
  if (a === void 0)
    return b;
  if (b === void 0)
    return a;
  return older === a ? b : a;
}
function olderBranchEpoch(a, b) {
  if (typeof a !== "number" || typeof b !== "number")
    return void 0;
  return Math.min(a, b);
}
function minDefined(a, b) {
  if (typeof a !== "number")
    return b;
  if (typeof b !== "number")
    return a;
  return Math.min(a, b);
}
function maxDefined(a, b) {
  if (typeof a !== "number")
    return b;
  if (typeof b !== "number")
    return a;
  return Math.max(a, b);
}
function writeDeferredActionableWarningsReport(args) {
  const { mergedFiles, droppedFiles } = publishActionableWarningsReport(args.cacheManager, args.cwd, args.report, { origin: "deferred", getFileSeq: args.getFileSeq, dbg: args.dbg });
  if (droppedFiles.length > 0) {
    incrementDegradationCount({
      kind: "actionable-warnings-deferred-superseded",
      subject: `${path14.resolve(args.cwd)}:deferred-file-superseded`,
      reason: `${droppedFiles.length} changed file(s): warnings are LOST; unchanged files were merged into the persisted report (files: ${droppedFiles.slice(0, 3).join(", ")}${droppedFiles.length > 3 ? ", ..." : ""})`
    });
    args.dbg?.(`turn_end: deferred actionable-warnings dropped ${droppedFiles.length} superseded file entry/entries`);
  }
  args.dbg?.(`turn_end: deferred actionable-warnings merged ${mergedFiles} file entry/entries into the persisted report`);
  return { mergedFiles, droppedFiles: droppedFiles.length };
}
function getActionableWarningsHistoryPath(cwd) {
  return path14.join(getProjectDataDir(cwd), "actionable-warnings.jsonl");
}
function appendActionableWarningsHistory(cwd, report) {
  const entries = [];
  for (const file of report.files) {
    if (file.reVerifyIncomplete)
      continue;
    for (const warning of file.warnings) {
      entries.push({
        timestamp: report.generatedAt,
        sessionId: report.sessionId,
        turnIndex: report.turnIndex,
        projectSeq: report.projectSeqEnd,
        filePath: warning.filePath,
        displayPath: warning.displayPath,
        fileSeq: file.fileSeq,
        line: warning.line,
        column: warning.column,
        severity: warning.severity,
        tool: warning.tool,
        source: warning.source,
        rule: warning.rule,
        code: warning.code,
        message: warning.message,
        fixKind: warning.fixKind,
        autoFixAvailable: warning.autoFixAvailable,
        actionCount: warning.actions.length,
        autoFixEligibleActionCount: warning.actions.filter((action) => action.autoFixEligible).length,
        suppressed: warning.suppressed,
        suppressionReason: warning.suppressionReason,
        origin: warning.origin,
        warningId: warning.id
      });
    }
  }
  if (entries.length === 0)
    return;
  const historyPath = getActionableWarningsHistoryPath(cwd);
  try {
    fs12.mkdirSync(path14.dirname(historyPath), { recursive: true });
    fs12.appendFileSync(historyPath, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}
`, "utf8");
  } catch {
  }
}
function formatActionableWarningsAdvisory(report, cwd, host = "pi", filterBuiltReport) {
  const filtered = filterBuiltReport?.(report);
  const advisoryReport = filtered ? {
    ...report,
    files: filtered.files,
    summary: summarizeReportFiles(filtered.files)
  } : report;
  if (advisoryReport.summary.unsuppressed === 0)
    return void 0;
  const files = advisoryReport.files.filter((file) => file.warnings.some((warning) => !warning.suppressed));
  const fileList = files.slice(0, 5).map((file) => `  ${file.displayPath}: ${file.warnings.filter((warning) => !warning.suppressed).length}${file.reVerifyIncomplete ? " (re-verify incomplete)" : ""}`).join("\n");
  const more = files.length > 5 ? `
  ... and ${files.length - 5} more file(s)` : "";
  const safe = advisoryReport.summary.autoFixEligible > 0 ? ` ${advisoryReport.summary.autoFixEligible} appear to have conservative preferred quickfixes.` : "";
  const byTier = advisoryReport.summary.byTier;
  const quiet = byTier ? byTier.hint + byTier.info : 0;
  const tierLine = quiet > 0 ? `${quiet} of those are hint/info tier \u2014 style opinions, worth fixing only while you are already in that code.` : void 0;
  const reportPath = displayProjectDataPath(cwd, "cache", "actionable-warnings.json");
  const diagnosticsTool = resolveLensToolName("lens_diagnostics", host);
  return [
    `\u{1F7E1} Fixable warnings introduced this turn: ${advisoryReport.summary.unsuppressed}.${safe}`,
    filtered && filtered.suppressed > 0 ? `suppressed by disposition: ${filtered.suppressed} finding(s).` : void 0,
    tierLine,
    diagnosticsTool ? `Use ${diagnosticsTool} with mode=delta to inspect these warnings.` : void 0,
    fileList ? `Files:
${fileList}${more}` : void 0,
    "If continuing in these files, resolve warnings that are safe and relevant. Do not apply broad refactors unless requested.",
    `Raw report (only if you need the JSON): ${reportPath}`
  ].filter(Boolean).join("\n");
}

// dist/clients/code-quality-warnings.js
import * as fs13 from "node:fs";
import * as path15 from "node:path";
var TIER_BUDGET_ORDER = [
  "warning",
  "info",
  "hint"
];
function lineInModifiedRanges2(line, ranges) {
  if (line === void 0)
    return true;
  if (ranges.length === 0)
    return true;
  return ranges.some((range) => line >= range.start - 2 && line <= range.end + 2);
}
function buildCodeQualityWarningsReport(args) {
  const cwd = path15.resolve(args.cwd);
  const maxWarnings = Math.max(1, args.maxWarnings ?? 50);
  const byId = /* @__PURE__ */ new Map();
  for (const warning of args.warnings) {
    const ranges = args.modifiedRangesByFile.get(normalizeMapKey(warning.filePath)) ?? [];
    if (!lineInModifiedRanges2(warning.line, ranges))
      continue;
    byId.set(warning.id, warning);
  }
  const byDisplayOrder = (a, b) => a.displayPath.localeCompare(b.displayPath) || (a.line ?? 0) - (b.line ?? 0) || a.message.localeCompare(b.message);
  const candidates = [...byId.values()].sort(byDisplayOrder);
  const merged = [];
  for (const tier of TIER_BUDGET_ORDER) {
    for (const warning of candidates) {
      if (merged.length >= maxWarnings)
        break;
      if (warning.severity === tier)
        merged.push(warning);
    }
  }
  merged.sort(byDisplayOrder);
  const byFile = /* @__PURE__ */ new Map();
  for (const warning of merged) {
    const arr = byFile.get(warning.filePath) ?? [];
    arr.push(warning);
    byFile.set(warning.filePath, arr);
  }
  const files = [...byFile.entries()].map(([filePath, warnings]) => ({
    filePath,
    displayPath: toRunnerDisplayPath(cwd, filePath),
    fileSeq: args.fileSeqByPath?.get(normalizeMapKey(filePath)),
    warnings
  }));
  const ruleCounts = /* @__PURE__ */ new Map();
  for (const warning of merged) {
    const rule = warning.rule ?? warning.tool;
    ruleCounts.set(rule, (ruleCounts.get(rule) ?? 0) + 1);
  }
  const topRules = [...ruleCounts.entries()].map(([rule, count]) => ({ rule, count })).sort((a, b) => b.count - a.count || a.rule.localeCompare(b.rule)).slice(0, 8);
  return {
    generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
    scope: "turn_delta",
    sessionId: args.sessionId,
    turnIndex: args.turnIndex,
    projectSeqStart: args.projectSeqStart,
    projectSeqEnd: args.projectSeqEnd,
    deltaOnly: true,
    files,
    summary: {
      warnings: merged.length,
      files: files.length,
      byTier: {
        warning: merged.filter((w) => w.severity === "warning").length,
        info: merged.filter((w) => w.severity === "info").length,
        hint: merged.filter((w) => w.severity === "hint").length
      },
      topRules
    }
  };
}
function writeCodeQualityWarningsReport(cacheManager2, cwd, report) {
  cacheManager2.writeCache("code-quality-warnings", report, cwd);
}
function getCodeQualityWarningsHistoryPath(cwd) {
  return path15.join(getProjectDataDir(cwd), "code-quality-warnings.jsonl");
}
function appendCodeQualityWarningsHistory(cwd, report) {
  const warnings = report.files.flatMap((file) => file.warnings.map((warning) => ({
    timestamp: report.generatedAt,
    sessionId: report.sessionId,
    turnIndex: report.turnIndex,
    projectSeq: report.projectSeqEnd,
    filePath: warning.filePath,
    displayPath: warning.displayPath,
    fileSeq: file.fileSeq,
    line: warning.line,
    column: warning.column,
    severity: warning.severity,
    tool: warning.tool,
    rule: warning.rule,
    code: warning.code,
    message: warning.message,
    category: warning.category,
    warningId: warning.id
  })));
  if (warnings.length === 0)
    return;
  const historyPath = getCodeQualityWarningsHistoryPath(cwd);
  try {
    fs13.mkdirSync(path15.dirname(historyPath), { recursive: true });
    fs13.appendFileSync(historyPath, `${warnings.map((entry) => JSON.stringify(entry)).join("\n")}
`, "utf8");
  } catch {
  }
}
function formatCodeQualityWarningsAdvisory(report, cwd, host = "pi") {
  if (report.summary.warnings === 0)
    return void 0;
  const topRules = report.summary.topRules.slice(0, 3).map((entry) => `${entry.rule}\xD7${entry.count}`).join(", ");
  const byTier = report.summary.byTier ?? { warning: 0, info: 0, hint: 0 };
  const tiers = byTier.hint || byTier.info ? [
    byTier.warning ? `${byTier.warning} warning` : void 0,
    byTier.info ? `${byTier.info} info` : void 0,
    byTier.hint ? `${byTier.hint} hint` : void 0
  ].filter(Boolean).join(", ") : "";
  const diagnosticsTool = resolveLensToolName("lens_diagnostics", host);
  return [
    `Code-quality warnings introduced/touched this turn: ${report.summary.warnings} across ${report.summary.files} file(s).`,
    tiers ? `By tier: ${tiers}. Hint and info are style opinions, not defects.` : void 0,
    topRules ? `Top rules: ${topRules}` : void 0,
    // #2521: tool route first (`mode=delta` covers the code-quality cache
    // as well as the actionable one), resolved path second.
    // #2535 F3: omit when unmapped on this host, never name a dead tool
    // (pinned available on both hosts; defensive only).
    diagnosticsTool ? `Use ${diagnosticsTool} with mode=delta to inspect these warnings.` : void 0,
    "No action required unless you are already refactoring these areas.",
    `Raw report (only if you need the JSON): ${displayProjectDataPath(cwd, "cache", "code-quality-warnings.json")}`
  ].filter(Boolean).join("\n");
}

// dist/clients/git-guard.js
import * as path16 from "node:path";

// dist/clients/read-guard-tool-lines.js
var MAX_FAILURE_TRACKER_SIZE = 200;
var recentOldTextFailures = new BoundedFifoMap(MAX_FAILURE_TRACKER_SIZE);

// dist/clients/deferred-runner-blockers.js
import { createHash as createHash3 } from "node:crypto";
import * as fs14 from "node:fs";
function judgeDeferredRunnerFindings(pending2, cwd, options = {}) {
  const findings = pending2.result?.diagnostics ?? [];
  if (findings.length === 0) {
    return { stale: 0, live: 0, kept: [], suppressed: 0, bytes: void 0 };
  }
  const { "late-runner-findings": gate3 } = gateFindingsByPathFreshness({
    cwd,
    sources: {
      "late-runner-findings": {
        findings,
        scannedAt: pending2.markedAtMs,
        citedPath: (finding) => finding.filePath
      }
    },
    ...options.quiet === true ? { quiet: true } : {}
  });
  if (gate3.live.length === 0) {
    return {
      stale: gate3.stale.length,
      live: 0,
      kept: [],
      suppressed: 0,
      bytes: void 0
    };
  }
  const bytes = readBytes(pending2.filePath);
  const { kept, suppressed } = applyPushedFindingPolicy(gate3.live, {
    cwd,
    filePath: pending2.filePath,
    content: bytes?.toString("utf-8")
  });
  return {
    stale: gate3.stale.length,
    live: gate3.live.length,
    kept,
    suppressed,
    bytes
  };
}
function recordDeferredRunnerBlockers(runtime, pending2, survivors, bytes) {
  const blocking = survivors.flatMap((d) => d.semantic === "blocking" ? [d] : []);
  if (blocking.length === 0)
    return 0;
  const recorded = runtime.recordDeferredInlineBlockers(pending2.filePath, blocking, {
    recordedAtMs: pending2.markedAtMs,
    ...bytes ? {
      contentBaseline: {
        size: bytes.byteLength,
        sha256: createHash3("sha256").update(bytes).digest("hex")
      }
    } : {}
  });
  if (recorded > 0)
    runtime.updateGitGuardStatus(false, "");
  return recorded;
}
function readBytes(filePath) {
  try {
    return fs14.readFileSync(filePath);
  } catch {
    return void 0;
  }
}

// dist/clients/git-guard.js
function resolveGuardPath(filePath, cwd) {
  return path16.resolve(cwd, filePath);
}
function guardPathKey(filePath, cwd) {
  return advisoryPathKey(filePath, cwd);
}
function capAffectedFiles(files, cwd) {
  const unique = [...new Set(files.map((file) => resolveGuardPath(file, cwd)))];
  return {
    files: unique.slice(0, MAX_ADVISORY_AFFECTED_FILES),
    truncated: unique.length > MAX_ADVISORY_AFFECTED_FILES
  };
}
var TURN_END_BLOCKER_SECTION = /^Unresolved from this turn — ([^\s].*?)(?: \(suppressed by disposition: \d+ finding\(s\)\))?:$/;
function blockerSectionFile(line) {
  const turnEndSection = TURN_END_BLOCKER_SECTION.exec(line);
  if (turnEndSection)
    return turnEndSection[1];
  if (/^\s/.test(line))
    return void 0;
  const separator = line.indexOf(": ");
  if (separator <= 0)
    return void 0;
  return line.slice(0, separator);
}
function blockingProvenance(blockerContent, blockingFiles, cwd) {
  if (typeof blockerContent !== "string" || blockerContent.length === 0)
    return void 0;
  if (!Array.isArray(blockingFiles))
    return void 0;
  if (blockingFiles.some((file) => typeof file !== "string" || file.trim().length === 0)) {
    return void 0;
  }
  const provenanceKeys = blockingFiles.map((file) => guardPathKey(file, cwd));
  const sectionKeys = [];
  let inSection = false;
  for (const line of blockerContent.split("\n")) {
    if (line.length === 0) {
      inSection = false;
      continue;
    }
    const file = blockerSectionFile(line);
    if (file === void 0) {
      if (!inSection)
        return void 0;
      continue;
    }
    sectionKeys.push(guardPathKey(file, cwd));
    inSection = true;
  }
  if (sectionKeys.length === 0)
    return void 0;
  return sectionKeys.every((key) => provenanceKeys.includes(key)) ? sectionKeys : void 0;
}
function isTurnEndFindingsCache(value) {
  if (!value || typeof value !== "object")
    return false;
  const record = value;
  return typeof record.content === "string" && typeof record.hasBlockers === "boolean" && Array.isArray(record.affectedFiles) && record.affectedFiles.every((file) => typeof file === "string") && typeof record.sessionId === "string" && typeof record.projectSeqStart === "number" && typeof record.projectSeqEnd === "number" && Number.isFinite(record.projectSeqStart) && Number.isFinite(record.projectSeqEnd) && !!record.fileSeqByPath && typeof record.fileSeqByPath === "object" && Object.values(record.fileSeqByPath).every((seq) => typeof seq === "number" && Number.isFinite(seq)) && !!record.fileContentHashes && typeof record.fileContentHashes === "object" && Object.values(record.fileContentHashes).every((hash) => typeof hash === "string") && (record.affectedFilesTruncated === void 0 || typeof record.affectedFilesTruncated === "boolean") && (record.blockingFiles === void 0 || Array.isArray(record.blockingFiles));
}
function cacheRecord(cacheManager2, cwd) {
  const entry = cacheManager2.readCache("turn-end-findings", cwd);
  return entry && isTurnEndFindingsCache(entry.data) ? entry.data : void 0;
}
function markCacheUnknown(runtime, reason) {
  runtime.markGitGuardCacheUnknown(reason);
}
function writeGitGuardRecord(cacheManager2, runtime, cwd, record) {
  const capped = capAffectedFiles(Array.isArray(record.affectedFiles) ? record.affectedFiles : [], cwd);
  const fileSeqByPath = { ...record.fileSeqByPath };
  for (const file of capped.files) {
    const key = guardPathKey(file, cwd);
    if (fileSeqByPath[key] === void 0) {
      fileSeqByPath[key] = runtime.getFileSeq(resolveGuardPath(file, cwd));
    }
  }
  const currentProvenance = snapshotAdvisoryProvenance({
    cwd,
    runtime,
    generation: 0,
    files: capped.files.map((file) => ({
      path: file,
      role: "affected"
    })),
    truncated: capped.truncated
  });
  const fileContentHashes = Object.fromEntries(currentProvenance.files.map((file) => [
    guardPathKey(file.path, cwd),
    file.sha256
  ]));
  const data = {
    ...record,
    affectedFiles: capped.files,
    affectedFilesTruncated: capped.truncated,
    fileSeqByPath,
    blockingFiles: Array.isArray(record.blockingFiles) ? capAffectedFiles(record.blockingFiles, cwd).files : void 0,
    fileContentHashes,
    provenance: record.provenance ?? currentProvenance
  };
  try {
    cacheManager2.writeCache("turn-end-findings", data, cwd);
    runtime.clearGitGuardCacheUnknown();
    return true;
  } catch {
    markCacheUnknown(runtime, "cache_write_failed");
    return false;
  }
}
function retireInlineBlockerAndResyncGuard(args) {
  const retired = args.runtime.retireInlineBlockerOnConfirmedClean(args.filePath, args.writeIndex, args.coveredSources);
  if (!retired)
    return false;
  args.runtime.updateGitGuardStatus(false, "");
  if (args.lensGuardEnabled) {
    syncGitGuardRecord(args.runtime, args.cacheManager, args.cwd, args.filePath);
  }
  return true;
}
function resyncGitGuardAfterInlinePolicy(args) {
  const changed = args.runtime.applyInlineBlockerPolicyVerdicts(args.suppressedFilePaths);
  if (args.suppressedFilePaths.length === 0 && changed === 0)
    return;
  args.runtime.updateGitGuardStatus(false, "");
}
function syncGitGuardRecord(runtime, cacheManager2, cwd, editedFilePath) {
  const entries = (runtime.getInlineBlockersSnapshot?.() ?? []).filter((entry) => !entry.policySuppressed);
  const inspection = cacheManager2.inspectCache("turn-end-findings", cwd);
  const existing = cacheRecord(cacheManager2, cwd);
  if (existing && existing.sessionId !== runtime.telemetrySessionId && entries.length === 0) {
    markCacheUnknown(runtime, "session_mismatch");
    return;
  }
  if (!existing && inspection !== "missing" && entries.length === 0) {
    markCacheUnknown(runtime, `cache_${inspection}`);
    return;
  }
  const fileSeqByPath = {};
  for (const [filePath, seq] of runtime.getFileSeqEntries?.() ?? []) {
    fileSeqByPath[guardPathKey(filePath, cwd)] = seq;
  }
  const inlineFiles = entries.map((entry) => resolveGuardPath(entry.filePath, cwd));
  const existingBlockingFiles = existing?.blockingFiles ?? [];
  const blockerSections = existing?.blockerContent ? blockingProvenance(existing.blockerContent, existingBlockingFiles, cwd) : void 0;
  if (existing?.blockerContent && !blockerSections) {
    markCacheUnknown(runtime, "blocking_provenance_untrusted");
    return;
  }
  const editedKey = editedFilePath ? guardPathKey(editedFilePath, cwd) : void 0;
  const testFiles = existing?.testFailureFiles ?? [];
  const remainingBlockingFiles = editedKey && existingBlockingFiles.length > 0 ? existingBlockingFiles.filter((file) => guardPathKey(file, cwd) !== editedKey) : existingBlockingFiles;
  let affectedFiles = [...existing?.affectedFiles ?? [], ...inlineFiles];
  if (!entries.length && editedKey && existing) {
    const isStillTestFailure = testFiles.some((file) => guardPathKey(file, cwd) === editedKey);
    const isUnknownBlockingPath = existingBlockingFiles.length === 0 && !!existing.blockerContent;
    if (!isStillTestFailure && !isUnknownBlockingPath) {
      affectedFiles = affectedFiles.filter((file) => guardPathKey(file, cwd) !== editedKey);
    }
  }
  const clearedLastKnownBlocker = !entries.length && !!editedKey && !!blockerSections && blockerSections.every((key) => key === editedKey);
  const blockerContent = entries.length > 0 ? entries.map((entry) => `${entry.filePath}: ${entry.summary}`).join("\n") : clearedLastKnownBlocker ? void 0 : existing?.blockerContent;
  const hasTestFailures = existing?.testFailures === true;
  const hasBlockers = !!blockerContent || hasTestFailures;
  const content = [
    blockerContent,
    hasTestFailures ? existing?.testFailureContent : void 0
  ].filter((value) => !!value).join("\n\n");
  if (!hasBlockers && !content) {
    cacheManager2.clearCache("turn-end-findings", cwd);
    runtime.clearGitGuardCacheUnknown();
    return;
  }
  writeGitGuardRecord(cacheManager2, runtime, cwd, {
    content: content || existing?.content || "",
    blockerContent,
    blockingFiles: entries.length > 0 ? inlineFiles : remainingBlockingFiles,
    hasBlockers,
    affectedFiles,
    sessionId: runtime.telemetrySessionId,
    projectSeqStart: runtime.turnStartProjectSeq,
    projectSeqEnd: runtime.projectSeq,
    fileSeqByPath,
    fileContentHashes: {},
    consumed: false,
    testFailures: existing?.testFailures,
    testFailureContent: existing?.testFailureContent,
    testFailureFiles: existing?.testFailureFiles
  });
}
function mergeGitGuardTestFailure(cacheManager2, cwd, runtime, content, files) {
  const existing = cacheRecord(cacheManager2, cwd);
  const fileSeqByPath = {};
  for (const [filePath, seq] of runtime.getFileSeqEntries?.() ?? []) {
    fileSeqByPath[guardPathKey(filePath, cwd)] = seq;
  }
  const failedFiles = files.map((file) => resolveGuardPath(file, cwd));
  const blockerContent = existing?.blockerContent;
  const testFailureFiles = [
    ...existing?.testFailureFiles ?? [],
    ...failedFiles
  ].filter((file, index, all) => all.findIndex((candidate) => guardPathKey(candidate, cwd) === guardPathKey(file, cwd)) === index);
  writeGitGuardRecord(cacheManager2, runtime, cwd, {
    content: [blockerContent, content].filter(Boolean).join("\n\n"),
    blockerContent,
    blockingFiles: existing?.blockingFiles ?? [],
    testFailureContent: content,
    testFailureFiles,
    hasBlockers: true,
    affectedFiles: [...existing?.affectedFiles ?? [], ...failedFiles],
    sessionId: runtime.telemetrySessionId,
    projectSeqStart: runtime.turnStartProjectSeq,
    projectSeqEnd: runtime.projectSeq,
    fileSeqByPath,
    fileContentHashes: {},
    consumed: false,
    testFailures: true
  });
}
function clearGitGuardTestFailure(cacheManager2, cwd, runtime, passedFiles = []) {
  const existing = cacheRecord(cacheManager2, cwd);
  if (!existing?.testFailures)
    return;
  const passedKeys = new Set((passedFiles.length > 0 ? passedFiles : existing.testFailureFiles ?? []).map((file) => guardPathKey(file, cwd)));
  const remainingFiles = (existing.testFailureFiles ?? []).filter((file) => !passedKeys.has(guardPathKey(file, cwd)));
  const blockerContent = existing.blockerContent ?? "";
  if (!blockerContent && remainingFiles.length === 0) {
    cacheManager2.clearCache("turn-end-findings", cwd);
    return;
  }
  const blockingKeys = new Set((existing.blockingFiles ?? []).map((file) => guardPathKey(file, cwd)));
  const affectedFiles = existing.affectedFiles.filter((file) => blockingKeys.has(guardPathKey(file, cwd)) || remainingFiles.some((testFile) => guardPathKey(testFile, cwd) === guardPathKey(file, cwd)));
  writeGitGuardRecord(cacheManager2, runtime, cwd, {
    ...existing,
    content: blockerContent,
    blockerContent,
    affectedFiles,
    testFailures: remainingFiles.length > 0,
    testFailureContent: remainingFiles.length > 0 ? existing.testFailureContent : void 0,
    testFailureFiles: remainingFiles.length > 0 ? remainingFiles : void 0,
    hasBlockers: !!blockerContent || remainingFiles.length > 0,
    sessionId: runtime.telemetrySessionId,
    projectSeqStart: runtime.turnStartProjectSeq,
    projectSeqEnd: runtime.projectSeq,
    fileSeqByPath: Object.fromEntries(runtime.getFileSeqEntries().map(([filePath, seq]) => [guardPathKey(filePath, cwd), seq])),
    fileContentHashes: {}
  });
}

// dist/clients/secret-findings.js
var SECRET_RULE_PATTERN = /hardcoded|secret|password|credential/i;
function isSecretWarning(warning) {
  const id = `${warning.rule ?? ""} ${warning.code ?? ""}`;
  return SECRET_RULE_PATTERN.test(id);
}
function secretLocationKey(file, line) {
  return `${normalizeFilePath(file)}:${line}`;
}
function fromGitleaks(findings) {
  return findings.map((f) => ({
    file: f.file,
    line: f.startLine,
    sources: ["gitleaks"],
    rule: f.ruleId,
    description: f.description
  }));
}
function fromTrivySecrets(findings) {
  return findings.map((f) => ({
    file: f.file,
    line: f.line,
    sources: ["trivy"],
    rule: f.ruleId,
    description: f.title
  }));
}
function fromAstGrepWarnings(warnings) {
  const out = [];
  for (const w of warnings) {
    if (!isSecretWarning(w) || typeof w.line !== "number")
      continue;
    out.push({
      file: w.filePath,
      line: w.line,
      sources: ["ast-grep"],
      rule: w.rule ?? w.code ?? "hardcoded-secret",
      description: w.message
    });
  }
  return out;
}
var SOURCE_PRIORITY = {
  gitleaks: 0,
  trivy: 1,
  "ast-grep": 2
};
function sortSources(sources) {
  return [...new Set(sources)].sort((a, b) => SOURCE_PRIORITY[a] - SOURCE_PRIORITY[b]);
}
function dedupeSecretFindings(findings) {
  const byKey = /* @__PURE__ */ new Map();
  for (const f of findings) {
    const key = secretLocationKey(f.file, f.line);
    const incoming = sortSources(f.sources);
    const existed = byKey.get(key);
    if (!existed) {
      byKey.set(key, { ...f, sources: incoming });
      continue;
    }
    const existingTop = SOURCE_PRIORITY[existed.sources[0]];
    existed.sources = sortSources([...existed.sources, ...incoming]);
    if (SOURCE_PRIORITY[incoming[0]] < existingTop) {
      existed.rule = f.rule;
    }
    existed.description ??= f.description;
  }
  return [...byKey.values()];
}

// dist/clients/project-diagnostics/runner-adapters/govulncheck.js
import * as path17 from "node:path";
function govulncheckFindingToProjectDiagnostic(cwd, finding) {
  const frame = finding.trace?.find((f) => f.filename);
  const filePath = frame?.filename ? path17.isAbsolute(frame.filename) ? frame.filename : path17.resolve(cwd, frame.filename) : cwd;
  const fix = finding.fixedVersion ? ` (fixed in ${finding.fixedVersion})` : "";
  return {
    filePath,
    line: frame?.line,
    severity: "warning",
    semantic: "warning",
    tool: "govulncheck",
    runner: "govulncheck",
    rule: `govulncheck:${finding.osv}`,
    code: finding.packageName ?? finding.module,
    message: `Vulnerability ${finding.osv}: ${finding.summary ?? "reachable vulnerable dependency"}${fix}`,
    source: "project-scan"
  };
}
function govulncheckResultToProjectDiagnostics(cwd, result) {
  if (!result.success || result.findings.length === 0)
    return [];
  return result.findings.map((finding) => govulncheckFindingToProjectDiagnostic(cwd, finding));
}

// dist/clients/turn-end/lanes/govulncheck.js
async function collect(ctx) {
  const data = (await ctx.readScannerCache("govulncheck"))?.data;
  return {
    govulncheck: {
      findings: data?.findings ?? [],
      scannedAt: data?.scannedAt,
      citedPath: (finding) => finding.trace.find((frame) => frame.filename)?.filename,
      onMissing: "demote"
    }
  };
}
function gate(gates, ctx) {
  const stale = new Set(gates.govulncheck.stale);
  const filtered = filterFindingsByDisposition([...gates.govulncheck.live, ...gates.govulncheck.stale], ctx.cwd, (f) => govulncheckFindingToProjectDiagnostic(ctx.cwd, f));
  return {
    findings: filtered.kept,
    stale,
    suppressed: filtered.suppressed
  };
}
function render(kept, ctx) {
  const advisorySections = [];
  if (kept.findings.length) {
    const findings = kept.findings.slice(0, 5);
    let report = "\u{1F6E1}\uFE0F Go CVEs reachable from this code (govulncheck) \u2014 upgrade where possible:\n";
    for (const f of findings) {
      const callSite = f.trace.find((t) => t.filename);
      const stale = kept.stale.has(f);
      const where = callSite?.filename ? `${toRunnerDisplayPath(ctx.cwd, callSite.filename)}${!stale && callSite.line ? `:${callSite.line}` : ""}${stale ? ` ${STALE_LINE_MARKER}` : ""}` : f.module ?? f.packageName ?? "(module)";
      const fix = f.fixedVersion ? ` \u2014 upgrade to ${f.fixedVersion} or later` : " \u2014 no fix yet, track upstream";
      report += `  ${f.osv} (${where})${fix}
`;
    }
    if (kept.findings.length > findings.length) {
      report += `  \u2026 and ${kept.findings.length - findings.length} more
`;
    }
    advisorySections.push(report);
  }
  return {
    advisoryParts: advisorySections,
    dispositionSuppressed: { govulncheck: kept.suppressed }
  };
}
var govulncheckLane = {
  collect,
  gate,
  render
};

// dist/clients/project-diagnostics/runner-adapters/gitleaks.js
import * as path18 from "node:path";
function gitleaksFindingToProjectDiagnostic(cwd, finding) {
  const statusSuffix = finding.pathStatus ? ` [git: ${finding.pathStatus}]` : "";
  const isBlocking2 = finding.pathStatus !== "scratch" && finding.pathStatus !== "ignored" && finding.pathStatus !== "nested-repository";
  return {
    filePath: path18.isAbsolute(finding.file) ? finding.file : path18.resolve(cwd, finding.file),
    line: finding.startLine,
    severity: isBlocking2 ? "error" : "info",
    semantic: isBlocking2 ? "blocking" : "none",
    tool: "gitleaks",
    runner: "gitleaks",
    rule: `gitleaks:${finding.ruleId}`,
    message: `Potential secret: ${finding.description || finding.ruleId}${statusSuffix}`,
    source: "project-scan"
  };
}
function gitleaksResultToProjectDiagnostics(cwd, result) {
  if (!result.success || result.findings.length === 0)
    return [];
  return result.findings.map((finding) => gitleaksFindingToProjectDiagnostic(cwd, finding));
}

// dist/clients/project-diagnostics/runner-adapters/trivy.js
import * as path19 from "node:path";
function trivyFindingToProjectDiagnostic(cwd, finding) {
  const filePath = finding.target ? path19.isAbsolute(finding.target) ? finding.target : path19.resolve(cwd, finding.target) : cwd;
  const version = finding.installedVersion ? `@${finding.installedVersion}` : "";
  const fix = finding.fixedVersion ? ` (fixed in ${finding.fixedVersion})` : "";
  return {
    filePath,
    severity: "warning",
    semantic: "warning",
    tool: "trivy",
    runner: "trivy",
    rule: `trivy:${finding.vulnerabilityId}`,
    code: finding.pkgName,
    message: `${finding.severity} vulnerability ${finding.vulnerabilityId} in ${finding.pkgName}${version}${fix}`,
    source: "project-scan"
  };
}
function trivySecretFindingToProjectDiagnostic(cwd, finding) {
  return {
    filePath: path19.isAbsolute(finding.file) ? finding.file : path19.resolve(cwd, finding.file),
    line: finding.line,
    severity: "error",
    semantic: "blocking",
    tool: "trivy",
    runner: "trivy",
    rule: `trivy-secret:${finding.ruleId}`,
    message: `Potential secret: ${finding.title || finding.ruleId}`,
    source: "project-scan"
  };
}
function trivySecretsToProjectDiagnostics(cwd, secrets) {
  return secrets.map((finding) => trivySecretFindingToProjectDiagnostic(cwd, finding));
}
function trivyResultToProjectDiagnostics(cwd, result) {
  if (!result.success)
    return [];
  return [
    ...(result.findings ?? []).map((finding) => trivyFindingToProjectDiagnostic(cwd, finding)),
    // `secrets` defaults to `[]` here rather than assuming the field is
    // always present: `fetchFreshProjectDiagnostics` swallows a task
    // rejection into a silent "completed" outcome (its
    // `.catch(() => "completed")`), so an unguarded `.map` on a missing
    // field here would drop this lane's CVE diagnostics too, not just the
    // secrets — a caller-visible regression for a field this adapter
    // doesn't itself require.
    ...trivySecretsToProjectDiagnostics(cwd, result.secrets ?? [])
  ];
}

// dist/clients/turn-end/lanes/secrets.js
async function blockingGitleaksFindings(data, ctx) {
  const classification = await bounded(classifyAndFilterFindings(data?.findings ?? [], ctx.cwd), {
    ms: HOOK_WALL_BUDGET_MS.turn_end,
    signal: ctx.signal,
    hook: "turn_end",
    label: "classifyAndFilterFindings"
  });
  if (classification === void 0) {
    recordDegradationOnce({
      kind: "gitleaks_classification_timeout",
      subject: ctx.cwd,
      reason: "gitleaks classification exceeded the turn_end budget; retained raw findings to fail open"
    });
  }
  return (classification ?? data?.findings ?? []).filter((finding) => gitleaksFindingToProjectDiagnostic(ctx.cwd, finding).semantic === "blocking");
}
async function collect2(ctx) {
  const [gitleaksEntry, trivyEntry] = await Promise.all([
    ctx.readScannerCache("gitleaks"),
    ctx.readScannerCache("trivy")
  ]);
  const gitleaksData = gitleaksEntry?.data;
  const trivyData = trivyEntry?.data;
  return {
    gitleaks: {
      findings: await blockingGitleaksFindings(gitleaksData, ctx),
      citedPath: (finding) => finding.file,
      scannedAt: gitleaksData?.scannedAt
    },
    "trivy-secrets": {
      findings: trivyData?.secrets ?? [],
      citedPath: (finding) => finding.file,
      scannedAt: trivyData?.scannedAt
    }
  };
}
function gate2(gates, ctx) {
  const gitleaksLive = filterFindingsByDisposition(gates.gitleaks.live, ctx.cwd, (f) => gitleaksFindingToProjectDiagnostic(ctx.cwd, f));
  const gitleaksStale = filterFindingsByDisposition(gates.gitleaks.stale, ctx.cwd, (f) => gitleaksFindingToProjectDiagnostic(ctx.cwd, f));
  const trivyLive = filterFindingsByDisposition(gates["trivy-secrets"].live, ctx.cwd, (f) => trivySecretFindingToProjectDiagnostic(ctx.cwd, f));
  const trivyStale = filterFindingsByDisposition(gates["trivy-secrets"].stale, ctx.cwd, (f) => trivySecretFindingToProjectDiagnostic(ctx.cwd, f));
  return {
    gitleaksLive: gitleaksLive.kept,
    gitleaksStale: gitleaksStale.kept,
    trivyLive: trivyLive.kept,
    trivyStale: trivyStale.kept,
    suppressed: {
      gitleaks: gitleaksLive.suppressed + gitleaksStale.suppressed,
      "trivy-secrets": trivyLive.suppressed + trivyStale.suppressed
    }
  };
}
function render2(kept, ctx) {
  const sessionSecrets = dedupeSecretFindings([
    ...fromGitleaks(kept.gitleaksLive),
    ...fromTrivySecrets(kept.trivyLive)
  ]);
  const deliveredLocationKeys = new Set(sessionSecrets.map((f) => secretLocationKey(f.file, f.line)));
  const blockerSections = [];
  if (sessionSecrets.length) {
    const enriched = dedupeSecretFindings([
      ...sessionSecrets,
      ...fromAstGrepWarnings(ctx.peekActionableWarnings().filter(isSecretWarning)).filter((a) => deliveredLocationKeys.has(secretLocationKey(a.file, a.line)))
    ]);
    const shown2 = enriched.slice(0, 5);
    let report = "\u{1F534} STOP \u2014 hardcoded secrets detected. Rotate the credentials and remove them from source:\n";
    for (const f of shown2) {
      const where = `${toRunnerDisplayPath(ctx.cwd, f.file)}:${f.line}`;
      report += `  ${where} \u2014 ${f.rule} [${f.sources.join(" + ")}]${f.description ? `: ${f.description}` : ""}
`;
    }
    if (enriched.length > shown2.length) {
      report += `  \u2026 and ${enriched.length - shown2.length} more
`;
    }
    blockerSections.push(report);
  }
  const staleSecretEntries = [
    ...kept.gitleaksStale.map((f) => ({
      file: toRunnerDisplayPath(ctx.cwd, f.file),
      rule: f.ruleId,
      source: "gitleaks"
    })),
    ...kept.trivyStale.map((f) => ({
      file: toRunnerDisplayPath(ctx.cwd, f.file),
      rule: f.ruleId,
      source: "trivy"
    }))
  ];
  const staleSecrets = [
    ...new Map(staleSecretEntries.map((e) => [`${e.file}|${e.rule}|${e.source}`, e])).values()
  ];
  const staleSections = [];
  if (staleSecrets.length) {
    const shown2 = staleSecrets.slice(0, 5);
    let report = `\u{1F511} ACTION NEEDED \u2014 secrets were flagged in files that changed after the scan. ${STALE_LINE_MARKER}
The cached line numbers are no longer trustworthy, so they are withheld. Re-run a secrets scan to confirm or clear these:
`;
    for (const entry of shown2) {
      report += `  ${entry.file} \u2014 ${entry.rule} [${entry.source}]
`;
    }
    if (staleSecrets.length > shown2.length) {
      report += `  \u2026 and ${staleSecrets.length - shown2.length} more
`;
    }
    staleSections.push(report);
  }
  return {
    blockerParts: blockerSections,
    staleSecretParts: staleSections,
    dispositionSuppressed: kept.suppressed,
    deliveredLocationKeys
  };
}
var secretsLane = {
  collect: collect2,
  gate: gate2,
  render: render2
};

// dist/clients/turn-end/delivery-holds.js
function planDeliveryHolds(args) {
  const byPart = /* @__PURE__ */ new Map();
  for (const hold of args.holds) {
    const queue = byPart.get(hold.part);
    if (queue)
      queue.push(hold);
    else
      byPart.set(hold.part, [hold]);
  }
  const settled = [];
  let start = 0;
  for (const part of args.parts) {
    const end = start + part.text.length;
    const hold = byPart.get(part.raw)?.shift();
    if (hold) {
      const whole = end <= args.keptChars;
      const leadsOversized = start === 0;
      const reached = whole || leadsOversized;
      settled.push({
        hold,
        reached,
        keeps: reached || (hold.canHold?.() ?? true)
      });
    }
    start = end + args.separatorLength;
  }
  return {
    heldCount: settled.reduce((n, entry) => n + (entry.reached || !entry.keeps ? 0 : 1), 0),
    settle: ({ suppressed, isCurrentSession, onFault }) => {
      let delivered = 0;
      let held = 0;
      let dropped = 0;
      const live = isCurrentSession();
      for (const { hold, reached, keeps } of settled) {
        if (reached)
          delivered += 1;
        else if (keeps)
          held += 1;
        else
          dropped += 1;
        if (!live)
          continue;
        const run = reached ? suppressed && hold.skipOnSuppressed ? void 0 : hold.onDelivered : keeps ? hold.onHeld : hold.onDropped;
        if (!run)
          continue;
        try {
          run();
        } catch (cause) {
          onFault(cause);
        }
      }
      return { delivered, held, dropped };
    }
  };
}
function stillReportedParked(parked, current, keyOf, offeredKeys) {
  const parkedKeys = new Set(parked.map(keyOf));
  return current.filter((item) => parkedKeys.has(keyOf(item)) && !offeredKeys.has(keyOf(item)));
}

// dist/clients/knip-scan-roots.js
import { resolve as resolve11 } from "node:path";
var MAX_KNIP_ROOTS_PER_TURN = 3;
function resolveKnipScanRoots(sessionCwd, modifiedFiles) {
  const session = resolveGitCheckout(sessionCwd);
  if (!session || modifiedFiles.length === 0) {
    return { roots: [sessionCwd], overCap: [] };
  }
  let sessionOwnsEdit = false;
  const linked = [];
  for (const file of modifiedFiles) {
    const owner = resolveLinkedWorktreeOwner(session, resolve11(sessionCwd, file));
    if (owner === null) {
      sessionOwnsEdit = true;
    } else if (!linked.some((known) => pathsEqual(known.root, owner.root))) {
      linked.push(owner);
    }
  }
  const linkedRoots = linked.map((checkout) => checkout.spelledRoot);
  const roots = sessionOwnsEdit ? [sessionCwd, ...linkedRoots] : linkedRoots;
  return {
    roots: roots.slice(0, MAX_KNIP_ROOTS_PER_TURN),
    overCap: roots.slice(MAX_KNIP_ROOTS_PER_TURN)
  };
}

// dist/clients/project-diagnostics/runner-adapters/dead-code.js
import * as path20 from "node:path";
function deadCodeRunnerId(language) {
  return `dead-code-${language}`;
}
function deadCodeMessage(issue) {
  if (issue.category === "unlisted")
    return `Unlisted dependency ${issue.name}`;
  if (issue.category === "dependency")
    return `Unused dependency ${issue.name}`;
  if (issue.category === "file")
    return `Unused file ${issue.name}`;
  return `Unused ${issue.kind} ${issue.name}`;
}
function deadCodeIssueToProjectDiagnostic(cwd, issue, language) {
  const blocking = issue.category === "unlisted";
  return {
    filePath: issue.file ? path20.isAbsolute(issue.file) ? issue.file : path20.resolve(cwd, issue.file) : cwd,
    line: issue.line,
    severity: blocking ? "error" : "warning",
    semantic: blocking ? "blocking" : "warning",
    tool: "dead-code",
    runner: deadCodeRunnerId(language),
    rule: `dead-code:${issue.category}`,
    message: deadCodeMessage(issue),
    source: "project-scan"
  };
}
function deadCodeResultToProjectDiagnostics(cwd, result) {
  if (!result.success)
    return [];
  const issues = [
    ...result.unusedExports,
    ...result.unusedFiles,
    ...result.unusedDeps,
    ...result.unlistedDeps
  ];
  return issues.map((issue) => deadCodeIssueToProjectDiagnostic(cwd, issue, result.language));
}

// dist/clients/project-diagnostics/runner-adapters/knip.js
import * as path21 from "node:path";
function knipIssueFile(cwd, issue) {
  if (!issue.file)
    return cwd;
  return path21.isAbsolute(issue.file) ? issue.file : path21.resolve(cwd, issue.file);
}
function knipIssueMessage(issue) {
  if (issue.type === "unlisted") {
    return `Unlisted dependency ${issue.name}`;
  }
  if (issue.type === "bin") {
    return `Unlisted binary ${issue.name}`;
  }
  if (issue.type === "export") {
    return `Unused export ${issue.name}`;
  }
  if (issue.type === "enumMember") {
    return `Unused enum member ${issue.name}`;
  }
  if (issue.type === "file") {
    return `Unused file ${issue.name}`;
  }
  if (issue.type === "dependency" || issue.type === "devDependency") {
    return `Unused ${issue.type} ${issue.name}`;
  }
  return `${issue.type}: ${issue.name}`;
}
function knipIssueToProjectDiagnostic(cwd, issue) {
  const blocking = issue.type === "unlisted" || issue.type === "bin";
  return {
    filePath: knipIssueFile(cwd, issue),
    line: issue.line,
    severity: blocking ? "error" : "warning",
    semantic: blocking ? "blocking" : "warning",
    tool: "knip",
    runner: "knip",
    rule: `knip:${issue.type}`,
    code: issue.package,
    message: knipIssueMessage(issue),
    source: "project-scan"
  };
}
function knipIssuesToProjectDiagnostics(cwd, issues) {
  return issues.map((issue) => knipIssueToProjectDiagnostic(cwd, issue));
}

// dist/clients/lens-events.js
var LENS_EVENT_VERSION = 1;
var LENS_EVENT_NAMES = {
  analysisComplete: "pi-lens/analysis-complete",
  findings: "pi-lens/findings",
  turnFindings: "pi-lens/turn-findings"
};
var liveEmitter = createLiveBusEmitter();
function truncateText(value, maxChars) {
  if (value === void 0)
    return void 0;
  if (value.length <= maxChars)
    return value;
  return `${value.slice(0, maxChars)}\u2026`;
}
var _droppedEmitLogged = false;
var hasLoggedFailure = false;
function emitLensEvent(eventName, payload) {
  setImmediate(() => {
    const rawCwd = payload.cwd ?? process.cwd();
    const resolution = resolveLiveBusEmitter(liveEmitter, () => ({
      event: eventName,
      cwd: normalizeFilePath(rawCwd)
    }));
    if (resolution.outcome === "stale-session")
      return;
    if (resolution.outcome === "unwired") {
      if (!_droppedEmitLogged) {
        _droppedEmitLogged = true;
        logExtension({
          subsystem: "lens-events",
          level: "warn",
          message: `lens event dropped (no live bus): ${eventName} \u2014 further drops logged once per process`,
          metadata: { eventName }
        });
      }
      return;
    }
    const cwd = normalizeFilePath(rawCwd);
    try {
      resolution.emit(eventName, payload);
      hasLoggedFailure = false;
      logBusEvent({ event: eventName, outcome: "emitted", cwd });
    } catch (err) {
      logBusEvent({
        event: eventName,
        outcome: "emit_failed",
        cwd,
        error: String(err),
        ctxSource: resolution.ctxSource
      });
      if (!hasLoggedFailure) {
        hasLoggedFailure = true;
        recordStaleBusFailure(eventName, err);
      }
    }
  });
}
function emitLensTurnFindings(payload) {
  emitLensEvent(LENS_EVENT_NAMES.turnFindings, {
    version: LENS_EVENT_VERSION,
    source: "pi-lens",
    timestamp: (/* @__PURE__ */ new Date()).toISOString(),
    ...payload,
    content: truncateText(payload.content, 8e3) ?? ""
  });
}

// dist/clients/test-target-roots.js
import { dirname as dirname7 } from "node:path";
function createTurnEndTestRoots(sessionCwd) {
  const session = resolveGitCheckout(sessionCwd);
  const ownerByDir = /* @__PURE__ */ new Map();
  return (absoluteFile) => {
    if (session === null)
      return sessionCwd;
    const dir = dirname7(absoluteFile);
    let owner = ownerByDir.get(dir);
    if (owner === void 0) {
      owner = resolveLinkedWorktreeOwner(session, absoluteFile);
      ownerByDir.set(dir, owner);
    }
    return owner === null ? sessionCwd : owner.spelledRoot;
  };
}

// dist/clients/blocker-past-eof.js
function sweepInlineBlockerPastEof(runtime, cwd, options) {
  const counts = {
    total: 0,
    checked: 0,
    demoted: 0,
    deadLinesByPath: /* @__PURE__ */ new Map()
  };
  const resync = options?.resync ?? resyncDocumentOnPastEof;
  let entries;
  try {
    entries = runtime.getInlineBlockersSnapshot();
  } catch {
    return counts;
  }
  counts.total = entries.length;
  for (const entry of entries) {
    try {
      if (!entry.lines || entry.lines.length === 0)
        continue;
      if (entry.stale && entry.staleReason !== "past-eof")
        continue;
      counts.checked += 1;
      const probes = entry.lines.map((line) => ({
        line,
        stale: entry.stale,
        staleReason: entry.staleReason
      }));
      const { diagnostics } = demotePastEofDiagnostics({
        store: "inline-blocker",
        cwd,
        filePath: entry.filePath,
        diagnostics: probes,
        lineCountCache: options?.lineCountCache,
        resync
      });
      const isPastEof = diagnostics.some((d) => d.stale);
      if (isPastEof) {
        counts.deadLinesByPath.set(entry.filePath, diagnostics.filter((d) => d.stale).map((d) => d.line).filter((line) => typeof line === "number"));
      }
      const transitioned = runtime.setInlineBlockerPastEofStale(entry.filePath, isPastEof);
      if (!transitioned)
        continue;
      if (isPastEof)
        counts.demoted += 1;
    } catch {
    }
  }
  return counts;
}

// dist/clients/persistent-reverify.js
import * as fs15 from "node:fs";
var MAX_REVERIFY_FILES = 4;
var REVERIFY_BUDGET_MS = 3e3;
var TOUCH_SOURCE = "persistent_reverify";
function selectPersistentReverifyFiles(report, nowMs = Date.now()) {
  const candidates = [];
  let skippedChanged = 0;
  for (const entry of report.files) {
    if (entry.origin !== "deferred")
      continue;
    if (entry.warnings.some((w) => findAuxiliaryProfileForSource(w.source ?? "") !== void 0)) {
      continue;
    }
    const stamp = entry.generatedAt ? Date.parse(entry.generatedAt) : NaN;
    if (!Number.isFinite(stamp) || stamp > nowMs)
      continue;
    let mtimeMs;
    try {
      mtimeMs = fs15.statSync(entry.filePath).mtimeMs;
    } catch {
      continue;
    }
    if (mtimeMs > stamp) {
      skippedChanged += 1;
      continue;
    }
    if (candidates.length < MAX_REVERIFY_FILES)
      candidates.push(entry);
  }
  return { candidates, skippedChanged };
}
async function runPersistentReverify(args) {
  const started = Date.now();
  const deadlineAt = started + (args.budgetMs ?? REVERIFY_BUDGET_MS);
  const nowMs = args.nowMs ?? started;
  const { candidates, skippedChanged } = selectPersistentReverifyFiles(args.report, nowMs);
  const outcomes = [];
  const replacementFiles = [];
  let touched = 0;
  const buildArgs = {
    cwd: args.cwd,
    sessionId: args.report.sessionId,
    turnIndex: 0,
    files: [],
    modifiedRangesByFile: /* @__PURE__ */ new Map(),
    dispatchWarnings: [],
    includeLspCodeActions: true,
    deltaOnly: false,
    ...args.signal !== void 0 ? { signal: args.signal } : {},
    lspBudgetMs: REVERIFY_BUDGET_MS,
    dbg: () => {
    }
  };
  let processed = 0;
  for (const entry of candidates) {
    if (args.signal?.aborted || Date.now() >= deadlineAt)
      break;
    processed += 1;
    let content;
    try {
      content = fs15.readFileSync(entry.filePath, "utf-8");
    } catch {
      outcomes.push({
        filePath: entry.filePath,
        displayPath: entry.displayPath,
        outcome: "unconfirmed",
        dropped: 0,
        kept: entry.warnings.length
      });
      replacementFiles.push({ ...entry, reVerifyIncomplete: true });
      continue;
    }
    let touchedResult;
    try {
      touchedResult = await bounded(args.lspService.touchFile(entry.filePath, content, {
        diagnostics: "document",
        collectDiagnostics: true,
        source: TOUCH_SOURCE,
        clientScope: "primary"
      }), {
        ms: Math.max(250, deadlineAt - Date.now()),
        signal: args.signal,
        hook: "turn_end",
        label: "persistent_reverify_touch"
      });
    } catch {
      touchedResult = void 0;
    }
    if (touchedResult === void 0 || touchedResult.inconclusive === true || touchedResult.skipReason !== void 0 || (touchedResult.diagnosticsUnsupportedServerIds ?? []).length > 0 || !touchCompletedConfirmationPolicy(touchedResult)) {
      outcomes.push({
        filePath: entry.filePath,
        displayPath: entry.displayPath,
        outcome: "unconfirmed",
        dropped: 0,
        kept: entry.warnings.length
      });
      replacementFiles.push({ ...entry, reVerifyIncomplete: true });
      continue;
    }
    touched += 1;
    const freshRecords = await bounded(enrichFileFromLsp(args.cwd, buildArgs, { filePath: entry.filePath, cached: touchedResult.diags }, {
      lspService: args.lspService,
      pullTimeoutMs: 2e3,
      deadlineAt,
      ...args.signal !== void 0 ? { signal: args.signal } : {},
      site: { hook: "turn_end", label: "persistent_reverify" }
    }), {
      ms: Math.max(250, deadlineAt - Date.now()),
      signal: args.signal,
      hook: "turn_end",
      label: "persistent_reverify_enrich"
    }) ?? [];
    let dropped = 0;
    let kept = 0;
    for (const warning of entry.warnings) {
      const stillThere = freshRecords.some((record) => record.rule === warning.rule && record.message === warning.message);
      if (stillThere)
        kept += 1;
      else
        dropped += 1;
    }
    const outcome = freshRecords.length === 0 ? "clean" : dropped === 0 ? "reconfirmed" : "mixed";
    outcomes.push({
      filePath: entry.filePath,
      displayPath: entry.displayPath,
      outcome,
      dropped,
      kept
    });
    const { reVerifyIncomplete: _reVerifyIncomplete, ...entryWithoutIncomplete } = entry;
    replacementFiles.push({
      ...entryWithoutIncomplete,
      warnings: freshRecords,
      reVerified: true,
      generatedAt: new Date(nowMs).toISOString()
    });
  }
  logLatency({
    type: "phase",
    toolName: "turn_end",
    filePath: args.cwd,
    phase: "persistent_reverify",
    durationMs: Date.now() - started,
    metadata: {
      candidates: candidates.length,
      touched,
      clean: outcomes.filter((o) => o.outcome === "clean").length,
      reconfirmed: outcomes.filter((o) => o.outcome === "reconfirmed").length,
      mixed: outcomes.filter((o) => o.outcome === "mixed").length,
      unconfirmed: outcomes.filter((o) => o.outcome === "unconfirmed").length,
      skippedChanged,
      skippedBudget: candidates.length - processed
    }
  });
  return {
    outcomes,
    replacementFiles,
    candidates: candidates.length,
    touched,
    skippedChanged
  };
}

// dist/clients/inline-blocker-dispositions.js
import * as fs16 from "node:fs";
import * as path22 from "node:path";
function readCurrentContent(filePath) {
  try {
    return fs16.readFileSync(filePath, "utf-8");
  } catch {
    return "";
  }
}
function applyInlineBlockerPolicy(record, cwd) {
  const diagnostics = record.diagnostics;
  if (!diagnostics || diagnostics.length === 0) {
    if (hasAnyDispositionMarks(cwd)) {
      recordDegradationOnce({
        kind: "inline-blocker-unstructured",
        subject: `inline-blocker:${toRunnerDisplayPath(cwd, record.filePath)}`,
        reason: "record carries no structured diagnostics; disposition marks cannot be applied to its rendered summary"
      });
    }
    return {
      body: record.summary,
      candidates: 0,
      kept: 0,
      suppressed: 0,
      unstructured: true
    };
  }
  const byFile = /* @__PURE__ */ new Map();
  for (const diagnostic of diagnostics) {
    const owner = path22.resolve(cwd, diagnostic.filePath);
    const group = byFile.get(owner);
    if (group)
      group.push(diagnostic);
    else
      byFile.set(owner, [diagnostic]);
  }
  const policyMap = loadProjectRulePolicyMap(cwd);
  const dropped = /* @__PURE__ */ new Set();
  for (const [filePath, group] of byFile) {
    const { kept: kept2 } = applyFindingPolicy(group, {
      cwd,
      filePath,
      content: readCurrentContent(filePath),
      policyMap,
      identities: inlineBlockerIdentities
    });
    const survivors = new Set(kept2);
    for (const diagnostic of group) {
      if (!survivors.has(diagnostic))
        dropped.add(diagnostic);
    }
  }
  const kept = dropped.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => !dropped.has(diagnostic));
  return {
    body: kept.length === 0 ? void 0 : formatDiagnostics(kept, "blocking").trim(),
    candidates: diagnostics.length,
    kept: kept.length,
    suppressed: dropped.size,
    unstructured: false
  };
}
var IDENTITY_CAP = 10;
function summarizeInlineBlockerPolicy(entries) {
  const files = [];
  const tools = /* @__PURE__ */ new Set();
  let stale = 0;
  let candidates = 0;
  let kept = 0;
  let suppressed = 0;
  let unstructured = 0;
  for (const entry of entries) {
    for (const tool of entry.sources ?? [])
      tools.add(tool);
    if (entry.stale)
      stale += 1;
    const outcome = entry.outcome;
    if (outcome === void 0)
      continue;
    candidates += outcome.candidates;
    kept += outcome.kept;
    suppressed += outcome.suppressed;
    if (outcome.unstructured)
      unstructured += 1;
    if (outcome.suppressed > 0 && files.length < IDENTITY_CAP) {
      files.push(entry.displayPath);
    }
  }
  return {
    records: entries.length,
    stale,
    candidates,
    kept,
    dispositionSuppressed: suppressed,
    unstructured,
    files,
    tools: [...tools].slice(0, IDENTITY_CAP)
  };
}

// dist/clients/demoted-finding-render.js
var DEAD_LINE_ANNOTATION = "(line no longer exists)";
var AUTHORITY_REWRITES = [
  [
    /[^\S\n]*(?:🔴|⛔|❌)?[^\S\n]*STOP[^\S\n]*[—–-][^\S\n]*(\d+)[^\S\n]+issue\(s\)[^\S\n]+must be fixed:?/g,
    "$1 issue(s) were flagged before this file changed; the coordinates below may no longer exist:"
  ],
  [/(?:🔴|⛔|❌)[^\S\n]*STOP\b[^\S\n]*[—–-]?/g, "Previously flagged \u2014"],
  [/\bSTOP\b/g, "Previously flagged"],
  [/\bmust be fixed\b/g, "were flagged"]
];
var CITED_LINE_RE = /^([^\S\n]*)L(\d+)\b/;
var PROMOTION_NOTE_ROW = `  \u2139\uFE0F ${DELTA_UNUSED_PROMOTION_NOTE}`;
function splitBannerRegion(lines) {
  const firstCitedRow = lines.findIndex((line) => CITED_LINE_RE.test(line));
  if (firstCitedRow !== -1)
    return { bannerEnd: firstCitedRow };
  const firstNonBlank = lines.findIndex((line) => line.trim() !== "");
  return { bannerEnd: firstNonBlank === -1 ? 0 : firstNonBlank + 1 };
}
function degradeDemotedFindingBody(summary, options = {}) {
  const deadLines = new Set(options.deadLines ?? []);
  let authorityMarkersRemoved = 0;
  const sourceLines = summary.split("\n");
  const { bannerEnd } = splitBannerRegion(sourceLines);
  const degradedLines = sourceLines.map((line, index) => {
    if (index >= bannerEnd)
      return line;
    let rewritten = line;
    for (const [pattern, replacement] of AUTHORITY_REWRITES) {
      rewritten = rewritten.replace(pattern, (...args) => {
        authorityMarkersRemoved += 1;
        return replacement.replace(/\$(\d)/g, (_, digit) => String(args[Number(digit)] ?? ""));
      });
    }
    return rewritten;
  });
  const keptLines = degradedLines.filter((line) => line !== PROMOTION_NOTE_ROW);
  let body = keptLines.join("\n");
  const deadLinesAnnotated = [];
  if (deadLines.size > 0) {
    body = keptLines.map((line) => {
      const hit = CITED_LINE_RE.exec(line);
      if (!hit)
        return line;
      const cited = Number.parseInt(hit[2] ?? "", 10);
      if (!Number.isFinite(cited) || !deadLines.has(cited))
        return line;
      deadLinesAnnotated.push(cited);
      return `${hit[1] ?? ""}L${cited} ${DEAD_LINE_ANNOTATION}${line.slice(hit[0].length)}`;
    }).join("\n");
  }
  return { body, authorityMarkersRemoved, deadLinesAnnotated };
}
function formatRetirementNote(deadLines) {
  const cited = deadLines.length > 0 ? ` (cited ${deadLines.length === 1 ? "line" : "lines"} ${deadLines.join(", ")})` : "";
  return `Retired after this delivery${cited}: the file no longer has these lines, so this finding cannot be re-confirmed and will not be served again.`;
}
function formatDeliveryCapNote(deliveryCount) {
  return `Not shown again after ${deliveryCount} deliveries with no re-run: this finding may still be accurate, but re-serving it further adds no new information. A fresh dispatch of this file will re-confirm or clear it.`;
}

// dist/clients/runtime-turn.js
var LATE_AUX_COVERAGE_GAP_DETAIL_CAP_PER_TURN = 20;
var TEST_RUNNER_BATCH_CONCURRENCY = 4;
var TEST_RUNNER_MAX_TARGETS = 12;
var TURN_END_SELECTION_ROOT_ROWS = 8;
var TEST_RUNNER_BATCH_BUDGET_MS = 2e4;
var TEST_RUNNER_MAX_DEFERRALS = 2;
var TEST_RUNNER_MAX_PERSISTED_TARGETS = 64;
function deferralEntryKey(entry) {
  return `${entry.sessionId ?? ""}\0${normalizeMapKey(path23.resolve(entry.testFile))}`;
}
function mergeDeferredTargets(existing, incoming) {
  const byKey = /* @__PURE__ */ new Map();
  for (const entry of [...existing, ...incoming]) {
    const key = deferralEntryKey(entry);
    const prior = byKey.get(key);
    if (prior && (prior.attempts ?? 0) >= (entry.attempts ?? 0))
      continue;
    byKey.set(key, entry);
  }
  return [...byKey.values()];
}
async function runTestTargetsBounded(args) {
  const results = [];
  if (args.targets.length === 0)
    return { results, deferred: [] };
  let stopReason;
  const batchAbort = new AbortController();
  let closed = false;
  const settledBeforeClose = /* @__PURE__ */ new Set();
  const budgetMs = Math.max(0, args.budgetMs);
  const deadline = Date.now() + budgetMs;
  const shouldStop = () => {
    if (stopReason !== void 0)
      return true;
    if (args.signal?.aborted) {
      stopReason = "abort";
      return true;
    }
    if (Date.now() >= deadline) {
      stopReason = "budget";
      return true;
    }
    return false;
  };
  const pool = mapWithConcurrency(args.targets.map((target, index) => ({ target, index })), Math.max(1, args.concurrency), async ({ target, index }) => {
    if (shouldStop())
      return;
    const running = args.run(target, batchAbort.signal);
    const stamp = running.then((value) => {
      if (closed)
        return;
      settledBeforeClose.add(index);
      results.push({ status: "fulfilled", value });
    }, (reason) => {
      if (closed)
        return;
      settledBeforeClose.add(index);
      results.push({ status: "rejected", reason });
    });
    await stamp;
  });
  void pool.catch(() => {
  });
  await new Promise((resolve30) => {
    let settled = false;
    const finish = (reason) => {
      if (settled)
        return;
      settled = true;
      if (reason !== void 0)
        stopReason ??= reason;
      closed = true;
      batchAbort.abort();
      clearTimeout(timer);
      args.signal?.removeEventListener("abort", onAbort);
      resolve30();
    };
    const onAbort = () => finish("abort");
    const timer = setTimeout(() => finish("budget"), budgetMs);
    timer.unref?.();
    if (args.signal?.aborted)
      finish("abort");
    else
      args.signal?.addEventListener("abort", onAbort, { once: true });
    void pool.then(() => finish(), () => finish());
  });
  return {
    results,
    deferred: args.targets.filter((_, index) => !settledBeforeClose.has(index)),
    stopReason
  };
}
function wouldPoisonCache(prev, next) {
  return !next.success && prev?.data.success === true;
}
var LATE_DEAD_CODE_SCANS = "dead-code-late-scans";
function lateDeadCodeScansOf(scope) {
  return scopeCell(scope, LATE_DEAD_CODE_SCANS, () => /* @__PURE__ */ new Map());
}
function joinedEarlierScan(result, askedAt) {
  return result.scannedAt !== void 0 && Date.parse(result.scannedAt) < askedAt;
}
function withRowForFailureWrite(scan, cacheManager2, cacheKey, cwd, startRow) {
  return scan.then((result) => result.success || startRow?.data.success === true ? { result, row: startRow } : cacheManager2.readCacheAsync(cacheKey, cwd).then((row) => ({ result, row })));
}
function lateDeadCodeScanKey(client, cwd) {
  return `${client.id}\0${cwd}`;
}
function deadCodeScanEvent(client, cwd, result, durationMs) {
  return {
    language: client.language,
    root: cwd,
    success: result.success,
    cached: false,
    unusedExports: result.unusedExports.length,
    unusedFiles: result.unusedFiles.length,
    unusedDeps: result.unusedDeps.length,
    unlistedDeps: result.unlistedDeps.length,
    durationMs: result.durationMs ?? durationMs,
    ...result.excludedWorktrees !== void 0 && {
      excludedWorktrees: result.excludedWorktrees
    },
    ...!result.success && { reason: result.summary }
  };
}
function dropLateDeadCodeScan(scans, key, client, reason) {
  scans.delete(key);
  incrementDegradationCount({
    kind: "dead-code-late-scan-dropped",
    subject: client.id,
    reason
  });
}
function parkLateDeadCodeScan(args) {
  const { scope, cacheManager: cacheManager2, client, cwd, cacheKey, startedAt } = args;
  const scans = lateDeadCodeScansOf(scope);
  const key = lateDeadCodeScanKey(client, cwd);
  const entry = {
    files: args.files,
    previousScan: args.previousScan,
    carry: /* @__PURE__ */ new Set(),
    settled: null,
    askedAt: startedAt
  };
  scans.set(key, entry);
  void withRowForFailureWrite(args.scan, cacheManager2, cacheKey, cwd, entry.previousScan ? { data: entry.previousScan } : null).then(({ result, row }) => {
    if (!scope.isLive()) {
      dropLateDeadCodeScan(scans, key, client, "session-ended");
      return;
    }
    const durationMs = Date.now() - startedAt;
    const cacheKept = wouldPoisonCache(row, result);
    if (!cacheKept) {
      cacheManager2.writeCache(cacheKey, result, cwd, {
        scanDurationMs: durationMs
      });
    }
    logDeadCodeScan({
      ...deadCodeScanEvent(client, cwd, result, durationMs),
      ...cacheKept && { cacheKept }
    });
    if (!result.success) {
      dropLateDeadCodeScan(scans, key, client, `scan-failed: ${result.summary}`);
      return;
    }
    entry.settled = result;
  }, (err) => dropLateDeadCodeScan(scans, key, client, `scan-threw: ${err}`));
}
var lspIdleResetTimeout = null;
var pendingSweepRearm = null;
function emitIdleResetReporterWarning(reportErr) {
  try {
    process.emitWarning(`pi-lens LSP idle reset error reporter failed: ${reportErr}`, { code: "PI_LENS_LSP_IDLE_RESET_REPORTER_FAILED" });
  } catch {
    void reportErr;
  }
}
function reportIdleResetError(onError, err) {
  try {
    onError?.(err);
  } catch (reportErr) {
    emitIdleResetReporterWarning(reportErr);
  }
}
function scheduleLSPIdleReset(resetFn, delayMs, options = {}) {
  if (lspIdleResetTimeout) {
    clearTimeout(lspIdleResetTimeout);
  }
  if (pendingSweepRearm) {
    pendingSweepRearm.cancelled = true;
    pendingSweepRearm = null;
  }
  lspIdleResetTimeout = setTimeout(() => {
    lspIdleResetTimeout = null;
    if (isWorkspaceSweepActive()) {
      const rearmToken = { cancelled: false };
      pendingSweepRearm = rearmToken;
      runWhenWorkspaceSweepIdle(() => {
        if (rearmToken.cancelled)
          return;
        if (pendingSweepRearm === rearmToken)
          pendingSweepRearm = null;
        scheduleLSPIdleReset(resetFn, delayMs, options);
      });
      return;
    }
    try {
      if (options.isCurrentSession && !options.isCurrentSession()) {
        return;
      }
      if (options.isPrimarySession && !options.isPrimarySession()) {
        return;
      }
      resetFn();
    } catch (err) {
      reportIdleResetError(options.onError, err);
    }
  }, delayMs);
  lspIdleResetTimeout.unref();
}
var DEFAULT_LSP_IDLE_RESET_MS = 24e4;
function sweepDerivedFloorMs() {
  return getFullScanWallClockMs() + SWEEP_IDLE_SAFETY_MARGIN_MS;
}
function getBaseLspIdleResetMs() {
  return Math.max(DEFAULT_LSP_IDLE_RESET_MS, sweepDerivedFloorMs());
}
function getShortenedLspIdleResetMs() {
  return Math.max(getLspBudgetIdleTimeoutMs(), sweepDerivedFloorMs());
}
function getEffectiveLspIdleResetMs() {
  return isSubagentSession() || shouldShortenLspIdleTimeout() ? getShortenedLspIdleResetMs() : getBaseLspIdleResetMs();
}
function cancelLSPIdleReset() {
  if (lspIdleResetTimeout) {
    clearTimeout(lspIdleResetTimeout);
    lspIdleResetTimeout = null;
  }
  if (pendingSweepRearm) {
    pendingSweepRearm.cancelled = true;
    pendingSweepRearm = null;
  }
}
function capTurnEndMessage(content, held = 0) {
  const maxLines = RUNTIME_CONFIG.turnEnd.maxLines;
  const maxChars = RUNTIME_CONFIG.turnEnd.maxChars;
  const marker = held > 0 ? `... (truncated; ${held} held for the next turn)` : "... (truncated)";
  let kept = content;
  let cut = false;
  const lines = kept.split("\n");
  if (lines.length > maxLines) {
    kept = lines.slice(0, maxLines).join("\n");
    cut = true;
  }
  if (kept.length > maxChars) {
    kept = sliceAtCodePointBoundaries(kept, 0, maxChars);
    cut = true;
  }
  return cut ? `${kept}
${marker}` : content;
}
function turnEndKeptChars(content) {
  const maxLines = RUNTIME_CONFIG.turnEnd.maxLines;
  const maxChars = RUNTIME_CONFIG.turnEnd.maxChars;
  const lines = content.split("\n");
  const kept = lines.length > maxLines ? lines.slice(0, maxLines).join("\n").length : content.length;
  return Math.min(kept, sliceAtCodePointBoundaries(content, 0, maxChars).length);
}
function sliceAtCodePointBoundaries(value, start, end) {
  if (start > 0 && start < value.length && isLowSurrogate(value.charCodeAt(start)) && isHighSurrogate(value.charCodeAt(start - 1))) {
    start += 1;
  }
  if (end > 0 && end < value.length && isLowSurrogate(value.charCodeAt(end)) && isHighSurrogate(value.charCodeAt(end - 1))) {
    end -= 1;
  }
  return value.slice(start, end);
}
function isHighSurrogate(codeUnit) {
  return codeUnit >= 55296 && codeUnit <= 56319;
}
function isLowSurrogate(codeUnit) {
  return codeUnit >= 56320 && codeUnit <= 57343;
}
var RESOLVED_SHARE_OF_CHARS = 0.4;
var RESOLVED_MAX_LINES = 5;
function formatWriteOrdinal(n) {
  const mod100 = n % 100;
  if (mod100 >= 11 && mod100 <= 13)
    return `${n}th`;
  switch (n % 10) {
    case 1:
      return `${n}st`;
    case 2:
      return `${n}nd`;
    // spellchecker:disable-line
    case 3:
      return `${n}rd`;
    default:
      return `${n}th`;
  }
}
async function handleTurnEnd(deps) {
  const { ctxCwd, getFlag, dbg, runtime, cacheManager: cacheManager2, knipClient, deadCodeClients, depChecker, testRunnerClient, sessionId, host = "pi", owner, resetLSPService: resetLSPService2, resetFormatService: resetFormatService2 } = deps;
  const turnIndexAtDispatch = runtime.turnIndex;
  const clearOwnedTurnState = () => {
    if (runtime.turnIndex !== turnIndexAtDispatch) {
      dbg(`turn_end: retaining newer turn state (dispatch=${turnIndexAtDispatch}, current=${runtime.turnIndex})`);
      return;
    }
    cacheManager2.clearTurnState(cwd, currentOwner);
  };
  void updateHeartbeat().catch(() => {
  });
  const cwd = ctxCwd ?? process.cwd();
  let turnState = cacheManager2.readTurnState(cwd);
  const currentOwner = {
    ...owner ?? {
      kind: "pi",
      id: runtime.telemetrySessionId,
      pid: process.pid,
      lastSeen: (/* @__PURE__ */ new Date()).toISOString()
    },
    sessionStartedAt: owner?.sessionStartedAt ?? runtime.sessionStartedAt
  };
  const access = cacheManager2.getTurnStateAccess(cwd, currentOwner);
  const gateOwnerLabel = turnState.owner ? `${turnState.owner.kind}:${turnState.owner.id}` : turnState.sessionId ? `legacy:${turnState.sessionId}` : "none";
  const sameProcessPiSessionHandoff = access === "foreign-live" && currentOwner.kind === "pi" && turnState.owner?.kind === "pi" && turnState.owner.pid === process.pid && turnState.owner.id !== currentOwner.id;
  if (access === "foreign-live" && !sameProcessPiSessionHandoff) {
    dbg(`turn_end: foreign live owner retained (${turnState.owner?.kind ?? "legacy"}:${turnState.owner?.id ?? turnState.sessionId})`);
    return;
  }
  if (access === "available" && (turnState.files || turnState.owner || turnState.sessionId)) {
    dbg("turn_end: evicting stale turn-state owner");
    clearOwnedTurnState();
    turnState = cacheManager2.readTurnState(cwd);
  }
  const files = Object.keys(turnState.files);
  let widgetDemotedFindingsRetired = 0;
  for (const wPath of drainRenderedDependencyDriftFilePaths()) {
    const deliveryCount = incrementWidgetDependencyDriftDelivery(wPath);
    if (deliveryCount >= DEPENDENCY_DRIFT_MAX_DELIVERIES) {
      const capRetired = retireWidgetDependencyDriftBlockers(wPath);
      if (capRetired) {
        widgetDemotedFindingsRetired += 1;
        incrementDegradationCount({
          kind: "demoted-finding-retired",
          subject: `widget-blocker:${toRunnerDisplayPath(cwd, wPath)}`,
          reason: `capped after ${deliveryCount} deliveries with no re-run; hidden from the pi-lens footer, still listed by lens_diagnostics mode=all \u2014 re-run can still confirm`
        });
      }
    }
  }
  if (files.length === 0 && !runtime.hasCascadeRuns()) {
    if (getFlag("lens-guard") && !runtime.gitGuardHasBlockers) {
      const guardRecord = cacheManager2.readCache("turn-end-findings", cwd)?.data;
      if (guardRecord?.sessionId === runtime.telemetrySessionId && guardRecord.testFailures !== true) {
        cacheManager2.clearCache("turn-end-findings", cwd);
      }
    }
    const idleResetMs = getEffectiveLspIdleResetMs();
    dbg(`turn_end: no modified files, scheduling LSP idle reset (${idleResetMs / 1e3}s)`);
    if (!getFlag("no-lsp")) {
      const sessionGeneration = runtime.sessionGeneration;
      scheduleLSPIdleReset(resetLSPService2, idleResetMs, {
        isCurrentSession: () => runtime.isCurrentSession(sessionGeneration),
        // #2157 fix round 2: a secondary (subagent) evaluation's own timer
        // must not release the primary's shared fleet — see the option's
        // doc comment on `scheduleLSPIdleReset`.
        isPrimarySession: () => {
          const activePrimarySessionId = getActiveSessionId();
          return activePrimarySessionId === void 0 || activePrimarySessionId === runtime.telemetrySessionId;
        },
        onError: (err) => dbg(`lsp idle reset failed: ${err}`)
      });
    }
    resetFormatService2();
    if (pendingRunnerFindingsSize() === 0 && !runtime.hasResolvedBlockerFiles())
      return;
  }
  if (files.length > 0 && (lspIdleResetTimeout || pendingSweepRearm)) {
    cancelLSPIdleReset();
    dbg("turn_end: cancelled pending LSP idle reset (active editing)");
  }
  dbg(`turn_end: ${files.length} file(s) modified, cycles: ${turnState.turnCycles}/${turnState.maxCycles}, access: ${access}, owner: ${gateOwnerLabel}`);
  if (cacheManager2.isMaxCyclesExceeded(cwd)) {
    dbg("turn_end: max cycles exceeded, clearing state and forcing through");
    clearOwnedTurnState();
    runtime.fixedThisTurn.clear();
    resetFormatService2();
    return;
  }
  const turnEndStart = Date.now();
  const blockerParts = [];
  const staleSecretParts = [];
  const advisoryParts = [];
  const deliveryHolds = [];
  const turnScope = deps.sessionScope ?? runtime.sessionScope;
  const holdScope = runtime.captureSessionGeneration();
  const carryScope = sessionId ?? runtime.telemetrySessionId;
  const carryLane = (...parts) => [carryScope, ...parts].join(":");
  const takeParked = (lane) => holdScope.guardedWrite(`turn-end:${lane}`, () => runtime.takeCutAdvisoryItems(lane)) ?? [];
  const cutAdvisoryHold = (family, lane, part, shown2, isReOffer) => {
    const fresh = shown2.filter((item) => !isReOffer(item));
    const recordDropped = (reason) => {
      incrementDegradationCount({
        kind: "turn-end-advisory-carry-dropped",
        subject: family,
        reason
      });
    };
    return {
      part,
      canHold: () => fresh.length > 0,
      onHeld: () => {
        for (const evicted of runtime.parkCutAdvisoryItems(lane, fresh)) {
          recordDropped(`${evicted} evicted: more lanes had cut items parked than the carry bound keeps`);
        }
        if (fresh.length < shown2.length) {
          recordDropped("the cap cut items this lane had already re-offered once");
        }
      },
      onDropped: () => recordDropped("the cap cut a part that showed only items it had already re-offered once")
    };
  };
  const projectDiagnosticsDelta = [];
  const projectDiagnosticsSources = /* @__PURE__ */ new Set();
  const runnerFindingsStart = Date.now();
  const pendingRunnerFindings = await drainPendingRunnerFindings(0);
  let runnerFindingsDelivered = 0;
  let runnerFindingsStale = 0;
  let runnerFindingsFailed = 0;
  let runnerFindingsDropped = 0;
  let runnerBlockersRecorded = 0;
  let runnerFindingsDispositionSuppressed = 0;
  const runnerFindingsDeliveredIds = [];
  for (const pending2 of pendingRunnerFindings) {
    const result = pending2.result;
    if (!result)
      continue;
    recordRunner(pending2.filePath, pending2.runnerId, result.status, result.diagnostics.length, Date.now() - pending2.markedAtMs, pending2.writeIndex);
    if (result.status === "failed" && !hasUsableResult(result)) {
      runnerFindingsFailed += 1;
      const detail = result.failureMessage ? `: ${result.failureMessage}` : "";
      const failedNote = `\u274C Deferred runner ${pending2.runnerId} failed (${result.failureKind ?? "unknown"})${detail}`;
      deliveryHolds.push({
        part: failedNote,
        onHeld: () => requeueRunnerFindings({
          ...pending2,
          result: { ...result, diagnostics: [] }
        })
      });
      advisoryParts.push(failedNote);
    }
    const verdict = judgeDeferredRunnerFindings(pending2, cwd);
    runnerFindingsStale += verdict.stale;
    if (verdict.stale > 0) {
      dropStaleRunnerFindings(pending2);
      runnerFindingsDropped += 1;
    }
    if (verdict.live === 0)
      continue;
    const displayPath = toRunnerDisplayPath(cwd, pending2.filePath);
    const runnerKept = verdict.kept;
    runnerFindingsDispositionSuppressed += verdict.suppressed;
    if (runnerKept.length === 0) {
      continue;
    }
    runnerFindingsDelivered += runnerKept.length;
    runnerBlockersRecorded += recordDeferredRunnerBlockers(runtime, pending2, runnerKept, verdict.bytes);
    for (const finding of runnerKept) {
      if (runnerFindingsDeliveredIds.length < 50) {
        runnerFindingsDeliveredIds.push(finding.id);
      }
    }
    const advisoryKept = runnerKept.flatMap((finding) => finding.semantic === "blocking" ? [] : [finding]);
    if (advisoryKept.length === 0)
      continue;
    const lines = advisoryKept.map((finding) => `  ${displayPath}:${finding.line ?? 1}:${finding.column ?? 1} [${finding.rule ?? finding.id}] ${finding.message}`);
    const runnerSuppressedNote = verdict.suppressed > 0 ? `; suppressed by disposition: ${verdict.suppressed} finding(s)` : "";
    const lateRunnerPart = `\u23F1\uFE0F Late runner diagnostics (${pending2.runnerId} completed after the edit${runnerSuppressedNote}):
${lines.join("\n")}`;
    const { failureKind: _kind, failureMessage: _message, ...findingsOnly } = result;
    deliveryHolds.push({
      part: lateRunnerPart,
      onHeld: () => requeueRunnerFindings({
        ...pending2,
        result: findingsResult(advisoryKept, findingsOnly)
      })
    });
    advisoryParts.push(lateRunnerPart);
  }
  logLatency({
    type: "phase",
    toolName: "turn_end",
    filePath: cwd,
    phase: "late_runner_findings",
    durationMs: Date.now() - runnerFindingsStart,
    metadata: {
      pending: pendingRunnerFindings.length,
      delivered: runnerFindingsDelivered,
      stale: runnerFindingsStale,
      failed: runnerFindingsFailed,
      dropped: runnerFindingsDropped,
      blockersRecorded: runnerBlockersRecorded,
      dispositionSuppressed: runnerFindingsDispositionSuppressed,
      deliveredIds: runnerFindingsDeliveredIds
    }
  });
  const blockerPastEofStart = Date.now();
  const blockerPastEof = sweepInlineBlockerPastEof(runtime, cwd);
  logLatency({
    type: "phase",
    toolName: "turn_end",
    filePath: cwd,
    phase: "blocker_past_eof_sweep",
    durationMs: Date.now() - blockerPastEofStart,
    metadata: {
      total: blockerPastEof.total,
      checked: blockerPastEof.checked,
      demoted: blockerPastEof.demoted
      // #1944 review F3: `healed` is gone. Retirement makes the falling edge
      // unreachable on this store, so the field could only ever log zero —
      // see `BlockerPastEofCounts`.
    }
  });
  let demotedFindingsRetired = 0;
  const retirePastEofOnDelivery = (bPath, deadLines, displayPath) => () => {
    if (!runtime.retireDemotedPastEofBlocker(bPath, deadLines))
      return;
    runtime.updateGitGuardStatus(false, "");
    demotedFindingsRetired += 1;
    incrementDegradationCount({
      kind: "demoted-finding-retired",
      subject: `inline-blocker:${displayPath}`,
      reason: `file shrank past cited line(s) ${deadLines.join(", ")}; retired after one degraded delivery`
    });
  };
  const countDriftDeliveryOnDelivery = (bPath, displayPath) => () => {
    const deliveryCount = runtime.incrementInlineBlockerStaleDelivery(bPath);
    if (deliveryCount < DEPENDENCY_DRIFT_MAX_DELIVERIES)
      return;
    if (!runtime.retireDemotedDependencyDriftBlocker(bPath))
      return;
    runtime.updateGitGuardStatus(false, "");
    demotedFindingsRetired += 1;
    incrementDegradationCount({
      kind: "demoted-finding-retired",
      subject: `inline-blocker:${displayPath}`,
      reason: `capped after ${deliveryCount} deliveries with no re-run; re-run can still confirm`
    });
  };
  const blockerFreshnessStart = Date.now();
  const blockerFreshness = await sweepInlineBlockerFreshness(runtime, cwd, {
    // #2982: the hook's own signal, so the self axis's filesystem work is
    // bounded by the same abort everything else in this handler honours.
    ...deps.signal === void 0 ? {} : { signal: deps.signal },
    additionalEntries: getWidgetBlockingFilesForSweep().map((row) => ({
      filePath: row.filePath,
      recordedAtMs: row.recordedAtMs,
      demote: () => markWidgetFileBlockersStale(row.filePath, "dependency-drift")
    }))
  });
  logLatency({
    type: "phase",
    toolName: "turn_end",
    filePath: cwd,
    phase: "blocker_freshness_sweep",
    durationMs: Date.now() - blockerFreshnessStart,
    metadata: {
      total: blockerFreshness.total,
      kept: blockerFreshness.kept,
      revalidated: blockerFreshness.revalidated,
      alreadyStale: blockerFreshness.alreadyStale,
      truncatedImports: blockerFreshness.truncatedImports,
      selfHealed: blockerFreshness.selfHealed,
      selfUnverifiable: blockerFreshness.selfUnverifiable,
      hashBudgetExhausted: blockerFreshness.hashBudgetExhausted
    }
  });
  const unresolvedBlockers = runtime.getInlineBlockersSnapshot();
  const inlinePolicyEntries = [];
  const policySuppressedPaths = [];
  const inlinePolicyStart = Date.now();
  for (const record of unresolvedBlockers) {
    const { filePath: bPath, summary, stale, staleReason } = record;
    const displayPath = toRunnerDisplayPath(cwd, bPath);
    const tally = { displayPath, sources: record.sources };
    if (stale) {
      inlinePolicyEntries.push({ ...tally, stale: true });
      const deadLines = blockerPastEof.deadLinesByPath.get(bPath) ?? [];
      const degraded = degradeDemotedFindingBody(summary, { deadLines });
      const retires = runtime.wouldRetireDemotedPastEofBlocker(bPath, deadLines);
      let retirementNote;
      if (retires) {
        retirementNote = formatRetirementNote(deadLines);
      } else if (staleReason === "dependency-drift") {
        const tentativeCount = runtime.peekInlineBlockerStaleDeliveryCount(bPath) + 1;
        if (tentativeCount >= DEPENDENCY_DRIFT_MAX_DELIVERIES) {
          retirementNote = formatDeliveryCapNote(tentativeCount);
        }
      }
      const advisoryPart = `${STALE_LINE_MARKER} ${displayPath}:
${degraded.body}` + (retirementNote ? `
${retirementNote}` : "");
      advisoryParts.push(advisoryPart);
      if (retires) {
        deliveryHolds.push({
          part: advisoryPart,
          onDelivered: retirePastEofOnDelivery(bPath, deadLines, displayPath)
        });
      } else if (staleReason === "dependency-drift") {
        deliveryHolds.push({
          part: advisoryPart,
          onDelivered: countDriftDeliveryOnDelivery(bPath, displayPath),
          skipOnSuppressed: true
        });
      }
    } else {
      const policy = applyInlineBlockerPolicy(record, cwd);
      inlinePolicyEntries.push({ ...tally, stale: false, outcome: policy });
      if (policy.body === void 0) {
        policySuppressedPaths.push(bPath);
        continue;
      }
      const suppressedNote = policy.suppressed > 0 ? ` (suppressed by disposition: ${policy.suppressed} finding(s))` : "";
      blockerParts.push(`Unresolved from this turn \u2014 ${displayPath}${suppressedNote}:
${policy.body}`);
    }
  }
  const guardLatchBefore = runtime.gitGuardHasBlockers;
  resyncGitGuardAfterInlinePolicy({
    runtime,
    suppressedFilePaths: policySuppressedPaths
  });
  if (inlinePolicyEntries.length > 0) {
    logLatency({
      type: "phase",
      toolName: "turn_end",
      filePath: cwd,
      phase: "inline_blocker_policy",
      durationMs: Date.now() - inlinePolicyStart,
      metadata: {
        ...summarizeInlineBlockerPolicy(inlinePolicyEntries),
        // #3248: one row per TURN for the gate outcome, never per finding
        // — the flip is the event a reader needs to explain why a commit
        // that was blocked is now allowed.
        guardFilesSuppressed: policySuppressedPaths.length,
        guardLatchCleared: guardLatchBefore && !runtime.gitGuardHasBlockers
      }
    });
  }
  const cascadeSettleStart = Date.now();
  const { settled, timedOut } = await runtime.settleCascadeRuns(cascadeSettleWaitMs(), { trackTurnEndClock: true, generation: holdScope });
  logLatency({
    type: "phase",
    toolName: "turn_end",
    filePath: cwd,
    phase: "cascade_settle_wait",
    durationMs: Date.now() - cascadeSettleStart,
    metadata: { settled, timedOut }
  });
  const t0 = Date.now();
  const cascadeRuns = (holdScope.guardedWrite("turn-end:cascade-runs", () => runtime.consumeCascadeRuns()) ?? []).filter((run) => {
    const originSeq = run.origin?.projectSeq;
    const originTurn = run.origin?.turnSeq;
    if (originSeq !== void 0) {
      const changedSince = runtime.getFilesChangedSince(originSeq);
      if (changedSince.length > 0) {
        const changedSet = new Set(changedSince);
        const primaryKey = normalizeMapKey(path23.resolve(run.filePath));
        const neighborKeys = [
          ...(run.result?.neighbors ?? []).map((n) => n.filePath),
          ...run.selectedNeighborPaths ?? []
        ].map((filePath) => normalizeMapKey(path23.resolve(filePath)));
        const supersededByOwnFile = changedSet.has(primaryKey) || neighborKeys.some((k) => changedSet.has(k));
        if (supersededByOwnFile) {
          logCascade({
            phase: "cascade_carry_over_drop",
            filePath: run.filePath,
            neighborCount: run.neighborCount,
            diagnosticCount: run.diagnosticCount,
            reason: "superseded_by_later_write",
            metadata: {
              originProjectSeq: originSeq,
              projectSeq: runtime.projectSeq,
              originTurnSeq: originTurn,
              turnIndex: runtime.turnIndex,
              carriedTurns: run.carriedTurns,
              changedFiles: changedSince
            }
          });
          return false;
        }
      }
    }
    return true;
  });
  const cascadeResults = cascadeRuns.flatMap((r) => r.result ? [r.result] : []);
  let carriedRunsRendered = 0;
  let labeledAdvisories = 0;
  const carriedMetaByResult = /* @__PURE__ */ new Map();
  const runByResult = /* @__PURE__ */ new Map();
  for (const r of cascadeRuns) {
    if (r.result)
      runByResult.set(r.result, r);
    if (r.result && (r.carriedTurns ?? 0) > 0 && r.carriedTurns !== void 0) {
      carriedMetaByResult.set(r.result, {
        carriedTurns: r.carriedTurns,
        observedAt: r.observedAt
      });
    }
  }
  const cascadeLogFilePath = cascadeResults[0]?.filePath ?? files[0] ?? cwd;
  if (cascadeResults.length > 0) {
    const seen = /* @__PURE__ */ new Map();
    for (const result of cascadeResults) {
      seen.set(normalizeMapKey(result.filePath), result);
    }
    const neighborOwner = /* @__PURE__ */ new Map();
    for (const result of [...seen.values()].reverse()) {
      const pk = normalizeMapKey(result.filePath);
      for (const n of result.neighbors) {
        const nk = normalizeMapKey(n.filePath);
        if (!neighborOwner.has(nk))
          neighborOwner.set(nk, pk);
      }
    }
    const parts = [];
    const sectionRuns = [];
    let injectedNeighborCount = 0;
    let injectedDiagnosticCount = 0;
    let suppressedByOwnership = 0;
    for (const result of seen.values()) {
      const pk = normalizeMapKey(result.filePath);
      const ownsAny = result.neighbors.some((n) => neighborOwner.get(normalizeMapKey(n.filePath)) === pk);
      if (ownsAny && result.formatted) {
        const carryMeta = carriedMetaByResult.get(result);
        const carrySuffix = cascadeCarrySuffix(carryMeta?.carriedTurns, carryMeta?.observedAt);
        parts.push(carrySuffix ? `${result.formatted}
${carrySuffix}` : result.formatted);
        if (carrySuffix)
          carriedRunsRendered += 1;
        const sectionRun = runByResult.get(result);
        if (sectionRun)
          sectionRuns.push(sectionRun);
        injectedNeighborCount += result.neighbors.length;
        injectedDiagnosticCount += result.neighbors.reduce((s, n) => s + n.diagnostics.length, 0);
      } else if (!ownsAny && result.formatted) {
        suppressedByOwnership++;
      }
    }
    const neighborFilesWithErrors = cascadeResults.flatMap((r) => r.neighbors).filter((n) => n.diagnostics.length > 0).map((n) => n.filePath);
    const uniqueNeighborFiles = [...new Set(neighborFilesWithErrors)];
    let testSuggestionCount = 0;
    if (uniqueNeighborFiles.length > 0 && typeof testRunnerClient.suggestTestFiles === "function") {
      const testSuggestions = testRunnerClient.suggestTestFiles(uniqueNeighborFiles, cwd);
      testSuggestionCount = testSuggestions.length;
      logCascade({
        phase: "cascade_test_targets",
        filePath: cascadeLogFilePath,
        neighborCount: uniqueNeighborFiles.length,
        metadata: {
          neighborFiles: uniqueNeighborFiles.slice(0, 10),
          suggestedTestFiles: testSuggestions.slice(0, 10).map((s) => s.testFile),
          runner: testSuggestions[0]?.runner,
          truncated: testSuggestions.length > 10,
          zeroSuggestions: testSuggestions.length === 0
        }
      });
      if (testSuggestions.length > 0) {
        const testLines = testSuggestions.slice(0, 5).map((s) => `  ${toRunnerDisplayPath(cwd, s.testFile)} (${s.runner})`);
        let testSection = `\u{1F9EA} Likely tests for affected neighbors:
${testLines.join("\n")}`;
        if (testSuggestions.length > 5) {
          testSection += `
  ... and ${testSuggestions.length - 5} more`;
        }
        parts.push(testSection);
      }
    }
    if (parts.length > 0) {
      const section = parts.join("\n\n");
      blockerParts.push(section);
      deliveryHolds.push({
        part: section,
        onHeld: () => {
          for (const run of sectionRuns)
            runtime.appendCascadeRun(run);
        }
      });
      logCascade({
        phase: "cascade_injected",
        filePath: cascadeLogFilePath,
        neighborCount: injectedNeighborCount,
        diagnosticCount: injectedDiagnosticCount,
        metadata: {
          sectionChars: section.length,
          testSuggestionCount,
          suppressedByOwnership
        }
      });
    }
    logCascade({
      phase: "cascade_turn_end",
      filePath: cascadeLogFilePath,
      neighborCount: cascadeResults.reduce((s, r) => s + r.neighbors.length, 0),
      diagnosticCount: cascadeResults.reduce((s, r) => s + r.neighbors.reduce((ns, n) => ns + n.diagnostics.length, 0), 0),
      metadata: {
        fileCount: cascadeResults.length,
        mergedResults: seen.size
      }
    });
  }
  const indeterminateRuns = cascadeRuns.filter((r) => r.indeterminate);
  if (indeterminateRuns.length > 0) {
    const buildAdvisory = (runs, frame) => {
      if (runs.length === 0)
        return void 0;
      const byDetail = /* @__PURE__ */ new Map();
      for (const r of runs) {
        const detail = r.indeterminate?.detail ?? frame.fallbackDetail(r);
        const files2 = byDetail.get(detail) ?? [];
        files2.push(toRunnerDisplayPath(cwd, r.filePath));
        byDetail.set(detail, files2);
      }
      const lines = [];
      for (const [detail, filesRaw] of byDetail) {
        const files2 = [...new Set(filesRaw)];
        const shown2 = files2.slice(0, 5).join(", ");
        const more = files2.length > 5 ? ` (+${files2.length - 5} more)` : "";
        lines.push(`  \u2022 ${detail}: ${shown2}${more}`);
      }
      const fileCount2 = new Set(runs.map((r) => normalizeMapKey(r.filePath))).size;
      const reasons = [...byDetail.keys()].join("; ");
      return `${frame.lead(fileCount2, reasons)}
${lines.join("\n")}`;
    };
    const graphRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason !== "lsp_binding_rejected" && r.indeterminate?.reason !== "excluded_by_role" && r.indeterminate?.reason !== "budget_truncated" && r.indeterminate?.budget === void 0);
    const bindingRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason === "lsp_binding_rejected" && r.indeterminate?.budget === void 0);
    const budgetRuns = indeterminateRuns.filter((r) => r.indeterminate?.reason === "budget_truncated" || r.indeterminate?.budget !== void 0);
    const withCarryLabel = (advisory, runs) => {
      if (advisory === void 0)
        return void 0;
      const carried = runs.filter((r) => (r.carriedTurns ?? 0) > 0);
      if (carried.length === 0 || carried.length !== runs.length) {
        return advisory;
      }
      const stamps = carried.map((r) => r.observedAt);
      const observedAt = stamps.every((s) => s !== void 0) ? Math.min(...stamps) : void 0;
      const suffix = cascadeCarrySuffix(carried[0]?.carriedTurns, observedAt);
      if (!suffix)
        return advisory;
      labeledAdvisories += 1;
      return `${advisory}
${suffix}`;
    };
    const holdAdvisory = (advisory, runs) => {
      if (advisory === void 0)
        return;
      deliveryHolds.push({
        part: advisory,
        onHeld: () => {
          for (const run of runs)
            runtime.appendCascadeRun({ ...run, result: void 0 });
        }
      });
    };
    const graphAdvisory = withCarryLabel(buildAdvisory(graphRuns, {
      lead: (fileCount2, reasons) => `Cascade could not compute downstream impact for ${fileCount2} edited file(s) this turn \u2014 the review graph was unavailable (${reasons}), so their dependents were not cascade-checked and a clean cascade result does not cover them.`,
      fallbackDetail: (r) => r.indeterminate?.reason === "missing_node" ? "changed file not in the review graph" : "review graph unavailable"
    }), graphRuns);
    if (graphAdvisory)
      advisoryParts.push(graphAdvisory);
    holdAdvisory(graphAdvisory, graphRuns);
    const bindingAdvisory = withCarryLabel(buildAdvisory(bindingRuns, {
      lead: (fileCount2, reasons) => `Cascade identified dependents for ${fileCount2} edited file(s) this turn, but their diagnostics could not be freshly confirmed (${reasons}) and were withheld \u2014 a clean cascade result does not cover them.`,
      fallbackDetail: () => "cascade diagnostics withheld (binding rejected)"
    }), bindingRuns);
    if (bindingAdvisory)
      advisoryParts.push(bindingAdvisory);
    holdAdvisory(bindingAdvisory, bindingRuns);
    const budgetAdvisory = withCarryLabel(buildAdvisory(budgetRuns, {
      lead: (fileCount2, reasons) => `Cascade checked the selected neighbors for ${fileCount2} edited file(s) this turn, but some eligible dependents were not checked because the cascade budget was exhausted (${reasons}); a clean cascade result does not cover them.`,
      fallbackDetail: (r) => {
        const budget = r.indeterminate?.budget;
        if (!budget)
          return "cascade budget omitted eligible dependents";
        const detail = `cascade budget checked ${budget.selectedCount} of ${budget.eligibleCount} eligible dependents (${budget.truncatedCount} omitted)`;
        return budget.transitiveTruncated ? `${detail}; transitive expansion was capped before all eligible dependents were enumerated` : detail;
      }
    }), budgetRuns);
    if (budgetAdvisory)
      advisoryParts.push(budgetAdvisory);
    holdAdvisory(budgetAdvisory, budgetRuns);
    const fileCount = new Set(indeterminateRuns.map((r) => normalizeMapKey(r.filePath))).size;
    const byFile = indeterminateRuns.map((r) => ({
      file: toRunnerDisplayPath(cwd, r.filePath),
      reason: r.indeterminate?.reason,
      ...r.indeterminate?.detail && { detail: r.indeterminate.detail },
      ...r.indeterminate?.budget && { budget: r.indeterminate.budget },
      ...r.indeterminate?.diagnostic && {
        diagnostic: r.indeterminate.diagnostic
      }
    }));
    logCascade({
      phase: "cascade_indeterminate",
      // The first indeterminate run's own file. `files[0] ?? cwd` survives only
      // as a last resort for a run with no path at all.
      filePath: indeterminateRuns[0]?.filePath ?? files[0] ?? cwd,
      metadata: {
        fileCount,
        reasons: indeterminateRuns.map((r) => r.indeterminate?.reason),
        byFile: byFile.slice(0, 20),
        ...byFile.length > 20 && { byFileTruncated: byFile.length - 20 }
      }
    });
  }
  if (carriedRunsRendered > 0 || labeledAdvisories > 0) {
    logCascade({
      phase: "cascade_carry_rendered",
      filePath: cascadeLogFilePath,
      metadata: { carriedRunsRendered, labeledAdvisories }
    });
  }
  const cascadeSkipped = {
    blockers: 0,
    non_code: 0,
    no_neighbors: 0,
    clean: 0,
    indeterminate: 0,
    error: 0
  };
  for (const r of cascadeRuns) {
    if (r.skipReason)
      cascadeSkipped[r.skipReason] = (cascadeSkipped[r.skipReason] ?? 0) + 1;
  }
  logLatency({
    type: "phase",
    toolName: "turn_end",
    filePath: cwd,
    phase: "cascade_merge",
    durationMs: Date.now() - t0,
    metadata: {
      runsTotal: cascadeRuns.length,
      resultCount: cascadeResults.length,
      neighborCount: cascadeRuns.reduce((s, r) => s + r.neighborCount, 0),
      diagnosticCount: cascadeRuns.reduce((s, r) => s + r.diagnosticCount, 0),
      skipped: cascadeSkipped
    }
  });
  const knipScan = resolveKnipScanRoots(cwd, files);
  for (const root of knipScan.overCap) {
    incrementDegradationCount({
      kind: "turn-end-knip-root-skipped",
      subject: "root-cap",
      reason: `${toRunnerDisplayPath(cwd, root)} not scanned: more than ${MAX_KNIP_ROOTS_PER_TURN} checkouts edited this turn`
    });
  }
  const KNOWN_SLOW_KNIP_GRACE_MS = 1;
  const KNIP_MAX_SHOWN = 5;
  const applyKnipResult = (scanRoot, prevKnip, knipResult, rowNow) => {
    const knipWouldPoison = wouldPoisonCache(rowNow, knipResult);
    if (knipWouldPoison) {
      dbg(`turn_end: keeping last good knip cache; this run failed: ${knipResult.summary}`);
    } else {
      cacheManager2.writeCache("knip", knipResult, scanRoot);
    }
    const knipMeta = {
      execution: knipResult.execution ?? "executed",
      success: knipResult.success,
      totalIssues: knipResult.issues.length,
      newIssues: 0,
      blockerIssues: 0,
      // #3248: bounded per-turn, on the row this lane already writes —
      // never one record per finding.
      dispositionSuppressed: 0,
      ...!knipResult.success && { reason: knipResult.summary },
      ...knipResult.failureKind && { failureKind: knipResult.failureKind },
      ...knipResult.nestedWorktrees !== void 0 && {
        nestedWorktrees: knipResult.nestedWorktrees,
        nestedFilesDropped: knipResult.nestedFilesDropped
      },
      ...knipWouldPoison && { cacheKept: true }
    };
    const knipBlockerLane = carryLane("knip", scanRoot, "blocker");
    const knipAdvisoryLane = carryLane("knip", scanRoot, "advisory");
    const parkedKnip = knipResult.success ? [
      ...takeParked(knipBlockerLane),
      ...takeParked(knipAdvisoryLane)
    ] : [];
    if (knipResult.success && knipResult.issues.length > 0) {
      const issueKey = (i) => stableFindingKey(i.type, i.file, i.name, i.package);
      const prevKeys = new Set((prevKnip?.data?.issues ?? []).map(issueKey));
      const modifiedSet = new Set(files.map((f) => resolveRunnerPath(cwd, f)));
      const newIssues = knipResult.issues.filter((issue) => {
        if (prevKeys.has(issueKey(issue)))
          return false;
        if (!issue.file)
          return false;
        const abs = resolveRunnerPath(scanRoot, issue.file);
        return modifiedSet.has(abs);
      });
      knipMeta.newIssues = newIssues.length;
      const reOffered = stillReportedParked(parkedKnip, knipResult.issues, issueKey, new Set(newIssues.map(issueKey)));
      const reOfferedKeys = new Set(reOffered.map(issueKey));
      if (reOffered.length > 0)
        knipMeta.reOffered = reOffered.length;
      if (newIssues.length > 0) {
        projectDiagnosticsDelta.push(...knipIssuesToProjectDiagnostics(scanRoot, newIssues));
        projectDiagnosticsSources.add("knip");
      }
      const knipPaired = [...newIssues, ...reOffered].flatMap((issue) => knipIssuesToProjectDiagnostics(scanRoot, [issue]).map((diagnostic) => ({
        issue,
        diagnostic
      })));
      const knipFiltered = filterFindingsByDisposition(knipPaired, cwd, (pair) => pair.diagnostic);
      const knipDeliverable = {
        kept: knipFiltered.kept.map((pair) => pair.issue),
        suppressed: knipFiltered.suppressed
      };
      knipMeta.dispositionSuppressed = knipDeliverable.suppressed;
      const displayPath = (issue) => issue.file ? toRunnerDisplayPath(cwd, resolveRunnerPath(scanRoot, issue.file)) : "(unknown)";
      const blockerIssues = knipDeliverable.kept.filter((i) => i.type === "unlisted" || i.type === "bin");
      knipMeta.blockerIssues = blockerIssues.length;
      if (blockerIssues.length > 0) {
        let report = "\u{1F534} New unresolved imports/deps in modified code (Knip):\n";
        let firstPath = null;
        for (const issue of blockerIssues.slice(0, KNIP_MAX_SHOWN)) {
          const display = displayPath(issue);
          if (!firstPath && display !== "(unknown)")
            firstPath = display;
          report += `  ${display}${issue.line ? `:${issue.line}` : ""} \u2014 ${issue.type}: ${issue.name}
`;
        }
        if (firstPath) {
          report += `  First location: ${firstPath}
`;
        }
        blockerParts.push(report);
        deliveryHolds.push(cutAdvisoryHold("knip", knipBlockerLane, report, blockerIssues.slice(0, KNIP_MAX_SHOWN), (issue) => reOfferedKeys.has(issueKey(issue))));
      }
      const unusedExportDelta = knipDeliverable.kept.filter((i) => i.type === "export" || i.type === "enumMember");
      if (unusedExportDelta.length > 0) {
        let report = "\u26A0\uFE0F Newly unused exports in files you edited \u2014 check if callers need updating (Knip):\n";
        for (const issue of unusedExportDelta.slice(0, KNIP_MAX_SHOWN)) {
          const display = displayPath(issue);
          report += `  ${display}${issue.line ? `:${issue.line}` : ""} \u2014 ${issue.name}
`;
        }
        advisoryParts.push(report);
        deliveryHolds.push(cutAdvisoryHold("knip", knipAdvisoryLane, report, unusedExportDelta.slice(0, KNIP_MAX_SHOWN), (issue) => reOfferedKeys.has(issueKey(issue))));
      }
    }
    return knipMeta;
  };
  const logKnipRow = (root, startedAt, metadata) => logLatency({
    type: "phase",
    toolName: "turn_end",
    filePath: root,
    phase: "knip",
    durationMs: Date.now() - startedAt,
    metadata
  });
  if (runtime.isStartupScanInFlight("knip")) {
    dbg("turn_end: skipping knip (startup scan still in flight)");
    logKnipRow(cwd, Date.now(), { skipped: true });
  } else {
    let budgetSpent = false;
    for (const scanRoot of knipScan.roots) {
      if (budgetSpent) {
        incrementDegradationCount({
          kind: "turn-end-knip-root-skipped",
          subject: "budget",
          reason: `${toRunnerDisplayPath(cwd, scanRoot)} not scanned: an earlier knip scan spent the turn_end budget`
        });
        continue;
      }
      const startedAt = Date.now();
      const prevKnip = cacheManager2.readCache("knip", scanRoot);
      const previousFailedHard = prevKnip && !prevKnip.data.success && !prevKnip.data.failureKind && isHardFailureSummary(prevKnip.data.summary);
      const abandonedFailure = previousFailedHard ? null : knipClient.recentHardFailure?.(scanRoot) ?? null;
      let metadata;
      if (previousFailedHard || abandonedFailure !== null) {
        const reason = previousFailedHard ? prevKnip.data.summary : abandonedFailure ?? "";
        dbg(`turn_end: skipping knip after recent failure: ${reason}`);
        metadata = { skipped: true, reason };
      } else {
        const remainingMs = Math.max(0, HOOK_WALL_BUDGET_MS.turn_end - (Date.now() - turnEndStart));
        const scanFloorMs = knipClient.scanFloorMs?.(scanRoot);
        const knownSlow = scanFloorMs !== void 0 && scanFloorMs > remainingMs;
        const scanned = await bounded(withRowForFailureWrite(knipClient.analyze(scanRoot, getKnipIgnorePatterns(), {
          projectSeq: runtime.projectSeq
        }), cacheManager2, "knip", scanRoot, prevKnip), {
          ms: knownSlow ? KNOWN_SLOW_KNIP_GRACE_MS : remainingMs,
          signal: deps.signal,
          hook: "turn_end",
          label: "knip"
        });
        if (scanned === void 0) {
          dbg(`turn_end: knip for ${scanRoot} outlived the turn_end budget`);
          const aborted = deps.signal?.aborted === true;
          const nestedWorktrees = knipClient.nestedWorktrees?.(scanRoot) ?? 0;
          metadata = {
            execution: "deferred",
            aborted,
            ...knownSlow && {
              reason: "scan-exceeds-budget",
              scanFloorMs
            },
            ...nestedWorktrees > 0 && { nestedWorktrees }
          };
          if (nestedWorktrees > 0 && !aborted) {
            incrementDegradationCount({
              kind: "turn-end-knip-nested-worktrees",
              subject: toRunnerDisplayPath(cwd, scanRoot),
              reason: `${nestedWorktrees} linked worktree(s) nested under the scan root: knip walks them as project files and the scan did not fit the turn_end budget; add their directory (for example /.worktrees/) to .gitignore to drop the cost`
            });
          }
        } else {
          metadata = applyKnipResult(scanRoot, prevKnip, scanned.result, scanned.row);
        }
      }
      budgetSpent = metadata.execution === "deferred" && (metadata.scanFloorMs === void 0 || metadata.aborted === true);
      logKnipRow(scanRoot, startedAt, metadata);
    }
  }
  const tDeadCode = Date.now();
  const deadCodeMeta = {};
  if (runtime.isStartupScanInFlight("dead-code")) {
    dbg("turn_end: skipping dead-code (startup scan still in flight)");
    deadCodeMeta.skipped = true;
    deadCodeMeta.reason = "startup_scan_in_flight";
  } else if (deadCodeClients.length === 0) {
    deadCodeMeta.reason = "no_clients";
  } else {
    let modifiedSet = null;
    const modifiedFiles = () => modifiedSet ??= new Set(files.map((f) => resolveRunnerPath(cwd, f)));
    let newIssueTotal = 0;
    let deadCodeDispositionSuppressed = 0;
    const reasons = [];
    const deliverDeadCodeDelta = (client, sources) => {
      const latest = sources.at(-1);
      if (latest === void 0)
        return;
      const result = latest.result;
      const deadCodeLane = carryLane("dead-code", client.id);
      const parkedDeadCode = takeParked(deadCodeLane);
      deadCodeMeta.totalIssues = (deadCodeMeta.totalIssues ?? 0) + deadCodeIssues(result).length;
      if (!sources.some((source) => source.previousScan?.success)) {
        reasons.push(`${client.id}:no_previous_scan`);
        return;
      }
      const newIssues = [];
      for (const source of sources) {
        if (!source.previousScan?.success)
          continue;
        const prevKeys = new Set(deadCodeIssues(source.previousScan).map(deadCodeIssueKey));
        for (const issue of deadCodeIssues(source.result)) {
          if (prevKeys.has(deadCodeIssueKey(issue)) || !issue.file)
            continue;
          if (source.modified.has(resolveRunnerPath(cwd, issue.file))) {
            newIssues.push(issue);
          }
        }
      }
      const reOffered = stillReportedParked(parkedDeadCode, deadCodeIssues(result), deadCodeIssueKey, new Set(newIssues.map(deadCodeIssueKey)));
      if (newIssues.length === 0 && reOffered.length === 0) {
        reasons.push(`${client.id}:clean`);
        return;
      }
      newIssueTotal += newIssues.length;
      if (newIssues.length > 0) {
        projectDiagnosticsDelta.push(...newIssues.map((issue) => deadCodeIssueToProjectDiagnostic(cwd, issue, result.language)));
        projectDiagnosticsSources.add("dead-code");
      }
      const deadCodeDeliverable = filterFindingsByDisposition([...newIssues, ...reOffered], cwd, (issue) => deadCodeIssueToProjectDiagnostic(cwd, issue, result.language));
      deadCodeDispositionSuppressed += deadCodeDeliverable.suppressed;
      if (deadCodeDeliverable.kept.length === 0) {
        reasons.push(`${client.id}:all_disposed`);
        return;
      }
      const deadCodePart = formatDeadCodeDelta(deadCodeDeliverable.kept, result.language);
      advisoryParts.push(deadCodePart);
      const reOfferedKeys = new Set(reOffered.map(deadCodeIssueKey));
      deliveryHolds.push(cutAdvisoryHold("dead-code", deadCodeLane, deadCodePart, deadCodeDeliverable.kept.slice(0, DEAD_CODE_DELTA_MAX_SHOWN), (issue) => reOfferedKeys.has(deadCodeIssueKey(issue))));
    };
    try {
      const lateScans = lateDeadCodeScansOf(turnScope);
      for (const client of deadCodeClients) {
        if (!client.detect(cwd)) {
          reasons.push(`${client.id}:not_detected`);
          continue;
        }
        const cacheKey = `dead-code-${client.id}`;
        const lateKey = lateDeadCodeScanKey(client, cwd);
        const sources = [];
        const late = lateScans.get(lateKey);
        let carried = /* @__PURE__ */ new Set();
        if (late?.settled) {
          lateScans.delete(lateKey);
          sources.push({
            result: late.settled,
            previousScan: late.previousScan,
            modified: late.files
          });
          carried = late.carry;
          reasons.push(`${client.id}:late_scan`);
          if (joinedEarlierScan(late.settled, late.askedAt)) {
            carried = /* @__PURE__ */ new Set([...late.carry, ...late.files]);
            reasons.push(`${client.id}:joined`);
          }
        }
        try {
          if (late && !late.settled) {
            for (const file of modifiedFiles())
              late.carry.add(file);
            reasons.push(`${client.id}:in_flight`);
            continue;
          }
          const scanFiles = /* @__PURE__ */ new Set([...modifiedFiles(), ...carried]);
          if (![...scanFiles].some((f) => client.owns(f))) {
            reasons.push(`${client.id}:no_owned_files`);
            continue;
          }
          const prev = cacheManager2.readCache(cacheKey, cwd);
          const previousFailedHard = prev && !prev.data.success && isHardFailureSummary(prev.data.summary);
          const abandonedFailure = previousFailedHard ? null : client.recentHardFailure?.(cwd) ?? null;
          if (previousFailedHard || abandonedFailure !== null) {
            const failure = previousFailedHard ? prev.data.summary : abandonedFailure ?? "";
            dbg(`turn_end: skipping dead-code after failure: ${failure}`);
            deadCodeMeta.skipped = true;
            reasons.push(`${client.id}:backoff:${failure}`);
            continue;
          }
          const startMs = Date.now();
          const scan = client.analyze(cwd);
          const settled2 = await bounded(withRowForFailureWrite(scan, cacheManager2, cacheKey, cwd, prev), {
            signal: deps.signal,
            hook: "turn_end",
            label: "dead-code",
            ms: Math.max(0, HOOK_WALL_BUDGET_MS.turn_end - (Date.now() - turnEndStart))
          });
          const joined = settled2 !== void 0 && joinedEarlierScan(settled2.result, startMs);
          if (settled2 === void 0 || joined) {
            if (joined) {
              reasons.push(`${client.id}:joined`);
            } else {
              dbg(`turn_end: dead-code(${client.id}) outlived the budget`);
              deadCodeMeta.execution = "deferred";
              deadCodeMeta.aborted = deps.signal?.aborted === true;
              reasons.push(`${client.id}:deferred`);
            }
            parkLateDeadCodeScan({
              scope: turnScope,
              cacheManager: cacheManager2,
              client,
              cwd,
              cacheKey,
              scan,
              startedAt: startMs,
              files: scanFiles,
              previousScan: prev?.data
            });
            if (joined)
              continue;
            break;
          }
          const { result } = settled2;
          const durationMs = Date.now() - startMs;
          if (wouldPoisonCache(settled2.row, result)) {
            dbg(`turn_end: keeping last good dead-code(${client.id}) cache; this run failed: ${result.summary}`);
            deadCodeMeta.cacheKept = true;
          } else {
            cacheManager2.writeCache(cacheKey, result, cwd, {
              scanDurationMs: durationMs
            });
          }
          logDeadCodeScan(deadCodeScanEvent(client, cwd, result, durationMs));
          deadCodeMeta.success = result.success;
          if (result.excludedWorktrees !== void 0) {
            deadCodeMeta.excludedWorktrees = (deadCodeMeta.excludedWorktrees ?? 0) + result.excludedWorktrees;
          }
          if (!result.success) {
            reasons.push(`${client.id}:scan_failed:${result.summary}`);
            continue;
          }
          sources.push({
            result,
            previousScan: prev?.data,
            modified: scanFiles
          });
        } catch (err) {
          dbg(`turn_end: dead-code(${client.id}) failed: ${err}`);
          reasons.push(`${client.id}:threw`);
        } finally {
          try {
            deliverDeadCodeDelta(client, sources);
          } catch (err) {
            dbg(`turn_end: dead-code(${client.id}) delivery failed: ${err}`);
            reasons.push(`${client.id}:threw`);
          }
        }
      }
    } catch (err) {
      dbg(`turn_end: dead-code block failed: ${err}`);
      reasons.push("block_threw");
    }
    deadCodeMeta.newIssues = newIssueTotal;
    deadCodeMeta.dispositionSuppressed = deadCodeDispositionSuppressed;
    if (reasons.length > 0)
      deadCodeMeta.reason = reasons.join(",");
  }
  logLatency({
    type: "phase",
    toolName: "turn_end",
    filePath: cwd,
    phase: "dead-code",
    durationMs: Date.now() - tDeadCode,
    metadata: deadCodeMeta
  });
  let dispositionSuppressedTotal = 0;
  const dispositionSuppressedByLane = {};
  function recordDispositionSuppressed(lane, count) {
    if (count <= 0)
      return;
    dispositionSuppressedTotal += count;
    dispositionSuppressedByLane[lane] = (dispositionSuppressedByLane[lane] ?? 0) + count;
  }
  const scannerCacheReads = /* @__PURE__ */ new Map();
  const scannerStoresUnread = [];
  function readScannerCache(scanner) {
    let pending2 = scannerCacheReads.get(scanner);
    if (pending2 === void 0) {
      pending2 = deps.signal?.aborted ? Promise.resolve(null) : bounded(cacheManager2.readCacheAsync(scanner, cwd), {
        ms: HOOK_WALL_BUDGET_MS.turn_end,
        signal: deps.signal,
        hook: "turn_end",
        label: `readScannerCache:${scanner}`
      }).then((entry) => {
        if (entry === void 0)
          scannerStoresUnread.push(scanner);
        return entry ?? null;
      });
      scannerCacheReads.set(scanner, pending2);
    }
    return pending2;
  }
  const laneCtx = {
    cwd,
    signal: deps.signal,
    readScannerCache,
    peekActionableWarnings: () => runtime.peekActionableWarnings(sessionId)
  };
  const govSources = await govulncheckLane.collect(laneCtx);
  const trivyCacheEntry = await readScannerCache("trivy");
  const secretsSources = await secretsLane.collect(laneCtx);
  if (scannerStoresUnread.length > 0) {
    logLatency({
      type: "phase",
      toolName: "turn_end",
      filePath: cwd,
      phase: "scanner_cache_read_abandoned",
      durationMs: 0,
      metadata: {
        stores: scannerStoresUnread.join("+"),
        aborted: deps.signal?.aborted === true
      }
    });
  }
  const scannerGates = gateFindingsByPathFreshness({
    cwd,
    sources: {
      ...govSources,
      ...secretsSources
    }
  });
  const govGate = scannerGates.govulncheck;
  const gitleaksGate = scannerGates.gitleaks;
  const trivySecretsGate = scannerGates["trivy-secrets"];
  const govDelivery = govulncheckLane.render(govulncheckLane.gate({ govulncheck: govGate }, laneCtx), laneCtx);
  for (const [store, count] of Object.entries(govDelivery.dispositionSuppressed ?? {})) {
    recordDispositionSuppressed(store, count);
  }
  advisoryParts.push(...govDelivery.advisoryParts ?? []);
  const secretsDelivery = secretsLane.render(secretsLane.gate({
    gitleaks: gitleaksGate,
    "trivy-secrets": trivySecretsGate
  }, laneCtx), laneCtx);
  for (const [store, count] of Object.entries(secretsDelivery.dispositionSuppressed ?? {})) {
    recordDispositionSuppressed(store, count);
  }
  const secretBlockedLocations = secretsDelivery.deliveredLocationKeys ?? /* @__PURE__ */ new Set();
  blockerParts.push(...secretsDelivery.blockerParts ?? []);
  staleSecretParts.push(...secretsDelivery.staleSecretParts ?? []);
  const trivyAgeLabel = formatCacheAgeLabel(trivyCacheEntry?.data?.scannedAt);
  const trivyFindingsFiltered = filterFindingsByDisposition(trivyCacheEntry?.data?.findings ?? [], cwd, (f) => trivyFindingToProjectDiagnostic(cwd, f));
  recordDispositionSuppressed("trivy", trivyFindingsFiltered.suppressed);
  if (trivyFindingsFiltered.kept.length) {
    const all = trivyFindingsFiltered.kept;
    const critical = all.filter((f) => f.severity === "CRITICAL");
    const advisory = all.filter((f) => f.severity !== "CRITICAL");
    const fmt = (f) => {
      const pkg = f.installedVersion ? `${f.pkgName}@${f.installedVersion}` : f.pkgName;
      const fix = f.fixedVersion ? ` \u2014 upgrade to ${f.fixedVersion} or later` : " \u2014 no fix yet, track upstream";
      return `  ${f.vulnerabilityId} (${pkg})${fix}
`;
    };
    if (critical.length) {
      const shown2 = critical.slice(0, 5);
      let report = `\u{1F534} STOP \u2014 CRITICAL dependency CVEs (trivy, ${trivyAgeLabel}). Upgrade before shipping:
`;
      for (const f of shown2)
        report += fmt(f);
      if (critical.length > shown2.length) {
        report += `  \u2026 and ${critical.length - shown2.length} more
`;
      }
      blockerParts.push(report);
    }
    if (advisory.length) {
      const shown2 = advisory.slice(0, 5);
      let report = `\u{1F6E1}\uFE0F Dependency CVEs (trivy, ${trivyAgeLabel}) \u2014 upgrade where possible:
`;
      for (const f of shown2)
        report += fmt(f);
      if (advisory.length > shown2.length) {
        report += `  \u2026 and ${advisory.length - shown2.length} more
`;
      }
      advisoryParts.push(report);
    }
  }
  const licenses = trivyCacheEntry?.data?.licenses ?? [];
  if (licenses.length) {
    const shown2 = licenses.slice(0, 5);
    let report = `\u{1F4DC} Dependency license risk (trivy, ${trivyAgeLabel}) \u2014 review for compliance:
`;
    for (const l of shown2) {
      const cat = l.category ? `, ${l.category}` : "";
      report += `  ${l.pkgName} \u2014 ${l.license} (${l.severity}${cat})
`;
    }
    if (licenses.length > shown2.length) {
      report += `  \u2026 and ${licenses.length - shown2.length} more
`;
    }
    advisoryParts.push(report);
  }
  if (dispositionSuppressedTotal > 0) {
    const byLane = Object.entries(dispositionSuppressedByLane).map(([lane, count]) => `${lane} ${count}`).join(", ");
    advisoryParts.push(`suppressed by disposition: ${dispositionSuppressedTotal} finding(s) dropped from this turn's gitleaks/govulncheck/trivy sections (${byLane}) (marked false-positive or won't-fix).`);
  }
  const t3 = Date.now();
  let madgeStats;
  if (getFlag("lens-turn-end-madge") && await depChecker.ensureAvailable()) {
    const madgeFiles = cacheManager2.getFilesForMadge(cwd);
    if (madgeFiles.length > 0) {
      dbg(`turn_end: madge checking ${madgeFiles.length} file(s) for circular deps`);
      const absFiles = madgeFiles.map((file) => path23.resolve(cwd, file));
      const batch = await depChecker.checkFilesBatch(absFiles, cwd);
      const depResults = batch.results;
      madgeStats = batch.stats;
      for (const file of madgeFiles) {
        const absPath = path23.resolve(cwd, file);
        const depResult = depResults.get(absPath);
        if (!depResult)
          continue;
        if (depResult.hasCircular && depResult.circular.length > 0) {
          dbg(`turn_end: circular dependency note for ${file} (suppressed in blockers-only mode)`);
        }
      }
    }
  }
  logLatency({
    type: "phase",
    toolName: "turn_end",
    filePath: cwd,
    phase: "madge",
    durationMs: Date.now() - t3,
    // A ~0ms entry with no metadata is indistinguishable from "ran and was
    // fast" when re-analyzing the #766 tail — mark the skipped case.
    metadata: madgeStats ?? { skipped: true }
  });
  if (!getFlag("no-tests") && files.length > 0) {
    const seen = /* @__PURE__ */ new Set();
    const targets = [];
    const testRootFor = createTurnEndTestRoots(cwd);
    const candidates = [];
    const seenCandidateKeys = /* @__PURE__ */ new Set();
    for (const file of files) {
      const abs = resolveRunnerPath(cwd, file);
      const key = normalizeMapKey(abs);
      if (seenCandidateKeys.has(key))
        continue;
      seenCandidateKeys.add(key);
      candidates.push({ display: file, abs, isNeighbor: false });
    }
    for (const result of cascadeResults) {
      for (const neighbor of result.neighbors) {
        const abs = path23.isAbsolute(neighbor.filePath) ? neighbor.filePath : resolveRunnerPath(cwd, neighbor.filePath);
        const key = normalizeMapKey(abs);
        if (seenCandidateKeys.has(key))
          continue;
        seenCandidateKeys.add(key);
        candidates.push({ display: neighbor.filePath, abs, isNeighbor: true });
      }
    }
    const priorTestCache = cacheManager2.readCache("test-runner-findings", cwd)?.data;
    const carriedDeferred = priorTestCache?.deferredTargets ?? [];
    const turnSessionId = sessionId ?? runtime.telemetrySessionId;
    const isThisSession = (entry) => entry.sessionId === turnSessionId;
    const entryPathKey = (entry) => normalizeMapKey(path23.resolve(cwd, entry.testFile));
    const retiredCarry = (priorTestCache?.retiredTargets ?? []).filter(isThisSession);
    const retiredKeys = new Set(retiredCarry.map(entryPathKey));
    const resolvedDeferralKeys = /* @__PURE__ */ new Set();
    const heldDeferred = [];
    const boundPersisted = (entries, label) => {
      const ordered = [
        ...entries.filter((entry) => !isThisSession(entry)),
        ...entries.filter(isThisSession)
      ];
      if (ordered.length <= TEST_RUNNER_MAX_PERSISTED_TARGETS)
        return ordered;
      dbg(`turn_end: ${label} test target list held ${ordered.length} entries, bounded to the newest ${TEST_RUNNER_MAX_PERSISTED_TARGETS} (oldest foreign-session rows dropped)`);
      return ordered.slice(-TEST_RUNNER_MAX_PERSISTED_TARGETS);
    };
    const writeTestFindings = (record, ownDeferred) => {
      const live = cacheManager2.readCache("test-runner-findings", cwd)?.data;
      const own = mergeDeferredTargets(heldDeferred, ownDeferred);
      const ownKeys = new Set(own.map(entryPathKey));
      const merged = mergeDeferredTargets(live?.deferredTargets ?? [], own).filter((entry) => {
        if (!isThisSession(entry))
          return true;
        const key = entryPathKey(entry);
        return !resolvedDeferralKeys.has(key) || ownKeys.has(key);
      });
      cacheManager2.writeCache("test-runner-findings", {
        ...record,
        deferredTargets: boundPersisted(merged, "deferred"),
        retiredTargets: boundPersisted(mergeDeferredTargets(live?.retiredTargets ?? [], retiredCarry), "retired")
      }, cwd);
    };
    let deadDeferred = 0;
    let heldOverCap = 0;
    let foreignDeferred = 0;
    let redispatchedDeferred = 0;
    let retiredThisTurn = 0;
    for (const carried of carriedDeferred) {
      const testFile = path23.resolve(cwd, carried.testFile);
      const carriedKey = normalizeMapKey(testFile);
      if (!isThisSession(carried)) {
        foreignDeferred++;
        continue;
      }
      if (seen.has(carriedKey)) {
        resolvedDeferralKeys.add(carriedKey);
        continue;
      }
      const attempts = carried.attempts ?? 0;
      if (attempts >= TEST_RUNNER_MAX_DEFERRALS) {
        resolvedDeferralKeys.add(carriedKey);
        if (!retiredKeys.has(carriedKey)) {
          retiredKeys.add(carriedKey);
          retiredThisTurn++;
          retiredCarry.push({
            testFile,
            runner: carried.runner,
            attempts,
            sessionId: turnSessionId
          });
          incrementDegradationCount({
            kind: "test-runner-batch-capped",
            subject: `${cwd}:deferral-exhausted`,
            reason: `test target was cut after ${attempts} turn(s) at the turn-end budget and is retired for this session \u2014 too slow for a per-turn batch; run it explicitly (target: ${path23.relative(cwd, testFile)})`
          });
          dbg(`turn_end: retiring deferred test target ${path23.relative(cwd, testFile)} after ${attempts} cut batch(es) \u2014 too slow for the turn-end budget, run it explicitly`);
        }
        continue;
      }
      const config = RUNNERS[carried.runner];
      const carriedRoot = testRootFor(testFile);
      if (!config || isExcludedTestTarget(testFile, carriedRoot) || !fs17.existsSync(testFile)) {
        resolvedDeferralKeys.add(carriedKey);
        deadDeferred++;
        continue;
      }
      if (targets.length >= TEST_RUNNER_MAX_TARGETS) {
        heldOverCap++;
        heldDeferred.push({
          testFile,
          runner: carried.runner,
          sourceFile: carried.sourceFile ?? testFile,
          attempts,
          sessionId: turnSessionId
        });
        continue;
      }
      resolvedDeferralKeys.add(carriedKey);
      seen.add(carriedKey);
      redispatchedDeferred++;
      targets.push({
        testFile,
        sourceFile: carried.sourceFile ?? testFile,
        runner: carried.runner,
        config,
        strategy: "deferred",
        testRoot: carriedRoot,
        deferralAttempts: attempts
      });
      dbg(`turn_end: re-running deferred test target ${path23.relative(cwd, testFile)} (${carried.runner}) from the previous turn's cut batch`);
    }
    if (deadDeferred > 0) {
      dbg(`turn_end: dropped ${deadDeferred} deferred test target(s) that no longer resolve (missing file, excluded, or unknown runner)`);
    }
    if (heldOverCap > 0) {
      dbg(`turn_end: held ${heldOverCap} deferred test target(s) over the per-turn cap of ${TEST_RUNNER_MAX_TARGETS} \u2014 kept on the deferral list for the next turn, attempts unchanged`);
    }
    if (foreignDeferred > 0) {
      dbg(`turn_end: ignored ${foreignDeferred} deferred test target(s) belonging to another session \u2014 left on the record for the session that cut them`);
    }
    if (carriedDeferred.length > 0) {
      logLatency({
        type: "phase",
        toolName: "turn_end",
        filePath: cwd,
        phase: "test_runner_deferral",
        durationMs: 0,
        metadata: {
          carried: carriedDeferred.length,
          redispatched: redispatchedDeferred,
          heldOverCap,
          retired: retiredThisTurn,
          dropped: deadDeferred,
          foreignSession: foreignDeferred,
          sessionId: turnSessionId
        }
      });
    }
    let overCapTargets = 0;
    let missingTargetFiles = 0;
    let excludedTargets = 0;
    let retiredSkips = 0;
    let noTestFile = 0;
    const candidatesByRoot = /* @__PURE__ */ new Map();
    for (const { display, abs, isNeighbor } of candidates) {
      const testRoot = testRootFor(abs);
      candidatesByRoot.set(testRoot, (candidatesByRoot.get(testRoot) ?? 0) + 1);
      const target = testRunnerClient.getTestRunTarget(abs, testRoot, runtime.turnIndex);
      const targetKey = target ? normalizeMapKey(target.testFile) : "";
      if (target && !seen.has(targetKey)) {
        seen.add(targetKey);
        if (retiredKeys.has(targetKey)) {
          retiredSkips++;
          dbg(`turn_end: ${display} \u2192 test target retired earlier this session (outran the turn-end batch budget), skipping spawn (${path23.relative(cwd, target.testFile)})`);
          continue;
        }
        if (isExcludedTestTarget(target.testFile, testRoot)) {
          excludedTargets++;
          dbg(`turn_end: ${display} \u2192 test target excluded by the built-in turn-end policy, skipping spawn (${path23.relative(cwd, target.testFile)})`);
          continue;
        }
        if (!fs17.existsSync(target.testFile)) {
          missingTargetFiles++;
          dbg(`turn_end: ${display} \u2192 test file missing, skipping spawn (${path23.relative(cwd, target.testFile)})`);
          continue;
        }
        if (targets.length >= TEST_RUNNER_MAX_TARGETS) {
          overCapTargets++;
          continue;
        }
        targets.push({ ...target, sourceFile: abs, testRoot });
        dbg(`turn_end: ${display} \u2192 test ${target.runner} ${path23.relative(cwd, target.testFile)} (${target.strategy}${isNeighbor ? ", cascade-neighbor" : ""})`);
      } else if (!target) {
        noTestFile++;
        dbg(`turn_end: ${display} \u2192 no test file found${isNeighbor ? " (cascade-neighbor)" : ""}`);
      }
    }
    if (excludedTargets > 0) {
      dbg(`turn_end: excluded ${excludedTargets} test target(s) by the built-in turn-end policy`);
    }
    if (retiredSkips > 0) {
      dbg(`turn_end: skipped ${retiredSkips} test target(s) retired earlier this session for outrunning the turn-end batch budget`);
    }
    if (missingTargetFiles > 0) {
      dbg(`turn_end: skipped ${missingTargetFiles} test target(s) whose file no longer exists`);
    }
    if (overCapTargets > 0) {
      recordDegradationOnce({
        kind: "test-runner-batch-capped",
        subject: cwd,
        reason: `turn touched more test targets than one turn may fire; ran ${TEST_RUNNER_MAX_TARGETS}, skipped ${overCapTargets} \u2014 re-run the remainder with lens_diagnostics or edit them in a smaller batch`
      });
      dbg(`turn_end: test target count capped at ${TEST_RUNNER_MAX_TARGETS}, ${overCapTargets} skipped`);
    }
    if (candidates.length > 0) {
      const selectedByRoot = /* @__PURE__ */ new Map();
      for (const target of targets) {
        selectedByRoot.set(target.testRoot, (selectedByRoot.get(target.testRoot) ?? 0) + 1);
      }
      const roots = [...candidatesByRoot.entries()].map(([root, candidateCount]) => ({
        root: root === cwd ? "." : toRunnerDisplayPath(cwd, root),
        candidates: candidateCount,
        selected: selectedByRoot.get(root) ?? 0
      }));
      emitBounded("turn_end_test_selection", `${cwd}:turn:${runtime.turnIndex}`, {
        filePath: cwd,
        durationMs: 0,
        metadata: {
          sessionId: turnSessionId,
          candidates: candidates.length,
          selected: targets.length,
          noTestFile,
          excluded: excludedTargets,
          missing: missingTargetFiles,
          overCap: overCapTargets,
          retired: retiredSkips,
          roots: roots.slice(0, TURN_END_SELECTION_ROOT_ROWS),
          rootsOmitted: Math.max(0, roots.length - TURN_END_SELECTION_ROOT_ROWS)
        }
      });
    }
    if (targets.length > 0) {
      for (const target of targets) {
        target.fileSeqAtRun = runtime.getFileSeq(target.sourceFile);
      }
      dbg(`turn_end: firing ${targets.length} test target(s) async (non-blocking, max ${TEST_RUNNER_BATCH_CONCURRENCY} concurrent)`);
      const firedAtTurn = runtime.turnIndex;
      const firedSessionId = turnSessionId;
      const testRunGeneration = (priorTestCache?.testRunGeneration ?? 0) + 1;
      const provenanceFiles = [
        ...candidates.map((candidate) => ({
          path: candidate.abs,
          role: "source"
        })),
        ...targets.map((target) => ({
          path: target.testFile,
          role: "test"
        }))
      ];
      const launchedFrom = snapshotAdvisoryProvenance({
        cwd,
        runtime,
        generation: testRunGeneration,
        files: provenanceFiles
      });
      writeTestFindings(
        {
          ...priorTestCache ?? { content: "" },
          testRunGeneration
        },
        // Consumed above into `targets` (or retired, or dropped). Anything
        // this turn HELD is re-asserted by the writer itself; the batch's own
        // outcome adds the NEW cut set below.
        []
      );
      runTestTargetsBounded({
        targets,
        concurrency: TEST_RUNNER_BATCH_CONCURRENCY,
        budgetMs: TEST_RUNNER_BATCH_BUDGET_MS,
        // Both bounds, per AGENTS.md: a wall budget AND the ambient
        // abort signal the rest of the spawn layer already honours.
        signal: getAmbientAbortSignal(),
        run: (t, batchSignal) => testRunnerClient.runTestFileAsync(t.testFile, t.testRoot, {
          runner: t.runner,
          config: t.config,
          turnIndex: firedAtTurn,
          // #2522 R2 F1: the BATCH's signal, so spending the 20s wall
          // budget kills this spawn rather than letting it run out its
          // own 60s timeout behind a batch that has already returned.
          signal: batchSignal,
          // #3871 r2: a failure in a linked worktree reads relative to the
          // session checkout (`.worktrees/x/tests/a.test.ts:12`), which
          // may hold a file at the worktree-relative path.
          displayRoot: cwd,
          // A worktree root without its own runner install is skipped, not
          // run through `npx` (a network fetch) or a bare interpreter.
          requireOwnInstall: t.testRoot !== cwd
        })
      }).then(({ results: batchResults, deferred, stopReason }) => {
        const settledResults = batchResults;
        const results = [];
        for (const result of settledResults) {
          if (!("value" in result) || !result.value.notRun) {
            results.push(result);
            continue;
          }
          const skippedRoot = targets.find((candidate) => candidate.testFile === result.value.file)?.testRoot ?? cwd;
          const ambientNote = result.value.runner === "pytest" && hasAmbientPythonEnvironment() ? "; ambient Python environments outside this checkout were not borrowed" : "";
          incrementDegradationCount({
            kind: "turn-end-test-root-skipped",
            subject: result.value.notRun,
            reason: `${toRunnerDisplayPath(cwd, skippedRoot)} has no ${result.value.runner} install of its own${ambientNote}; its tests were skipped rather than fetched through npx or run in another environment`
          });
          dbg(`turn_end: skipped ${toRunnerDisplayPath(cwd, result.value.file)}: ${result.value.notRun} in ${toRunnerDisplayPath(cwd, skippedRoot)}`);
        }
        const verdicts = results.flatMap((result) => {
          if (result.status === "rejected")
            return [];
          const target = targets.find((candidate) => candidate.testFile === result.value.file);
          return target ? [
            {
              file: result.value.file,
              sourceFile: target.sourceFile,
              fileSeq: target.fileSeqAtRun === void 0 ? {
                state: "unknown",
                reason: "sequence-unavailable"
              } : {
                state: "known",
                value: target.fileSeqAtRun
              }
            }
          ] : [];
        });
        const deferredTargets = deferred.map((t) => ({
          testFile: t.testFile,
          sourceFile: t.sourceFile,
          runner: t.runner,
          // One more cut batch for this target. Read back by the
          // selection loop above, which retires it at
          // TEST_RUNNER_MAX_DEFERRALS rather than carrying it forever.
          attempts: (t.deferralAttempts ?? 0) + 1,
          // #2522 R3 F5: stamped with the session that cut it, so the
          // next session re-arms this target instead of inheriting an
          // attempt count measured under a load it never saw.
          sessionId: firedSessionId
        }));
        if (deferred.length > 0) {
          incrementDegradationCount({
            kind: "test-runner-batch-capped",
            subject: `${cwd}:${stopReason ?? "incomplete"}`,
            reason: `test batch stopped early (${stopReason ?? "incomplete"}); ${deferred.length} target(s) killed or never dispatched, deferred to the next turn`
          });
          dbg(`turn_end: test batch stopped early (${stopReason ?? "incomplete"}), ${deferred.length} target(s) deferred to the next turn`);
        }
        const deferralNote = deferred.length > 0 ? `${deferred.length} test target(s) did not finish within the turn-end batch budget and are deferred to the next turn (they run first): ${deferredTargets.map((t) => path23.relative(cwd, t.testFile)).join(", ")}` : "";
        const supersededByNewerGeneration = (label) => {
          const current = cacheManager2.readCache("test-runner-findings", cwd)?.data;
          const currentGeneration = current?.testRunGeneration;
          if (currentGeneration !== void 0 && currentGeneration > testRunGeneration) {
            dbg(`turn_end: ${label}test generation ${testRunGeneration} superseded by ${currentGeneration}`);
            if (deferredTargets.length > 0) {
              writeTestFindings({ ...current ?? { content: "" } }, deferredTargets);
              dbg(`turn_end: ${label}carried ${deferredTargets.length} cut target(s) into the newer generation's deferral set`);
            }
            return true;
          }
          return false;
        };
        const publishedAgainst = snapshotAdvisoryProvenance({
          cwd,
          runtime,
          generation: testRunGeneration,
          files: provenanceFiles
        });
        const superseded = launchedFrom.revision.sessionId !== publishedAgainst.revision.sessionId || launchedFrom.revision.projectSeq !== publishedAgainst.revision.projectSeq || launchedFrom.revision.turnIndex !== publishedAgainst.revision.turnIndex || launchedFrom.files.some((file, index) => publishedAgainst.files[index]?.sha256 !== file.sha256 || publishedAgainst.files[index]?.path !== file.path);
        const stale = runtime.turnIndex !== firedAtTurn;
        const failures = [];
        const resultValues = [];
        let rejectedCount = 0;
        let hasRealFailure = false;
        for (const r of results) {
          if (r.status === "rejected") {
            rejectedCount++;
            emitBounded("test_runner_delivery", `${cwd}:generation:${testRunGeneration}:rejected`, {
              filePath: cwd,
              durationMs: 0,
              metadata: {
                outcome: "runner-promise-rejected",
                sessionId: firedSessionId,
                generation: testRunGeneration,
                targetCount: targets.length,
                droppedDetailCount: 0,
                reason: String(r.reason).slice(0, 500)
              }
            }, {
              ledgerKind: "test-runner-delivery",
              reason: "test runner promise rejected",
              capPerTurn: { limit: 8, turnIndex: firedAtTurn }
            });
            dbg(`turn_end: test run rejected \u2014 ${r.reason}`);
            continue;
          }
          resultValues.push(r.value);
          const { file, runner, passed, failed, duration, error } = r.value;
          if (failed > 0)
            hasRealFailure = true;
          const shortFile = path23.basename(file);
          const elapsed = formatRunDurationMs(duration);
          const verdict = failed > 0 ? "FAIL" : "PASS";
          const summary = isRunnerErrorResult(r.value) ? passed > 0 ? `error: ${error} (${passed} passed before)` : `error: ${error}` : `${verdict} ${passed}p/${failed}f (${elapsed})`;
          dbg(`turn_end: ${stale ? "[stale] " : ""}test ${runner} ${shortFile} \u2192 ${summary}`);
          if (failed > 0 || error) {
            if (error && String(r.value?.error ?? "").includes("Test file not found")) {
              continue;
            }
            const formatted = testRunnerClient.formatResult(r.value);
            if (formatted)
              failures.push(formatted);
          }
        }
        if (rejectedCount > 0) {
          failures.push(`Test runner rejected ${rejectedCount} promise(s) before producing a structured result.`);
        }
        if (failures.length > 0) {
          if (supersededByNewerGeneration(""))
            return;
          const content = [
            stale ? "[from a prior turn \u2014 the edit that triggered this run had already been superseded by the time results came back]" : "",
            failures.join("\n\n"),
            // A batch can BOTH find a real failure and be cut short.
            // The agent needs to see the failure AND that the run was
            // incomplete, or it reads a partial list as the full one.
            deferralNote
          ].filter(Boolean).join("\n\n");
          const runnerErrorOnly = !hasRealFailure;
          writeTestFindings({
            content,
            stale,
            results: resultValues,
            verdicts,
            testRunGeneration,
            launchedFrom,
            publishedAgainst,
            provenance: publishedAgainst,
            superseded,
            runnerErrorOnly
          }, deferredTargets);
          try {
            deps.onTestRunnerComplete?.({
              cwd,
              sessionId: firedSessionId,
              generation: testRunGeneration,
              targetCount: targets.length,
              hasFindings: true
            });
          } catch (deliveryErr) {
            dbg(`turn_end: test delivery staging failed \u2014 ${deliveryErr}`);
          }
          if (getFlag("lens-guard") && firedSessionId === runtime.telemetrySessionId) {
            const cleanFiles = resultValues.filter((value) => value.failed === 0 && !value.error).map((value) => value.file);
            if (cleanFiles.length > 0) {
              clearGitGuardTestFailure(cacheManager2, cwd, runtime, cleanFiles);
            }
            if (!runnerErrorOnly) {
              mergeGitGuardTestFailure(cacheManager2, cwd, runtime, content, resultValues.filter((value) => value.failed > 0).map((value) => value.file));
            } else {
              emitBounded("test_runner_delivery", `${cwd}:generation:${testRunGeneration}:runner-error-only`, {
                filePath: cwd,
                durationMs: 0,
                metadata: {
                  outcome: "runner-error-only-not-blocking",
                  sessionId: firedSessionId,
                  generation: testRunGeneration,
                  targetCount: targets.length,
                  droppedDetailCount: 0
                }
              }, {
                ledgerKind: "test-runner-delivery",
                reason: "runner-error-only batch kept off the --lens-guard blocker",
                capPerTurn: { limit: 8, turnIndex: firedAtTurn }
              });
            }
          }
          dbg(`turn_end: ${failures.length} test failure(s) cached for pull diagnostics and post-agent delivery${stale ? " (stale \u2014 turn advanced while tests ran)" : ""}`);
        } else if (deferred.length > 0) {
          if (supersededByNewerGeneration("deferred "))
            return;
          writeTestFindings({
            content: deferralNote,
            stale,
            results: resultValues,
            verdicts,
            testRunGeneration,
            launchedFrom,
            publishedAgainst,
            provenance: publishedAgainst,
            superseded,
            // Advisory framing: an unfinished batch is not a
            // failure the agent introduced, exactly like a runner
            // error. See `TestRunnerFindingsCache.runnerErrorOnly`.
            runnerErrorOnly: true
          }, deferredTargets);
          try {
            deps.onTestRunnerComplete?.({
              cwd,
              sessionId: firedSessionId,
              generation: testRunGeneration,
              targetCount: targets.length,
              hasFindings: true
            });
          } catch (deliveryErr) {
            dbg(`turn_end: test delivery staging failed \u2014 ${deliveryErr}`);
          }
          dbg(`turn_end: partial test batch \u2014 ${results.length} target(s) settled, ${deferred.length} deferred to the next turn; NOT recorded as a clean run`);
        } else if (results.length > 0) {
          if (supersededByNewerGeneration("clean "))
            return;
          writeTestFindings(
            {
              ...priorTestCache ?? { content: "" },
              content: "",
              stale: false,
              results: resultValues,
              testRunGeneration,
              launchedFrom,
              publishedAgainst,
              provenance: publishedAgainst,
              superseded
            },
            // Nothing was cut. This turn's own settled entries drop out;
            // I2 keeps every row this batch does not own — including a
            // concurrent older batch's hand-over.
            []
          );
          try {
            deps.onTestRunnerComplete?.({
              cwd,
              sessionId: firedSessionId,
              generation: testRunGeneration,
              targetCount: targets.length,
              hasFindings: false
            });
          } catch (deliveryErr) {
            dbg(`turn_end: test delivery staging failed \u2014 ${deliveryErr}`);
          }
          if (getFlag("lens-guard") && firedSessionId === runtime.telemetrySessionId) {
            clearGitGuardTestFailure(cacheManager2, cwd, runtime, resultValues.map((value) => value.file));
          }
          dbg(`turn_end: all tests passed${stale ? " (stale \u2014 turn advanced while tests ran)" : ""}`);
        }
      }).catch(() => {
      });
    } else if (carriedDeferred.length > 0) {
      writeTestFindings({ ...priorTestCache ?? { content: "" } }, []);
    }
  }
  if (runtime.errorDebtBaseline && files.length > 0) {
    dbg("turn_end: marking error debt check for next session");
    cacheManager2.writeCache("errorDebt", {
      pendingCheck: true,
      baselineTestsPassed: runtime.errorDebtBaseline.testsPassed
    }, cwd);
  }
  if (runtime.callGraph && files.length > 0) {
    const coverage = runtime.callGraph.coverage;
    if (!coverage || coverage.complete !== true) {
      advisoryParts.push("Call-graph impact was not emitted because call-graph extraction coverage is incomplete; the affected files may have unreported callers.");
    } else {
      const callGraphStart = Date.now();
      try {
        const { impact, formatImpact, parseSymbolKey } = await import("../chunk-FJNKANOS.js");
        const { callGraphImpactToProjectDiagnostics } = await import("../chunk-47D2BO4S.js");
        const impactLines = [];
        let callGraphDispositionSuppressed = 0;
        const impactFindings = [];
        const impactFiles = files.slice(0, 5);
        const impactFileKeys = new Set(impactFiles.map((file) => normalizeMapKey(resolveRunnerPath(cwd, file))));
        const reOfferedImpactFiles = takeParked(carryLane("call-graph")).filter((file) => !impactFileKeys.has(normalizeMapKey(resolveRunnerPath(cwd, file))));
        const shownImpactFiles = [];
        for (const filePath of [...impactFiles, ...reOfferedImpactFiles]) {
          const linesBefore = impactLines.length;
          const changedFileKey = normalizeMapKey(resolveRunnerPath(cwd, filePath));
          const fileCallerKeys = [...runtime.callGraph.callers.keys()].filter((k) => {
            const graphFilePath = parseSymbolKey(k).filePath;
            return normalizeMapKey(resolveRunnerPath(cwd, graphFilePath)) === changedFileKey;
          });
          for (const calleeKey of fileCallerKeys.slice(0, 3)) {
            const results = impact(runtime.callGraph, calleeKey).filter((r) => {
              const callerFile = parseSymbolKey(r.symbolKey).filePath;
              return !callerFile || !isTestRoleCollateral(resolveRunnerPath(cwd, callerFile));
            });
            if (results.length > 0) {
              impactFindings.push({ calleeKey, results });
              const markable = results.flatMap((result) => callGraphImpactToProjectDiagnostics(cwd, [
                { calleeKey, results: [result] }
              ]).map((diagnostic) => ({ result, diagnostic })));
              const filtered = filterFindingsByDisposition(markable, cwd, (entry) => entry.diagnostic);
              callGraphDispositionSuppressed += filtered.suppressed;
              const keptEntries = new Set(filtered.kept);
              const dropped = new Set(markable.filter((entry) => !keptEntries.has(entry)).map((entry) => entry.result));
              const survivors = dropped.size === 0 ? results : results.filter((r) => !dropped.has(r));
              const summary = survivors.length > 0 ? formatImpact(survivors, cwd) : "";
              if (summary)
                impactLines.push(`  ${parseSymbolKey(calleeKey).symbolName ?? calleeKey}: ${summary}`);
            }
          }
          if (impactLines.length > linesBefore)
            shownImpactFiles.push(filePath);
        }
        if (impactLines.length > 0) {
          const impactPart = `\u{1F4CA} Call-graph impact (changed symbols have callers):
${impactLines.join("\n")}`;
          advisoryParts.push(impactPart);
          deliveryHolds.push(cutAdvisoryHold("call-graph", carryLane("call-graph"), impactPart, shownImpactFiles, (file) => !impactFiles.includes(file)));
        }
        if (impactFindings.length > 0) {
          const impactDiagnostics = callGraphImpactToProjectDiagnostics(cwd, impactFindings);
          if (impactDiagnostics.length > 0) {
            projectDiagnosticsDelta.push(...impactDiagnostics);
            projectDiagnosticsSources.add("call-graph");
          }
        }
        if (impactFindings.length > 0 || callGraphDispositionSuppressed > 0) {
          logLatency({
            type: "phase",
            toolName: "turn_end",
            filePath: cwd,
            phase: "call_graph_impact",
            durationMs: Date.now() - callGraphStart,
            metadata: {
              callees: impactFindings.length,
              lines: impactLines.length,
              dispositionSuppressed: callGraphDispositionSuppressed
            }
          });
        }
      } catch {
      }
    }
  }
  if (projectDiagnosticsDelta.length > 0) {
    writeProjectDiagnosticsDeltaReport(cwd, {
      version: PROJECT_DIAGNOSTICS_CACHE_VERSION,
      cwd,
      generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
      sessionId: runtime.telemetrySessionId,
      turnIndex: runtime.turnIndex,
      projectSeqStart: runtime.turnStartProjectSeq,
      projectSeqEnd: runtime.projectSeq,
      diagnostics: projectDiagnosticsDelta,
      sources: [...projectDiagnosticsSources].sort((a, b) => a.localeCompare(b))
    });
  }
  const t4 = Date.now();
  const modifiedRangesByFile = new Map(Object.entries(turnState.files).map(([file, state]) => [
    normalizeMapKey(resolveRunnerPath(cwd, file)),
    state.modifiedRanges
  ]));
  const getFileSeq = runtime.getFileSeq;
  const fileSeqByPath = /* @__PURE__ */ new Map();
  if (getFileSeq) {
    for (const file of files) {
      const filePath = normalizeMapKey(resolveRunnerPath(cwd, file));
      fileSeqByPath.set(filePath, getFileSeq.call(runtime, filePath));
    }
  }
  let reverify;
  if (getFlag("lens-actionable-warnings")) {
    const persistedReport = cacheManager2.readCache("actionable-warnings", cwd, Number.MAX_SAFE_INTEGER)?.data;
    if (persistedReport?.files?.some((entry) => entry.origin === "deferred")) {
      const reverifyLspService = getLSPService();
      if (reverifyLspService) {
        reverify = await bounded(runPersistentReverify({
          report: persistedReport,
          cwd,
          lspService: reverifyLspService,
          signal: getAmbientAbortSignal()
        }), {
          ms: HOOK_WALL_BUDGET_MS.turn_end,
          signal: getAmbientAbortSignal(),
          hook: "turn_end",
          label: "persistent_reverify"
        });
      }
    }
    try {
      const report = await buildActionableWarningsReport({
        cwd,
        sessionId: runtime.telemetrySessionId,
        turnIndex: runtime.turnIndex,
        files,
        modifiedRangesByFile,
        dispatchWarnings: runtime.peekActionableWarnings(sessionId),
        includeLspCodeActions: !!getFlag("lens-actionable-warning-actions"),
        projectSeqStart: runtime.turnStartProjectSeq,
        projectSeqEnd: runtime.projectSeq,
        // #3676: the quick fix credits its writes with this epoch.
        branchEpoch: runtime.readGuard.currentBranchEpoch,
        branchScope: runtime.readGuard.lineageKey,
        fileSeqByPath,
        deltaOnly: !getFlag("lens-actionable-warning-all"),
        dbg,
        // #2504: this call is AWAITED on the turn_end hook, so its cost is
        // terminal-blocking time. The file cap and wall budget are its
        // bounds; the abort signal is the second one AGENTS.md requires.
        signal: getAmbientAbortSignal(),
        // A cold-cache turn hands the fresh-pull loop back here, off the
        // hook — it lands in the same cache the in-band report goes to.
        onDeferredReport: (deferred) => {
          try {
            writeDeferredActionableWarningsReport({
              cacheManager: cacheManager2,
              cwd,
              report: deferred,
              // The LIVE per-file sequence at WRITE time is the
              // baseline the merge judges each entry against. The
              // runtime is the only thing that knows it: a file
              // edited into cleanliness by a later turn is absent
              // from the persisted report entirely.
              getFileSeq: getFileSeq ? (filePath) => getFileSeq.call(runtime, filePath) : void 0,
              dbg
            });
          } catch (deferErr) {
            dbg(`turn_end: deferred actionable-warnings write failed \u2014 ${deferErr}`);
          }
        }
      });
      if (reverify?.replacementFiles.length) {
        const replacementByPath = new Map(reverify.replacementFiles.map((file) => [
          normalizeMapKey(file.filePath),
          file
        ]));
        const files2 = (report.files ?? []).map((file) => replacementByPath.get(normalizeMapKey(file.filePath)) ?? file);
        for (const replacement of reverify.replacementFiles) {
          if (!files2.some((file) => normalizeMapKey(file.filePath) === normalizeMapKey(replacement.filePath))) {
            files2.push(replacement);
          }
        }
        report.files = files2;
      }
      const publishResult = publishActionableWarningsReport(cacheManager2, cwd, report, {
        origin: "in-band",
        getFileSeq: getFileSeq ? (filePath) => getFileSeq.call(runtime, filePath) : void 0,
        dbg
      });
      const advisory = formatActionableWarningsAdvisory(publishResult.report, cwd, host, (report2) => {
        let dispositionSuppressed = 0;
        const files2 = report2.files.map((file) => {
          const policy = filterFindingsByDisposition(file.warnings, cwd, (warning) => {
            const { line, column, rule, code } = warning;
            return {
              filePath: warning.filePath,
              severity: warning.severity,
              semantic: "warning",
              tool: warning.tool,
              runner: warning.tool,
              message: warning.message,
              source: warning.origin === "lsp" ? "lsp" : "dispatch",
              ...line === void 0 ? {} : { line: warning.line },
              ...column === void 0 ? {} : { column: warning.column },
              ...rule === void 0 ? {} : { rule: warning.rule },
              ...code === void 0 ? {} : { code }
            };
          });
          dispositionSuppressed += policy.suppressed;
          const kept = policy.kept.filter((warning) => !(isSecretWarning(warning) && typeof warning.line === "number" && secretBlockedLocations.has(secretLocationKey(warning.filePath, warning.line))));
          return kept.length > 0 ? { ...file, warnings: kept } : void 0;
        }).filter((file) => file !== void 0);
        recordDispositionSuppressed("actionable-warnings", dispositionSuppressed);
        return { files: files2, suppressed: dispositionSuppressed };
      });
      if (advisory)
        advisoryParts.push(advisory);
      logActionableWarningsEvent({
        event: advisory ? "advisory_injected" : "advisory_skipped",
        sessionId: runtime.telemetrySessionId,
        metadata: {
          turnIndex: runtime.turnIndex,
          // #2504 review round 7 (F2): the MERGED report, matching the
          // advisory text above it (round 6, a) -- a rescued deferred
          // entry lives only on `publishResult.report`, and the pre-merge
          // `report` this turn assembled undercounts it. This is the value
          // scripts/analyze-pi-lens-logs.mjs sums, so an undercount here is
          // a silent miscount there too.
          unsuppressed: publishResult.report.summary.unsuppressed
        }
      });
      logLatency({
        type: "phase",
        toolName: "turn_end",
        filePath: cwd,
        phase: "actionable_warnings_report",
        durationMs: Date.now() - t4,
        metadata: report.summary
      });
    } catch (err) {
      dbg(`turn_end: actionable warning report failed: ${err}`);
      logLatency({
        type: "phase",
        toolName: "turn_end",
        filePath: cwd,
        phase: "actionable_warnings_report",
        durationMs: Date.now() - t4,
        metadata: {
          failed: true,
          error: err instanceof Error ? err.message : String(err)
        }
      });
    }
  }
  const t5 = Date.now();
  try {
    const qualityWarningsByFile = /* @__PURE__ */ new Map();
    for (const warning of runtime.peekCodeQualityWarnings(sessionId)) {
      const group = qualityWarningsByFile.get(warning.filePath);
      if (group)
        group.push(warning);
      else
        qualityWarningsByFile.set(warning.filePath, [warning]);
    }
    const qualityWarnings = [];
    let qualityDispositionSuppressed = 0;
    for (const [filePath, group] of qualityWarningsByFile) {
      let content;
      try {
        content = fs17.readFileSync(filePath, "utf-8");
      } catch {
        content = void 0;
      }
      const { kept, suppressed } = applyPushedFindingPolicy(group, {
        cwd,
        filePath,
        content
      });
      qualityWarnings.push(...kept);
      qualityDispositionSuppressed += suppressed;
    }
    const qualityReport = buildCodeQualityWarningsReport({
      cwd,
      sessionId: runtime.telemetrySessionId,
      turnIndex: runtime.turnIndex,
      warnings: qualityWarnings,
      modifiedRangesByFile,
      projectSeqStart: runtime.turnStartProjectSeq,
      projectSeqEnd: runtime.projectSeq,
      fileSeqByPath
    });
    writeCodeQualityWarningsReport(cacheManager2, cwd, qualityReport);
    appendCodeQualityWarningsHistory(cwd, qualityReport);
    const advisory = formatCodeQualityWarningsAdvisory(qualityReport, cwd, host);
    if (advisory)
      advisoryParts.push(advisory);
    logLatency({
      type: "phase",
      toolName: "turn_end",
      filePath: cwd,
      phase: "code_quality_warnings_report",
      durationMs: Date.now() - t5,
      metadata: {
        ...qualityReport.summary,
        // #3248: bounded per-turn on this lane's own row.
        dispositionSuppressed: qualityDispositionSuppressed
      }
    });
  } catch (err) {
    dbg(`turn_end: code quality warning report failed: ${err}`);
    logLatency({
      type: "phase",
      toolName: "turn_end",
      filePath: cwd,
      phase: "code_quality_warnings_report",
      durationMs: Date.now() - t5,
      metadata: {
        failed: true,
        error: err instanceof Error ? err.message : String(err)
      }
    });
  }
  cacheManager2.incrementTurnCycle(cwd, currentOwner);
  const lateAuxStart = Date.now();
  const { drainedPairs, lateAuxCapEvicted } = holdScope.guardedWrite("turn-end:late-aux", () => ({
    drainedPairs: drainPendingAuxiliaryCoverage(),
    lateAuxCapEvicted: drainPendingAuxCapEvictedCount()
  })) ?? { drainedPairs: [], lateAuxCapEvicted: 0 };
  const rearmLateAux = (pair) => holdScope.guardedWrite(
    // One subject for the store (#4168 F3): the row's count carries how many.
    "turn-end:late-aux-rearm",
    () => {
      rearmPendingAuxiliaryCoverage(pair);
      return true;
    }
  ) === true;
  let lateAuxDelivered = 0;
  let lateAuxStale = 0;
  let lateAuxMissing = 0;
  let lateAuxRearmed = 0;
  let lateAuxBacklogPending = 0;
  let lateAuxClientGone = 0;
  let lateAuxProbeFailed = 0;
  let lateAuxCleanConfirmed = 0;
  let lateAuxExpired = 0;
  let lateAuxCeilingExhausted = 0;
  let lateAuxAnswered = 0;
  let lateAuxNotifyStallDemoted = 0;
  let lateAuxHoldRearmed = 0;
  let lateAuxHoldExpired = 0;
  let lateAuxHoldCeiling = 0;
  let lateAuxDispositionSuppressed = 0;
  let lateAuxAuxSuppressed = 0;
  const lateAuxCoverageGapPairs = [];
  let lateAuxCoverageGapDetailCount = 0;
  let lateAuxCoverageGapDropCount = 0;
  const lateAuxStuckPairs = [];
  if (drainedPairs.length > 0) {
    const lateObserverDeadline = Date.now() + HOOK_WALL_BUDGET_MS.turn_end;
    const byFile = /* @__PURE__ */ new Map();
    for (const pair of drainedPairs) {
      const list = byFile.get(pair.filePath);
      if (list)
        list.push(pair);
      else
        byFile.set(pair.filePath, [pair]);
    }
    try {
      const service = getLSPService();
      for (const [lateAuxPath, pairs] of byFile) {
        let cached;
        try {
          cached = await service.readCachedDiagnosticsForServers(lateAuxPath, new Set(pairs.map((p) => p.serverId)));
        } catch {
          lateAuxProbeFailed += pairs.length;
          for (const pair of pairs) {
            if (canRearmPendingAuxiliary(pair)) {
              if (rearmLateAux(pair))
                lateAuxRearmed += 1;
            } else if (isPendingAuxiliaryPastRearmTtl(pair)) {
              lateAuxExpired += 1;
            } else {
              lateAuxCeilingExhausted += 1;
            }
          }
          continue;
        }
        const displayLateAuxPath = toRunnerDisplayPath(cwd, lateAuxPath);
        let lateAuxContentRead = false;
        let lateAuxContentValue;
        const readLateAuxContent = () => {
          if (!lateAuxContentRead) {
            lateAuxContentRead = true;
            try {
              lateAuxContentValue = fs17.readFileSync(lateAuxPath, "utf-8");
            } catch {
              lateAuxContentValue = void 0;
            }
          }
          return lateAuxContentValue;
        };
        for (const pair of pairs) {
          const cachedEntry = cached.get(pair.serverId);
          if (cachedEntry === void 0) {
            lateAuxClientGone += 1;
            continue;
          }
          if (cachedEntry.notifyStallDemoted) {
            if (cachedEntry.demotedAt === void 0 || pair.markedAtMs > cachedEntry.demotedAt) {
              lateAuxClientGone += 1;
              continue;
            }
            lateAuxNotifyStallDemoted += 1;
            const pastTtl = isPendingAuxiliaryPastRearmTtl(pair);
            const atCeiling = (pair.rearmCount ?? 0) >= MAX_LATE_AUX_REARMS;
            if (!pastTtl && !atCeiling) {
              if (rearmLateAux(pair)) {
                lateAuxRearmed += 1;
                if (lateAuxStuckPairs.length < 20)
                  lateAuxStuckPairs.push({
                    filePath: pair.filePath,
                    serverId: pair.serverId
                  });
              }
            } else {
              if (pastTtl)
                lateAuxExpired += 1;
              else
                lateAuxCeilingExhausted += 1;
              lateAuxCoverageGapPairs.push({
                filePath: pair.filePath,
                serverId: pair.serverId
              });
            }
            continue;
          }
          const rawDiags = cachedEntry.diags;
          const backlogPending = !isAuxBacklogPublished(pair);
          if (backlogPending)
            lateAuxBacklogPending += 1;
          if (cachedEntry.publishedAt === void 0 || cachedEntry.publishedAt <= pair.markedAtMs || backlogPending) {
            if (canRearmPendingAuxiliary(pair)) {
              if (rearmLateAux(pair)) {
                lateAuxRearmed += 1;
                if (lateAuxStuckPairs.length < 20)
                  lateAuxStuckPairs.push({
                    filePath: pair.filePath,
                    serverId: pair.serverId
                  });
              }
            } else {
              if (isPendingAuxiliaryPastRearmTtl(pair))
                lateAuxExpired += 1;
              else
                lateAuxCeilingExhausted += 1;
            }
            continue;
          }
          if (typeof service.observeLateAuxiliaryAnswer === "function") {
            await bounded(service.observeLateAuxiliaryAnswer(lateAuxPath, pair.serverId, cachedEntry.publishedAt - pair.markedAtMs), {
              ms: Math.max(1, lateObserverDeadline - Date.now()),
              signal: deps.signal,
              hook: "turn_end",
              label: "observeLateAuxiliaryAnswer"
            });
          }
          if (rawDiags.length === 0) {
            lateAuxCleanConfirmed += 1;
            continue;
          }
          const anchored = rawDiags.filter((d) => d.range?.start?.line !== void 0);
          if (anchored.length === 0) {
            lateAuxMissing += rawDiags.length;
            lateAuxAnswered += 1;
            continue;
          }
          const lateAuxContent = readLateAuxContent();
          const converted = convertLspDiagnostics(anchored, lateAuxPath);
          const retained = retagAuxiliaryDiagnostics(converted, anchored, lateAuxContent ?? "", { cwd, fileRole: detectFileRole(lateAuxPath, lateAuxContent) });
          lateAuxAuxSuppressed += converted.length - retained.length;
          if (retained.length === 0) {
            lateAuxAnswered += 1;
            continue;
          }
          const { "late-auxiliary-findings": gate3 } = gateFindingsByPathFreshness({
            cwd,
            sources: {
              "late-auxiliary-findings": {
                findings: retained,
                scannedAt: pair.markedAtMs,
                citedPath: () => lateAuxPath
              }
            }
          });
          lateAuxStale += gate3.stale.length;
          lateAuxMissing += retained.length - gate3.live.length - gate3.stale.length;
          if (gate3.live.length === 0) {
            if (gate3.stale.length > 0) {
              if (canRearmPendingAuxiliary(pair)) {
                if (rearmLateAux(pair))
                  lateAuxRearmed += 1;
              } else if (isPendingAuxiliaryPastRearmTtl(pair)) {
                lateAuxExpired += 1;
              } else {
                lateAuxCeilingExhausted += 1;
              }
            } else {
              lateAuxAnswered += 1;
            }
            continue;
          }
          const { kept: lateAuxKept, suppressed: lateAuxSuppressedHere } = applyPushedFindingPolicy(gate3.live, {
            cwd,
            filePath: lateAuxPath,
            content: lateAuxContent
          });
          lateAuxDispositionSuppressed += lateAuxSuppressedHere;
          if (lateAuxKept.length === 0) {
            lateAuxAnswered += 1;
            continue;
          }
          const lines = lateAuxKept.map((f) => `  ${displayLateAuxPath}:${f.line}:${f.column} [${f.rule}] ${f.message}`);
          lateAuxDelivered += lateAuxKept.length;
          lateAuxAnswered += 1;
          const lateAuxSuppressedNote = lateAuxSuppressedHere > 0 ? `; suppressed by disposition: ${lateAuxSuppressedHere} finding(s)` : "";
          const lateAuxPart = `\u{1F550} Late auxiliary diagnostics (${pair.serverId} answered after its grace window${lateAuxSuppressedNote}):
${lines.join("\n")}`;
          advisoryParts.push(lateAuxPart);
          deliveryHolds.push({
            part: lateAuxPart,
            canHold: () => canRearmPendingAuxiliary(pair),
            onHeld: () => {
              rearmPendingAuxiliaryCoverage(pair);
              lateAuxHoldRearmed += 1;
            },
            onDropped: () => {
              if (isPendingAuxiliaryPastRearmTtl(pair))
                lateAuxHoldExpired += 1;
              else
                lateAuxHoldCeiling += 1;
              incrementDegradationCount({
                kind: "late-auxiliary-held-dropped",
                subject: `late-auxiliary:${pair.serverId}`,
                reason: "cut by the turn-end cap and past its re-arm bound, so not re-armed"
              });
            }
          });
        }
      }
    } catch (err) {
      dbg(`turn_end: late-auxiliary probe failed: ${err}`);
    }
    for (const pair of lateAuxCoverageGapPairs) {
      const normalizedPairPath = normalizeMapKey(pair.filePath);
      const emitted2 = emitBounded("lsp_scanner_coverage_gap", `${pair.serverId}:${normalizedPairPath}`, {
        filePath: normalizedPairPath,
        durationMs: 0,
        metadata: {
          source: "late-auxiliary",
          serverIds: [pair.serverId],
          reason: "notify-stall-replacement-unavailable",
          reRaised: true
        }
      }, {
        ledgerKind: "lsp-scanner-coverage-gap",
        reason: "notify-stall replacement was not available before late-coverage ceiling",
        capPerTurn: {
          limit: LATE_AUX_COVERAGE_GAP_DETAIL_CAP_PER_TURN,
          turnIndex: runtime.turnIndex
        }
      });
      if (emitted2)
        lateAuxCoverageGapDetailCount += 1;
      else
        lateAuxCoverageGapDropCount += 1;
    }
    logLatency({
      type: "phase",
      toolName: "turn_end",
      filePath: cwd,
      phase: "late_auxiliary_findings",
      durationMs: Date.now() - lateAuxStart,
      metadata: {
        pending: drainedPairs.length,
        pairCreated: drainedPairs.length + lateAuxCapEvicted,
        pendingAfter: pendingAuxiliaryCoverageSize(),
        delivered: lateAuxDelivered,
        stale: lateAuxStale,
        missing: lateAuxMissing,
        rearmed: lateAuxRearmed,
        backlogPending: lateAuxBacklogPending,
        clientGone: lateAuxClientGone,
        probeFailed: lateAuxProbeFailed,
        cleanConfirmed: lateAuxCleanConfirmed,
        expired: lateAuxExpired,
        ceilingExhausted: lateAuxCeilingExhausted,
        answered: lateAuxAnswered,
        dispositionSuppressed: lateAuxDispositionSuppressed,
        auxSuppressed: lateAuxAuxSuppressed,
        notifyStallDemoted: lateAuxNotifyStallDemoted,
        coverageGapReRaised: lateAuxCoverageGapPairs.length,
        coverageGapReRaisedDetailed: lateAuxCoverageGapDetailCount,
        coverageGapReRaisedDropped: lateAuxCoverageGapDropCount,
        capEvicted: lateAuxCapEvicted,
        stuckPairs: lateAuxStuckPairs
      }
    });
  }
  const unresolvedKeys = new Set(unresolvedBlockers.map((record) => normalizeMapKey(path23.resolve(record.filePath))));
  const formatResolvedTail = (dropped) => `\u2026 and ${dropped} more`;
  const resolvedTailMax = formatResolvedTail(Number.MAX_SAFE_INTEGER).length;
  const resolvedShareChars = Math.floor(RUNTIME_CONFIG.turnEnd.maxChars * RESOLVED_SHARE_OF_CHARS) - resolvedTailMax;
  const formatResolvedLine = (entry) => {
    const clause = entry.confirmedClean ? " confirmed clean" : entry.writeIndex === void 0 ? "" : ` cleared by the ${formatWriteOrdinal(entry.writeIndex)} write`;
    const label = entry.turnIndex === runtime.turnIndex ? "Resolved this turn" : "Resolved since the last report";
    const head = `${label}: `;
    const tail = ` (${entry.blockerCount} blocker(s)${clause})`;
    let display = toRunnerDisplayPath(cwd, entry.filePath);
    const room = resolvedShareChars - 1 - head.length - tail.length;
    if (display.length > room) {
      const keepHead = Math.ceil((room - 1) / 2);
      display = `${sliceAtCodePointBoundaries(display, 0, keepHead)}\u2026${sliceAtCodePointBoundaries(display, display.length - (room - 1 - keepHead), display.length)}`;
    }
    return `${head}${display}${tail}`;
  };
  const blockerText = blockerParts.join("\n\n");
  const blockerChars = blockerParts.length > 0 ? blockerText.length + 2 : 0;
  const blockerLines = blockerParts.length > 0 ? blockerText.split("\n").length + 1 : 0;
  const resolvedCharBudget = Math.min(resolvedShareChars, RUNTIME_CONFIG.turnEnd.maxChars - blockerChars - resolvedTailMax);
  const resolvedLineBudget = Math.min(RESOLVED_MAX_LINES, RUNTIME_CONFIG.turnEnd.maxLines - blockerLines) - 1;
  let resolvedChars = 0;
  let resolvedCount = 0;
  const { files: resolvedBlockerFileList, dropped: resolvedBlockerFilesDropped } = runtime.consumeResolvedBlockerFiles((entry) => {
    if (unresolvedKeys.has(normalizeMapKey(path23.resolve(entry.filePath))))
      return true;
    const cost = formatResolvedLine(entry).length + 1;
    if (resolvedCount > 0 && (resolvedCount >= resolvedLineBudget || resolvedChars + cost > resolvedCharBudget))
      return true;
    resolvedChars += cost;
    resolvedCount += 1;
    return false;
  });
  const resolvedLines = resolvedBlockerFileList.map(formatResolvedLine);
  if (resolvedBlockerFilesDropped > 0) {
    resolvedLines.push(formatResolvedTail(resolvedBlockerFilesDropped));
  }
  const resolvedParts = resolvedLines.length > 0 ? [resolvedLines.join("\n")] : [];
  const asIs = (raw) => ({ raw, text: raw });
  const composedParts = [
    // #3218 criterion 2: the resolution lines ride FIRST, inside their
    // sized share, so the cap cuts neither a consumed retirement
    // (review-3776-verify V1) nor a live blocker (review-3776-r3 W1).
    // They are not a finding tier (no gate applies to a just-cleared
    // verdict), only a status line, and they are part of the block content
    // so an otherwise empty turn still delivers them ONCE (the coordinator
    // consumed them).
    ...resolvedParts.map(asIs),
    ...blockerParts.map(asIs),
    ...staleSecretParts.map(asIs),
    // #3814: a blocking late-runner finding never reaches `advisoryParts`; it
    // rides the blocker replay, so every advisory part gets the label.
    ...advisoryParts.map((raw) => ({
      raw,
      text: `\u2139\uFE0F Advisory \u2014 no action required this turn:
${raw}`
    }))
  ];
  const findingParts = composedParts.map((part) => part.text);
  let heldSections = 0;
  if (findingParts.length > 0) {
    dbg(`turn_end: ${blockerParts.length} blocker section(s), ${advisoryParts.length} advisory section(s) found, persisting for next context`);
    const joined = findingParts.join("\n\n");
    const holdPlan = planDeliveryHolds({
      holds: deliveryHolds,
      parts: composedParts,
      keptChars: turnEndKeptChars(joined),
      separatorLength: "\n\n".length
    });
    const settleHolds = (suppressed) => {
      const outcome = holdPlan.settle({
        suppressed,
        isCurrentSession: () => holdScope.isCurrent(),
        onFault: (cause) => dbg(`turn_end: delivery hold failed: ${cause}`)
      });
      heldSections = outcome.held;
      if (lateAuxHoldRearmed + lateAuxHoldExpired + lateAuxHoldCeiling > 0) {
        logLatency({
          type: "phase",
          toolName: "turn_end",
          filePath: cwd,
          phase: "late_auxiliary_holds",
          durationMs: 0,
          metadata: {
            rearmed: lateAuxHoldRearmed,
            expired: lateAuxHoldExpired,
            ceilingExhausted: lateAuxHoldCeiling,
            pendingAfter: pendingAuxiliaryCoverageSize()
          }
        });
      }
      if (outcome.held > 0) {
        incrementDegradationCount({
          kind: "turn-end-sections-held",
          subject: "turn-end",
          reason: `${outcome.held} one-shot section(s) cut by the turn-end cap, kept pending for the next turn`
        });
      }
    };
    const content = capTurnEndMessage(joined, holdPlan.heldCount);
    const signature = `${files.slice().sort((a, b) => compareOrdinal(a, b)).join("|")}::${content}`;
    const last = cacheManager2.readCache("turn-end-findings-last", cwd);
    if (last?.data?.signature === signature && last?.data?.sessionId === runtime.telemetrySessionId && // #3813: a retirement is an event, not a persisting finding. The
    // coordinator already consumed these entries, so an identical
    // second retirement on consecutive turns (same file, count and
    // ordinal) would be consumed and never shown (#3776 verify r3 F6).
    resolvedBlockerFileList.length === 0) {
      dbg("turn_end: duplicate findings detected (same session), suppressing re-prompt");
      if (getFlag("lens-guard")) {
        const existingGuard = cacheManager2.readCache("turn-end-findings", cwd)?.data;
        if (existingGuard) {
          writeGitGuardRecord(cacheManager2, runtime, cwd, {
            ...existingGuard,
            content,
            blockerContent: blockerParts.length > 0 ? capTurnEndMessage(blockerParts.join("\n\n")) : void 0,
            hasBlockers: blockerParts.length > 0 || existingGuard.testFailures === true,
            blockingFiles: blockerParts.length > 0 ? existingGuard.affectedFiles : void 0,
            projectSeqStart: runtime.turnStartProjectSeq,
            projectSeqEnd: runtime.projectSeq,
            fileSeqByPath: Object.fromEntries(runtime.getFileSeqEntries().map(([filePath, seq]) => [
              normalizeMapKey(path23.resolve(filePath)),
              seq
            ])),
            fileContentHashes: {},
            consumed: false
          });
        }
      }
      settleHolds(true);
      clearOwnedTurnState();
      runtime.fixedThisTurn.clear();
      resetFormatService2();
      return;
    }
    settleHolds(false);
    const fileSeqByPath2 = {};
    for (const [filePath, seq] of runtime.getFileSeqEntries()) {
      fileSeqByPath2[normalizeMapKey(path23.resolve(filePath))] = seq;
    }
    if (getFlag("lens-guard")) {
      const existingGuard = cacheManager2.readCache("turn-end-findings", cwd)?.data;
      const blockingContent = blockerParts.length > 0 ? capTurnEndMessage(blockerParts.join("\n\n")) : void 0;
      const affectedFiles = [
        ...existingGuard?.affectedFiles ?? [],
        ...files.map((file) => resolveRunnerPath(cwd, file)),
        ...cascadeResults.flatMap((result) => result.neighbors.filter((neighbor) => neighbor.diagnostics.length > 0).map((neighbor) => resolveRunnerPath(cwd, neighbor.filePath)))
      ];
      writeGitGuardRecord(cacheManager2, runtime, cwd, {
        content: [content, existingGuard?.testFailureContent].filter((value) => !!value).join("\n\n"),
        blockerContent: blockingContent,
        blockingFiles: blockerParts.length > 0 ? affectedFiles : void 0,
        hasBlockers: !!blockingContent || existingGuard?.testFailures === true,
        affectedFiles,
        sessionId: runtime.telemetrySessionId,
        projectSeqStart: runtime.turnStartProjectSeq,
        projectSeqEnd: runtime.projectSeq,
        fileSeqByPath: fileSeqByPath2,
        fileContentHashes: {},
        consumed: false,
        testFailures: existingGuard?.testFailures,
        testFailureContent: existingGuard?.testFailureContent,
        testFailureFiles: existingGuard?.testFailureFiles
      });
    } else {
      const allAffectedFiles = [
        ...files.map((file) => resolveRunnerPath(cwd, file)),
        ...cascadeResults.flatMap((result) => result.neighbors.filter((neighbor) => neighbor.diagnostics.length > 0).map((neighbor) => resolveRunnerPath(cwd, neighbor.filePath)))
      ];
      const affectedFiles = [...new Set(allAffectedFiles)].slice(0, MAX_ADVISORY_AFFECTED_FILES);
      const affectedFilesTruncated = new Set(allAffectedFiles).size > affectedFiles.length;
      cacheManager2.writeCache("turn-end-findings", {
        content,
        affectedFiles,
        affectedFilesTruncated,
        provenance: snapshotAdvisoryProvenance({
          cwd,
          runtime,
          generation: 0,
          files: affectedFiles.map((file) => ({
            path: file,
            role: "affected"
          })),
          truncated: affectedFilesTruncated
        })
      }, cwd);
    }
    cacheManager2.writeCache("turn-end-findings-last", {
      signature,
      sessionId: runtime.telemetrySessionId,
      projectSeqStart: runtime.turnStartProjectSeq,
      projectSeqEnd: runtime.projectSeq
    }, cwd);
    emitLensTurnFindings({
      cwd,
      filePaths: files.map((file) => resolveRunnerPath(cwd, file)),
      sessionId: runtime.telemetrySessionId,
      turnIndex: runtime.turnIndex,
      blockerSections: blockerParts.length,
      advisorySections: advisoryParts.length,
      content
    });
  }
  if (blockerParts.length === 0) {
    clearOwnedTurnState();
    if (getFlag("lens-guard") && advisoryParts.length === 0 && staleSecretParts.length === 0 && // #3218 criterion 2: a resolved line is content worth delivering,
    // so it must not be erased by the no-blockers clean-up. The
    // reader clears it after delivery because `hasBlockers` is false.
    resolvedParts.length === 0 && !runtime.gitGuardHasBlockers) {
      const guardRecord = cacheManager2.readCache("turn-end-findings", cwd)?.data;
      if (guardRecord?.sessionId === runtime.telemetrySessionId && guardRecord.testFailures !== true) {
        cacheManager2.clearCache("turn-end-findings", cwd);
      }
    }
  }
  runtime.fixedThisTurn.clear();
  runtime.clearActionableWarnings(sessionId);
  runtime.clearCodeQualityWarnings(sessionId);
  if (demotedFindingsRetired > 0) {
    cacheManager2.clearCache("turn-end-findings-last", cwd);
  }
  logLatency({
    type: "tool_result",
    toolName: "turn_end",
    filePath: cwd,
    durationMs: Date.now() - turnEndStart,
    // #1622 review M2: a pending stale secret is NOT a clean turn. It gets its
    // own result rather than being promoted to `blockers_found`, which would
    // undo the demotion the freshness gate just made.
    result: blockerParts.length > 0 ? "blockers_found" : staleSecretParts.length > 0 ? "stale_secrets_pending" : "clean",
    metadata: {
      fileCount: files.length,
      blockerSections: blockerParts.length,
      staleSecretSections: staleSecretParts.length,
      advisorySections: advisoryParts.length,
      // #3218 criterion 2: how many files this turn LISTED as resolved
      // (held-back entries are not counted), and how many retire events
      // fell past the cap. The line itself is in the delivered content;
      // these make the feature visible in latency.log even when the block
      // is suppressed or the agent reads only telemetry.
      resolvedBlockerFiles: resolvedBlockerFileList.length,
      resolvedBlockerFilesDropped,
      // #3813: one-shot parts the cap cut that stay pending for the next turn.
      heldSections,
      // #1944 AC3: an empty advisory section on its own cannot say whether
      // the turn had nothing to report or dropped something. This counter
      // answers that from latency.log even when the payload is empty, and
      // the payload itself carries the retirement note when it is not.
      demotedFindingsRetired,
      // #2275: the widget-footer store's own dependency-drift retirements —
      // a separate surface/counter from `demotedFindingsRetired` above,
      // since a widget retirement never touches `advisoryParts` or the
      // `turn-end-findings-last` suppression cache the block below clears.
      widgetDemotedFindingsRetired
    }
  });
  resetFormatService2();
}

// dist/clients/mcp/session.js
var contextPromise;
var resolvedRuntime;
function getMcpSessionContext() {
  contextPromise ??= (async () => {
    const context = {
      runtime: new RuntimeCoordinator(),
      cacheManager: new CacheManager(),
      astGrepClient: new AstGrepClient(),
      clients: await loadBootstrapClients()
    };
    resolvedRuntime = context.runtime;
    return context;
  })().catch((err) => {
    contextPromise = void 0;
    throw err;
  });
  return contextPromise;
}
function peekMcpSessionRuntime() {
  return resolvedRuntime;
}
var noop = () => {
};
function joinMessages(consumed) {
  if (!consumed?.messages?.length)
    return void 0;
  return consumed.messages.map((message) => message.content).join("\n\n");
}
var degradedSessionStart = {
  aliveLspClients: 0
};
async function runSessionStartImpl(cwd) {
  startSituationalToolTelemetrySession("mcp");
  const ctx = await getMcpSessionContext();
  const host = createMcpHost(void 0, cwd);
  await bounded(handleSessionStart({
    ctxCwd: cwd,
    // The MCP server has no TUI keystroke latency to protect, so the
    // first-call-quick heuristic must not apply. Force "full" mode so
    // the dominant-language LSP pre-warm, scans, and error-debt baseline
    // all run on the first (and only) session_start of the process.
    // An explicit PI_LENS_STARTUP_MODE env var still wins (handled in
    // handleSessionStart — the override is only checked when the env var
    // is unset).
    startupModeOverride: "full",
    getFlag: host.getFlag,
    notify: noop,
    dbg: noop,
    log: noop,
    runtime: ctx.runtime,
    cacheManager: ctx.cacheManager,
    astGrepClient: ctx.astGrepClient,
    // The MCP server has already loaded the analyzer graph (an MCP call has
    // nothing to defer to), so it hands the handler the same one-shape seam
    // every other caller uses rather than a second, field-by-field shape.
    bootstrap: residentBootstrapAccess(ctx.clients),
    ensureTool: async (name) => (await import("../probes/installer.js")).ensureTool(name),
    cleanStaleTsBuildInfo: () => [],
    resetDispatchBaselines: (cwdArg) => resetDispatchBaselines(cwdArg ?? cwd),
    resetLSPService
  }), {
    ms: HOOK_WALL_BUDGET_MS.session_start,
    signal: void 0,
    hook: "session_start",
    label: "handleSessionStart"
  });
  const baseline = ctx.runtime.errorDebtBaseline;
  return {
    guidance: joinMessages(consumeSessionStartGuidance(ctx.cacheManager, cwd, "mcp")),
    errorDebtBaseline: baseline ? { testsPassed: baseline.testsPassed, buildPassed: baseline.buildPassed } : void 0,
    aliveLspClients: getLSPService().getAliveClientCount()
  };
}
function runSessionStart(cwd) {
  return bounded(runSessionStartImpl(cwd), {
    ms: HOOK_WALL_BUDGET_MS.session_start,
    signal: void 0,
    hook: "session_start",
    label: "mcp-session-start"
  }).then((outcome) => outcome ?? degradedSessionStart);
}
var degradedTurnEnd = {
  outcome: { filesRegistered: 0 },
  commit: () => {
  }
};
var TURN_END_DELIVERY_TTL_MS = 10 * 6e4;
var TURN_END_DELIVERY_MAX = 8;
var TURN_END_QUEUE_WAIT_MS = 5e3;
var TurnEndQueue = class {
  running = false;
  pending = [];
  enqueue(work, waitMs = TURN_END_QUEUE_WAIT_MS) {
    if (this.pending.length >= 1) {
      return Promise.reject(new Error("turn_end queue is busy"));
    }
    return new Promise((resolve30, reject) => {
      const item = {
        work,
        resolve: resolve30,
        reject,
        timer: setTimeout(() => {
          const index = this.pending.indexOf(item);
          if (index === -1)
            return;
          this.pending.splice(index, 1);
          reject(new Error("turn_end queue wait timed out"));
        }, waitMs)
      };
      item.timer.unref();
      this.pending.push(item);
      this.drain();
    });
  }
  drain() {
    if (this.running)
      return;
    const item = this.pending.shift();
    if (!item)
      return;
    clearTimeout(item.timer);
    this.running = true;
    void item.work().then(item.resolve, item.reject).finally(() => {
      this.running = false;
      this.drain();
    });
  }
  reset() {
    for (const item of this.pending.splice(0)) {
      clearTimeout(item.timer);
      item.reject(new Error("turn_end queue reset"));
    }
    this.running = false;
  }
};
var turnEndQueue = new TurnEndQueue();
var pendingTurnEndDeliveries = /* @__PURE__ */ new Map();
var inFlightIpcTurnEnds = /* @__PURE__ */ new Map();
async function runTurnEndNowImpl(cwd, files = [], deferredDelivery = false) {
  const ctx = await getMcpSessionContext();
  const host = createMcpHost(void 0, cwd);
  let registered = 0;
  for (const file of files) {
    const abs = path24.isAbsolute(file) ? file : path24.resolve(cwd, file);
    let lineCount3 = 1;
    try {
      lineCount3 = fs18.readFileSync(abs, "utf8").split("\n").length;
    } catch {
      continue;
    }
    ctx.cacheManager.addModifiedRange(abs, { start: 1, end: lineCount3 }, true, cwd, MCP_TURN_STATE_OWNER_ID, "mcp");
    registered++;
  }
  const owner = {
    kind: "mcp",
    id: MCP_TURN_STATE_OWNER_ID,
    pid: process.pid,
    lastSeen: (/* @__PURE__ */ new Date()).toISOString()
  };
  await handleTurnEnd({
    signal: void 0,
    ctxCwd: cwd,
    getFlag: host.getFlag,
    dbg: noop,
    runtime: ctx.runtime,
    cacheManager: ctx.cacheManager,
    knipClient: ctx.clients.knipClient,
    deadCodeClients: ctx.clients.deadCodeClients,
    depChecker: ctx.clients.depChecker,
    testRunnerClient: ctx.clients.testRunnerClient,
    host: "mcp",
    owner,
    resetLSPService,
    resetFormatService
  });
  const outcome = deferredDelivery ? {
    turnEnd: joinMessages(peekTurnEndFindings(ctx.cacheManager, cwd, ctx.runtime, true)),
    tests: joinMessages(peekTestFindings(ctx.cacheManager, cwd, ctx.runtime, true)),
    filesRegistered: registered
  } : {
    turnEnd: joinMessages(consumeTurnEndFindings(ctx.cacheManager, cwd, ctx.runtime)),
    tests: joinMessages(consumeTestFindings(ctx.cacheManager, cwd, ctx.runtime)),
    filesRegistered: registered
  };
  return {
    outcome,
    commit: () => {
      if (!deferredDelivery)
        return;
      acknowledgeTurnEndFindings(ctx.cacheManager, cwd);
      acknowledgeTestFindings(ctx.cacheManager, cwd);
    }
  };
}
function runTurnEndNow(cwd, files = [], deferredDelivery = false) {
  return bounded(runTurnEndNowImpl(cwd, files, deferredDelivery), {
    ms: HOOK_WALL_BUDGET_MS.turn_end,
    signal: void 0,
    hook: "turn_end",
    label: "mcp-turn-end"
  }).then((transaction) => transaction ?? degradedTurnEnd);
}
function runTurnEnd(cwd, files = []) {
  return turnEndQueue.enqueue(async () => (await runTurnEndNow(cwd, files)).outcome);
}
function hasTurnEndFindings(outcome) {
  return Boolean(outcome.turnEnd || outcome.tests);
}
function prunePendingTurnEndDeliveries(now = Date.now()) {
  for (const [deliveryId, delivery] of pendingTurnEndDeliveries) {
    if (now - delivery.createdAt >= TURN_END_DELIVERY_TTL_MS) {
      pendingTurnEndDeliveries.delete(deliveryId);
    }
  }
  while (pendingTurnEndDeliveries.size > TURN_END_DELIVERY_MAX) {
    const oldest = pendingTurnEndDeliveries.keys().next();
    if (oldest.done)
      break;
    pendingTurnEndDeliveries.delete(oldest.value);
  }
}
function pendingForCwd(cwd) {
  for (const [deliveryId, delivery] of pendingTurnEndDeliveries) {
    if (delivery.cwd === cwd)
      return [deliveryId, delivery];
  }
  return void 0;
}
function runTurnEndForIpc(cwd) {
  const inFlight = inFlightIpcTurnEnds.get(cwd);
  if (inFlight)
    return inFlight;
  const run = runTurnEndForIpcNow(cwd).finally(() => {
    if (inFlightIpcTurnEnds.get(cwd) === run)
      inFlightIpcTurnEnds.delete(cwd);
  });
  inFlightIpcTurnEnds.set(cwd, run);
  return run;
}
function runTurnEndForIpcNow(cwd) {
  return turnEndQueue.enqueue(async () => {
    prunePendingTurnEndDeliveries();
    const pending2 = pendingForCwd(cwd);
    if (pending2) {
      return {
        outcome: pending2[1].transaction.outcome,
        deliveryId: pending2[0]
      };
    }
    const ctx = await getMcpSessionContext();
    const cachedOutcome = {
      turnEnd: joinMessages(peekTurnEndFindings(ctx.cacheManager, cwd, ctx.runtime, true)),
      tests: joinMessages(peekTestFindings(ctx.cacheManager, cwd, ctx.runtime, true)),
      filesRegistered: 0
    };
    const transaction = hasTurnEndFindings(cachedOutcome) ? {
      outcome: cachedOutcome,
      commit: () => {
        acknowledgeTurnEndFindings(ctx.cacheManager, cwd);
        acknowledgeTestFindings(ctx.cacheManager, cwd);
      }
    } : await runTurnEndNow(cwd, [], true);
    if (!hasTurnEndFindings(transaction.outcome)) {
      return { outcome: transaction.outcome };
    }
    const deliveryId = crypto.randomUUID();
    pendingTurnEndDeliveries.set(deliveryId, {
      cwd,
      transaction,
      createdAt: Date.now()
    });
    prunePendingTurnEndDeliveries();
    return { outcome: transaction.outcome, deliveryId };
  });
}
function acknowledgeTurnEnd(cwd, deliveryId) {
  prunePendingTurnEndDeliveries();
  const delivery = pendingTurnEndDeliveries.get(deliveryId);
  if (!delivery || delivery.cwd !== cwd)
    return false;
  delivery.transaction.commit();
  pendingTurnEndDeliveries.delete(deliveryId);
  return true;
}

// dist/clients/module-report.js
import * as fs19 from "node:fs";
import * as path25 from "node:path";

// dist/clients/middle-man-analysis.js
var SELF_TOKEN = {
  typescript: { token: "this", sep: "." },
  tsx: { token: "this", sep: "." },
  javascript: { token: "this", sep: "." },
  java: { token: "this", sep: "." },
  kotlin: { token: "this", sep: "." },
  csharp: { token: "this", sep: "." },
  swift: { token: "this", sep: "." },
  dart: { token: "this", sep: "." },
  rust: { token: "self", sep: "." },
  python: { token: "self", sep: "." },
  ruby: { token: "self", sep: "." },
  php: { token: "$this", sep: "->" }
};
var INTENTIONAL_FORWARDER_NAME = /adapter|facade|proxy|wrapper|decorator/i;
var CONSTRUCTOR_NAMES = /* @__PURE__ */ new Set([
  "constructor",
  "__init__",
  "__new__",
  "initialize"
]);
var MIN_CANDIDATE_METHODS = 2;
var DELEGATION_RATIO_THRESHOLD = 0.9;
function buildForwardRegex(token, sep2) {
  const t = escapeRegExp(token);
  const s = escapeRegExp(sep2);
  return new RegExp(`^(?:return\\s+)?(?:await\\s+)?${t}${s}(\\w+)${s}(\\w+)\\(([^()]*)\\)\\s*;?$`);
}
function splitArgs(s) {
  const trimmed = s.trim();
  if (!trimmed)
    return [];
  return trimmed.split(",").map((a) => a.trim());
}
function paramNamesFromSignature(signature, languageId) {
  const inner = (signature ?? "").trim().replace(/^\(/, "").replace(/\)$/, "");
  if (inner.trim() === "")
    return [];
  if (inner.includes("{") || inner.includes("["))
    return void 0;
  const names = splitArgs(inner).filter((p) => p.length > 0).map((raw) => {
    let name = raw;
    const isRest = name.startsWith("...");
    name = name.replace(/^\.\.\./, "");
    name = name.split("=")[0].trim();
    name = name.split(":")[0].trim();
    name = name.replace(/\?$/, "");
    return isRest ? `...${name}` : name;
  });
  if (languageId === "python" && names[0] === "self")
    return names.slice(1);
  if (languageId === "python" && names[0] === "cls")
    return names.slice(1);
  return names;
}
function bodyStatementLines(lines, entry, isPython) {
  const raw = lines.slice(entry.startLine - 1, entry.endLine).join("\n");
  let body;
  if (isPython) {
    const nlIdx = raw.indexOf("\n");
    if (nlIdx === -1)
      return [];
    body = raw.slice(nlIdx + 1);
  } else {
    const braceIdx = raw.indexOf("{");
    const lastBrace = raw.lastIndexOf("}");
    if (braceIdx === -1 || lastBrace === -1 || lastBrace <= braceIdx)
      return [];
    body = raw.slice(braceIdx + 1, lastBrace);
  }
  return body.replace(/\/\*[\s\S]*?\*\//g, "").split("\n").map((l) => l.replace(/\/\/.*$/, "")).map((l) => isPython ? l.replace(/#.*$/, "") : l).map((l) => l.trim()).filter((l) => l.length > 0).filter((l) => !/^("""|''')/.test(l));
}
function isAccessorLine(rawLine) {
  return /^\s*(?:public\s+|private\s+|protected\s+|internal\s+|static\s+|override\s+|readonly\s+)*(?:get|set)\s+\w+\s*[/(]/.test(rawLine);
}
function analyzeMiddleMan(classEntry, lines, languageId) {
  if (!languageId)
    return void 0;
  const selfSpec = SELF_TOKEN[languageId];
  if (!selfSpec)
    return void 0;
  if (!classEntry.members || classEntry.members.length === 0)
    return void 0;
  const isPython = languageId === "python";
  const forwardRe = buildForwardRegex(selfSpec.token, selfSpec.sep);
  const className = classEntry.name;
  const candidates = classEntry.members.filter((m) => {
    if (m.kind !== "method")
      return false;
    if (CONSTRUCTOR_NAMES.has(m.name))
      return false;
    if (m.name === className || m.name === `~${className}`)
      return false;
    const rawLine = lines[m.startLine - 1] ?? "";
    if (isAccessorLine(rawLine))
      return false;
    return true;
  });
  if (candidates.length < MIN_CANDIDATE_METHODS)
    return void 0;
  let forwardingCount = 0;
  const fieldCounts = /* @__PURE__ */ new Map();
  for (const member of candidates) {
    const bodyLines = bodyStatementLines(lines, member, isPython);
    if (bodyLines.length !== 1)
      continue;
    const match = forwardRe.exec(bodyLines[0]);
    if (!match)
      continue;
    const [, field, calledMethod, argsText] = match;
    const paramNames = paramNamesFromSignature(member.signature, languageId);
    if (paramNames === void 0)
      continue;
    const args = splitArgs(argsText);
    if (args.length !== paramNames.length)
      continue;
    if (!args.every((a, i) => a === paramNames[i]))
      continue;
    void calledMethod;
    forwardingCount += 1;
    fieldCounts.set(field, (fieldCounts.get(field) ?? 0) + 1);
  }
  if (forwardingCount === 0)
    return void 0;
  if (fieldCounts.size !== 1)
    return void 0;
  const [delegateField] = [...fieldCounts.keys()];
  return {
    delegationRatio: forwardingCount / candidates.length,
    candidateCount: candidates.length,
    forwardingCount,
    delegateField
  };
}
function implementsInterface(lines, classEntry) {
  const declText = lines.slice(classEntry.startLine - 1, Math.min(classEntry.startLine + 1, lines.length)).join(" ");
  return /\bimplements\b/.test(declText);
}
function annotateMiddleMan(entries, content, languageId) {
  if (!languageId || !SELF_TOKEN[languageId])
    return;
  const lines = content.split(/\r?\n/);
  for (const entry of entries) {
    if (entry.kind !== "class")
      continue;
    if (INTENTIONAL_FORWARDER_NAME.test(entry.name))
      continue;
    if (implementsInterface(lines, entry))
      continue;
    const signal = analyzeMiddleMan(entry, lines, languageId);
    if (!signal)
      continue;
    if (signal.delegationRatio < DELEGATION_RATIO_THRESHOLD)
      continue;
    entry.delegationRatio = signal.delegationRatio;
    entry.flags = entry.flags ? [...entry.flags, "middle man"] : ["middle man"];
  }
}

// dist/clients/module-report.js
var MAX_MODULE_REPORT_REFS = 100;
function normalizeMaxRefsPerSymbol(value) {
  if (value === void 0 || !Number.isFinite(value))
    return 10;
  return Math.min(MAX_MODULE_REPORT_REFS, Math.max(1, Math.floor(value)));
}
function tsLangForFile(filePath, kind) {
  if (kind === "jsts") {
    return resolveTreeSitterLanguage(filePath) ?? "typescript";
  }
  return symbolExtractionGrammar(kind, filePath);
}
var extractorCache = /* @__PURE__ */ new Map();
async function getExtractor(languageId) {
  let cached = extractorCache.get(languageId);
  if (!cached) {
    cached = (async () => {
      const client = getSharedTreeSitterClient();
      if (!client)
        return null;
      const extractor = new TreeSitterSymbolExtractor(languageId, client);
      const ok = await extractor.init();
      return ok ? extractor : null;
    })().catch((err) => {
      extractorCache.delete(languageId);
      throw err;
    });
    extractorCache.set(languageId, cached);
  }
  return cached;
}
function diagnosticMessage(err) {
  return err instanceof Error ? err.message : String(err);
}
async function extractFile(absPath, languageId, content) {
  try {
    const tsClient = getSharedTreeSitterClient();
    if (!tsClient) {
      return {
        symbols: [],
        imports: [],
        callbacks: [],
        error: "tree-sitter runtime unavailable (wasm aborted)"
      };
    }
    const initialized = await tsClient.init();
    if (!initialized) {
      return {
        symbols: [],
        imports: [],
        callbacks: [],
        error: "tree-sitter runtime failed to initialize"
      };
    }
    const extractor = await getExtractor(languageId);
    const extracted = await tsClient.withParsedTree(absPath, languageId, content, (tree) => {
      const warnings = extractor ? [] : [`Symbol extractor not available for ${languageId}`];
      const result = extractor ? extractor.extract(tree, absPath, content) : { symbols: [], imports: [] };
      const owners = result.symbols.filter((candidate) => !candidate.local).map((candidate) => ({
        name: candidate.name,
        startLine: candidate.line,
        endLine: candidate.endLine ?? candidate.line
      }));
      let callbacks = [];
      let callbackError;
      try {
        callbacks = extractCallbacks(tree.rootNode, owners, languageId, warnings);
      } catch (thrown) {
        if (classifyTreeSitterWasmError(thrown)) {
          tsClient.reportWasmAbort(thrown);
          callbackError = `tree-sitter wasm runtime failure (${diagnosticMessage(thrown)})`;
        } else {
          callbackError = diagnosticMessage(thrown);
        }
      }
      return {
        symbols: result.symbols,
        imports: result.imports,
        callbacks,
        callbackError,
        warnings
      };
    }, "module-report");
    if (!extracted.parsed) {
      return {
        symbols: [],
        imports: [],
        callbacks: [],
        error: `tree-sitter failed to parse as ${languageId}`
      };
    }
    return extracted.value;
  } catch (err) {
    const message = diagnosticMessage(err);
    logLatency({
      type: "phase",
      phase: "module_report_extract_error",
      filePath: absPath,
      durationMs: 0,
      metadata: { error: message }
    });
    return { symbols: [], imports: [], callbacks: [], error: message };
  }
}
function readArgsFor(filePath, startLine, endLine) {
  const offset = Math.max(1, startLine);
  const limit = Math.max(1, endLine - startLine + 1);
  return { path: filePath, offset, limit };
}
function resolveUsedBy(graph, symbolNodeId, cap, projectRoot) {
  const out = [];
  const seen = /* @__PURE__ */ new Set();
  for (const edge of graph.edgesByTo.get(symbolNodeId) ?? []) {
    if (edge.kind !== "calls" && edge.kind !== "references")
      continue;
    const from = graph.nodes.get(edge.from);
    const rawFile = from?.filePath ?? (edge.from.startsWith("file:") ? edge.from.slice("file:".length) : "");
    if (!rawFile)
      continue;
    const file = toDisplayPath(rawFile, projectRoot);
    const symbol = from?.qualifiedName ?? from?.symbolName ?? "";
    const line = (typeof from?.metadata?.line === "number" ? from.metadata.line : void 0) ?? (typeof edge.metadata?.line === "number" ? edge.metadata.line : 0);
    const key = `${file} ${symbol} ${edge.kind}`;
    if (seen.has(key))
      continue;
    seen.add(key);
    out.push({
      file,
      symbol,
      line,
      relation: edge.kind,
      ...edge.resolution && edge.resolution !== "unresolved" ? { resolution: edge.resolution } : {}
    });
    if (out.length >= cap)
      break;
  }
  return out;
}
function toDisplayPath(p, projectRoot) {
  return toProjectRelativePath(p, projectRoot);
}
function collectImports(graph, normalizedPath, projectRoot) {
  const fileNodeId = graph.fileNodes.get(normalizedPath);
  const external = /* @__PURE__ */ new Set();
  const internal = /* @__PURE__ */ new Set();
  if (fileNodeId) {
    for (const edge of graph.edgesByFrom.get(fileNodeId) ?? []) {
      if (edge.kind !== "imports")
        continue;
      const target = graph.nodes.get(edge.to);
      if (!target)
        continue;
      if (target.kind === "external") {
        external.add(String(target.metadata?.source ?? edge.to));
      } else if (target.filePath) {
        internal.add(toDisplayPath(target.filePath, projectRoot));
      } else {
        internal.add(String(target.metadata?.source ?? edge.to));
      }
    }
  }
  return {
    external: [...external].sort((a, b) => a.localeCompare(b)),
    internal: [...internal].sort((a, b) => a.localeCompare(b))
  };
}
function looksInternal(source, languageId) {
  if (languageId === "c" || languageId === "cpp") {
    return !source.startsWith("<");
  }
  return source.startsWith(".") || source.startsWith("crate::") || source.startsWith("super::") || source.startsWith("self::");
}
function coldImports(imports, languageId, absPath, projectRoot) {
  const external = /* @__PURE__ */ new Set();
  const internal = /* @__PURE__ */ new Set();
  const warnings = [];
  for (const imp of imports) {
    let files = [];
    try {
      files = resolveImportToFiles(projectRoot, absPath, languageId, imp.source);
    } catch (err) {
      const message = diagnosticMessage(err);
      warnings.push(`Failed to resolve import "${imp.source}": ${message}`);
      logLatency({
        type: "phase",
        phase: "module_report_import_resolve_error",
        filePath: absPath,
        durationMs: 0,
        metadata: {
          import: imp.source,
          error: message
        }
      });
    }
    if (files.length > 0) {
      for (const f of files)
        internal.add(toDisplayPath(f, projectRoot));
    } else if (looksInternal(imp.source, languageId)) {
      internal.add(imp.source);
    } else {
      external.add(imp.source);
    }
  }
  return {
    external: [...external].sort((a, b) => a.localeCompare(b)),
    internal: [...internal].sort((a, b) => a.localeCompare(b)),
    warnings
  };
}
function toEntry(sym, normalizedPath, graph, maxRefs, projectRoot, fileKind) {
  const startLine = sym.line;
  const endLine = sym.endLine ?? sym.line;
  const idKind = fileKind === "jsts" ? "function" : sym.kind;
  const symbolNodeId = buildSymbolId(normalizedPath, sym.name, idKind, sym.line);
  const node = graph?.nodes.get(symbolNodeId);
  const metadata = node?.metadata ?? {};
  const complexity = typeof metadata.cyclomaticComplexity === "number" ? metadata.cyclomaticComplexity : void 0;
  const fanout = graph ? (graph.edgesByFrom.get(symbolNodeId) ?? []).filter((edge) => edge.kind === "calls").length : void 0;
  const nonPublic = sym.visibility === "private" || sym.visibility === "protected";
  const exported = (sym.isExported || !!node?.exported) && !nonPublic;
  const flags = [];
  if (sym.isAsync)
    flags.push("async");
  if (fanout !== void 0 && fanout >= 4)
    flags.push("high fanout");
  if (complexity !== void 0 && complexity >= 8)
    flags.push("high complexity");
  if (metadata.isBoundaryWrapper)
    flags.push("boundary wrapper");
  const usedBy = graph ? resolveUsedBy(graph, symbolNodeId, maxRefs, projectRoot) : void 0;
  return {
    name: sym.name,
    kind: sym.kind,
    startLine,
    endLine,
    exported,
    ...sym.visibility ? { visibility: sym.visibility } : {},
    ...sym.decorators?.length ? { decorators: sym.decorators } : {},
    signature: sym.signature,
    doc: sym.doc,
    fanout: fanout && fanout > 0 ? fanout : void 0,
    complexity,
    // Empty flags array would waste ~3-5 tokens per entry on a 41-symbol
    // outline (~200 tok total). Omit when there's nothing to report.
    ...flags.length > 0 ? { flags } : {},
    usedBy: usedBy && usedBy.length > 0 ? usedBy : void 0
  };
}
function nestEntries(entries) {
  const span = (e) => e.endLine - e.startLine;
  const containerOf = /* @__PURE__ */ new Map();
  for (const e of entries) {
    let best;
    for (const c of entries) {
      if (c === e)
        continue;
      const contains = c.startLine <= e.startLine && c.endLine >= e.endLine && span(c) > span(e);
      if (!contains)
        continue;
      if (!best || span(c) < span(best))
        best = c;
    }
    containerOf.set(e, best);
  }
  for (const e of entries) {
    const parent = containerOf.get(e);
    if (!parent)
      continue;
    if (!parent.members)
      parent.members = [];
    parent.members.push(e);
  }
  for (const e of entries) {
    if (e.members)
      e.members.sort((a, b) => a.startLine - b.startLine);
  }
  return entries.filter((e) => !containerOf.get(e));
}
function summarizeEntries(entries) {
  return entries.map((entry) => ({
    name: entry.name,
    kind: entry.kind,
    startLine: entry.startLine,
    endLine: entry.endLine,
    exported: entry.exported,
    ...entry.visibility ? { visibility: entry.visibility } : {},
    ...entry.signature ? { signature: entry.signature } : {},
    ...entry.doc ? { doc: entry.doc } : {},
    ...entry.flags ? { flags: entry.flags } : {},
    ...entry.members ? {
      members: entry.members.map((member) => ({
        name: member.name,
        kind: member.kind,
        startLine: member.startLine,
        endLine: member.endLine,
        exported: member.exported,
        ...member.visibility ? { visibility: member.visibility } : {},
        ...member.signature ? { signature: member.signature } : {},
        ...member.doc ? { doc: member.doc } : {}
      }))
    } : {}
  }));
}
function normalizeFocus(focus) {
  return (focus ?? "").toLowerCase().split(/[^a-z0-9_.]+/).map((part) => part.trim()).filter((part) => part.length >= 3).slice(0, 8);
}
function focusScore(text, terms) {
  if (terms.length === 0)
    return 0;
  const haystack = text.toLowerCase();
  return terms.reduce((score, term) => score + (haystack.includes(term) ? 6 : 0), 0);
}
function rankRecommendedReads(entries, callbacks = [], limit = 5, focus) {
  const focusTerms = normalizeFocus(focus);
  const scoredSymbols = entries.map((entry) => {
    const refs = entry.usedBy?.length ?? 0;
    const focus2 = focusScore([
      entry.name,
      entry.kind,
      entry.signature ?? "",
      entry.flags?.join(" ") ?? ""
    ].join(" "), focusTerms);
    const score = refs * 2 + (entry.complexity ?? 0) + (entry.exported ? 2 : 0) + (entry.flags?.includes("high complexity") ? 3 : 0) + focus2;
    return { kind: "symbol", entry, score, refs, focus: focus2 };
  });
  const scoredCallbacks = callbacks.map((callback) => {
    const flags = callback.flags ?? [];
    const focus2 = focusScore([
      callback.name,
      callback.kind,
      callback.signature ?? "",
      flags.join(" ")
    ].join(" "), focusTerms);
    const score = (flags.includes("captures ctx.ui") ? 8 : 0) + (flags.includes("captures ctx") ? 5 : 0) + (flags.includes("detached timer") ? 4 : 0) + (flags.includes("lifecycle") ? 3 : 0) + (callback.kind === "object_property_callback" ? 2 : 0) + (callback.kind === "assigned_callback" ? 1 : 0) + focus2;
    return { kind: "callback", callback, score, focus: focus2 };
  });
  const scored = [...scoredSymbols, ...scoredCallbacks].filter((item) => item.score > 0);
  scored.sort((a, b) => b.score - a.score);
  return scored.slice(0, limit).map((item) => {
    if (item.kind === "symbol") {
      const { entry, refs } = item;
      const reasons2 = [];
      if (item.focus > 0)
        reasons2.push("matches focus");
      if (entry.exported)
        reasons2.push("exported");
      if (refs > 0)
        reasons2.push(`used by ${refs}`);
      if (entry.complexity !== void 0 && entry.complexity >= 8) {
        reasons2.push(`complexity ${entry.complexity}`);
      }
      return {
        reason: reasons2.join(", ") || "public surface",
        symbol: entry.name,
        startLine: entry.startLine,
        endLine: entry.endLine
      };
    }
    const reasons = [];
    if (item.focus > 0)
      reasons.push("matches focus");
    if (item.callback.flags?.length)
      reasons.push(...item.callback.flags);
    if (item.callback.kind === "object_property_callback") {
      reasons.push("callback property");
    }
    if (item.callback.kind === "assigned_callback") {
      reasons.push("assigned callback");
    }
    return {
      reason: reasons.join(", ") || item.callback.kind,
      symbol: item.callback.name,
      startLine: item.callback.startLine,
      endLine: item.callback.endLine
    };
  });
}
var BLAST_RADIUS_FILE_CAP = 12;
var BLAST_RADIUS_DEFAULT_READ_LIMIT = 400;
function blastReadArgs(graph, normalizedFile) {
  const fileNodeId = graph.fileNodes.get(normalizedFile);
  const lineCount3 = fileNodeId ? graph.nodes.get(fileNodeId)?.metadata?.lineCount : void 0;
  const limit = typeof lineCount3 === "number" && lineCount3 > 0 ? lineCount3 : BLAST_RADIUS_DEFAULT_READ_LIMIT;
  return { path: normalizedFile, offset: 1, limit };
}
async function computeBlastRadius(graph, normalizedPath, projectRoot, maxDepth) {
  const { computeTransitiveImpact } = await import("../chunk-34U7PLGE.js");
  const result = computeTransitiveImpact(graph, normalizedPath, { maxDepth });
  const byFile = /* @__PURE__ */ new Map();
  for (const hit of result.hits) {
    if (!hit.file)
      continue;
    const key = normalizeMapKey(hit.file);
    if (key === normalizedPath)
      continue;
    const cur = byFile.get(key) ?? {
      dependents: 0,
      minDepth: Number.POSITIVE_INFINITY,
      relations: /* @__PURE__ */ new Set()
    };
    cur.dependents += 1;
    cur.minDepth = Math.min(cur.minDepth, hit.depth);
    cur.relations.add(hit.relation);
    byFile.set(key, cur);
  }
  if (byFile.size === 0)
    return void 0;
  const files = [...byFile.entries()].map(([key, v]) => ({
    file: toDisplayPath(key, projectRoot),
    dependents: v.dependents,
    minDepth: v.minDepth,
    relations: [...v.relations].sort((a, b) => a.localeCompare(b)),
    read: blastReadArgs(graph, key)
  })).sort((a, b) => a.minDepth - b.minDepth || b.dependents - a.dependents || a.file.localeCompare(b.file)).slice(0, BLAST_RADIUS_FILE_CAP);
  return {
    truncated: result.truncated,
    maxDepth: result.maxDepthReached,
    files
  };
}
function nodeLine(node) {
  return node.startPosition.row + 1;
}
function nodeEndLine(node) {
  return node.endPosition.row + 1;
}
function firstLine(text) {
  return text.split(/\r?\n/, 1)[0]?.trim() ?? "";
}
function findNearestSymbolName(entries, startLine, endLine) {
  let best;
  for (const entry of entries) {
    if (entry.startLine > startLine || entry.endLine < endLine)
      continue;
    if (!best || entry.endLine - entry.startLine < best.endLine - best.startLine) {
      best = entry;
    }
  }
  return best?.name;
}
var INLINE_EXECUTABLE_NODE_KINDS = /* @__PURE__ */ new Set([
  // JavaScript / TypeScript
  "arrow_function",
  "function_expression",
  // Python
  "lambda",
  // Go
  "func_literal",
  // Rust
  "closure_expression",
  // Swift / Kotlin (trailing/lambda closures)
  "lambda_literal",
  // Other grammars use one of these for lambdas/anonymous functions.
  "lambda_expression",
  "anonymous_function"
]);
var ARGUMENT_CONTAINER_NODE_KINDS = /* @__PURE__ */ new Set([
  "arguments",
  "argument_list",
  "argument_list_expression"
]);
var CALL_NODE_KINDS = /* @__PURE__ */ new Set(["call_expression", "call"]);
function callNameForCallback(node) {
  const parent = node.parent;
  const call = ARGUMENT_CONTAINER_NODE_KINDS.has(parent?.type ?? "") ? parent?.parent : CALL_NODE_KINDS.has(parent?.type ?? "") ? parent : void 0;
  if (!call || !CALL_NODE_KINDS.has(call.type))
    return void 0;
  const callee = call.children.find((child) => !ARGUMENT_CONTAINER_NODE_KINDS.has(child.type));
  return callee?.text;
}
function eventNameForCallback(node) {
  const args = node.parent;
  if (args?.type !== "arguments")
    return void 0;
  const first = args.children.find((child) => child.type === "string" && nodeLine(child) <= nodeLine(node));
  return first?.text;
}
function propertyNameForCallback(node) {
  const parent = node.parent;
  if (parent?.type !== "pair" && parent?.type !== "key_value_pair") {
    return void 0;
  }
  const key = parent.children.find((child) => child !== node);
  return key?.text;
}
function assignedNameForCallback(node) {
  const parent = node.parent;
  if (!parent)
    return void 0;
  if (parent.type === "let_declaration" || parent.type === "variable_declarator") {
    return parent.children.find((child) => child.type === "identifier")?.text;
  }
  if (parent.type === "expression_list") {
    const declaration = parent.parent;
    if (declaration?.type !== "short_var_declaration")
      return void 0;
    const nameList = declaration.children.find((child) => child !== parent);
    return nameList?.children.find((child) => child.type === "identifier")?.text;
  }
  if (parent.type === "assignment") {
    return parent.children.find((child) => child.type === "identifier")?.text;
  }
  return void 0;
}
function ancestorOfType(node, types, maxHops = 6) {
  let current = node.parent;
  let hops = 0;
  while (current && hops < maxHops) {
    if (types.has(current.type))
      return current;
    current = current.parent;
    hops += 1;
  }
  return void 0;
}
function descendantOfType(node, type, maxDepth = 2) {
  if (maxDepth < 0)
    return void 0;
  for (const child of node.children ?? []) {
    if (child.type === type)
      return child;
    const found = descendantOfType(child, type, maxDepth - 1);
    if (found)
      return found;
  }
  return void 0;
}
function classifyGenericCallback(ctx) {
  const { node, callName, propertyName, assignedName } = ctx;
  let kind;
  if (callName === "setTimeout" || callName === "setInterval" || callName === "setImmediate") {
    kind = "timer_callback";
  } else if (callName === "pi.on" || callName?.endsWith(".on")) {
    kind = "event_handler";
  } else if (callName?.endsWith(".then") || callName?.endsWith(".catch") || callName?.endsWith(".finally")) {
    kind = "promise_callback";
  } else if (propertyName) {
    kind = "object_property_callback";
  } else if (assignedName) {
    kind = "assigned_callback";
  } else {
    kind = "callback";
  }
  const flags = [];
  if (node.text.trimStart().startsWith("async"))
    flags.push("async");
  if (/\bctx\s*\.\s*ui\b/.test(node.text))
    flags.push("captures ctx.ui");
  else if (/\bctx\b/.test(node.text))
    flags.push("captures ctx");
  if (kind === "timer_callback")
    flags.push("detached timer");
  if (kind === "event_handler")
    flags.push("lifecycle");
  const include = kind !== "callback" || !!propertyName || !!assignedName || flags.some((flag) => flag.startsWith("captures "));
  return { kind, ...flags.length > 0 ? { flags } : {}, include };
}
var jstsCallbackRules = {
  classify: (ctx) => classifyGenericCallback(ctx)
};
var GO_GOROUTINE_KINDS = /* @__PURE__ */ new Set(["go_statement"]);
var GO_DEFER_KINDS = /* @__PURE__ */ new Set(["defer_statement"]);
var goCallbackRules = {
  classify(ctx, owner) {
    if (ancestorOfType(ctx.node, GO_GOROUTINE_KINDS, 3)) {
      return {
        kind: "goroutine",
        flags: ["goroutine"],
        include: true,
        nameBase: owner ? `${owner}.goroutine` : "goroutine"
      };
    }
    if (ancestorOfType(ctx.node, GO_DEFER_KINDS, 3)) {
      return {
        kind: "deferred_callback",
        flags: ["deferred"],
        include: true,
        nameBase: owner ? `${owner}.defer` : "defer"
      };
    }
    return classifyGenericCallback(ctx);
  }
};
function withFlag(flags, flag) {
  const next = flags ? [...flags] : [];
  if (!next.includes(flag))
    next.push(flag);
  return next;
}
var pythonCallbackRules = {
  classify(ctx) {
    const base = classifyGenericCallback(ctx);
    const callName = ctx.callName ?? "";
    if (/(?:^|\.)(?:call_later|call_soon|call_at)$/.test(callName) || /(?:^|\.)Timer$/.test(callName)) {
      return {
        kind: "timer_callback",
        flags: withFlag(base.flags, "detached timer"),
        include: true
      };
    }
    if (callName.endsWith(".add_done_callback")) {
      return {
        kind: "future_callback",
        flags: withFlag(base.flags, "future completion"),
        include: true
      };
    }
    return base;
  }
};
var rustCallbackRules = {
  classify(ctx) {
    const base = classifyGenericCallback(ctx);
    const isMove = /^\s*(?:async\s+)?move\b/.test(ctx.node.text);
    const flags = isMove ? withFlag(base.flags, "move") : base.flags;
    const callName = ctx.callName ?? "";
    if (/(?:^|::|\.)spawn$/.test(callName)) {
      return {
        kind: "task",
        flags: withFlag(flags, "spawned"),
        include: true
      };
    }
    return {
      ...base,
      ...flags ? { flags } : {},
      include: base.include || isMove
    };
  }
};
var swiftCallbackRules = {
  classify(ctx) {
    const base = classifyGenericCallback(ctx);
    const node = ctx.node;
    const captureList = descendantOfType(node, "capture_list", 2);
    let weakSelf = false;
    if (captureList) {
      for (const item of captureList.children ?? []) {
        if (item.type !== "capture_list_item")
          continue;
        const own = item.children?.find((c) => c.type === "ownership_modifier");
        if (own && /\bself\b/.test(item.text))
          weakSelf = true;
      }
    }
    const refsSelf = /\bself\b/.test(node.text);
    let flags = base.flags;
    let include = base.include;
    if (weakSelf) {
      flags = withFlag(flags, "weak self");
      include = true;
    } else if (refsSelf) {
      flags = withFlag(flags, "captures self");
      include = true;
    }
    return { ...base, ...flags ? { flags } : {}, include };
  }
};
var cppCallbackRules = {
  classify(ctx) {
    const base = classifyGenericCallback(ctx);
    const capture = descendantOfType(ctx.node, "lambda_capture_specifier", 2);
    let flags = base.flags;
    let byRef = false;
    if (capture) {
      const def = capture.children?.find((c) => c.type === "lambda_default_capture");
      if (def?.text.includes("&")) {
        flags = withFlag(flags, "captures by reference");
        byRef = true;
      }
    }
    const callName = ctx.callName ?? "";
    if (/(?:^|::)(?:thread|async)$/.test(callName)) {
      return {
        kind: "task",
        flags: withFlag(flags, "spawned"),
        include: true
      };
    }
    return {
      ...base,
      ...flags ? { flags } : {},
      include: base.include || byRef
    };
  }
};
function lastCalleeSegment(text) {
  const m = String(text ?? "").trim().match(/([A-Za-z_$][\w$]*)\s*$/);
  return m ? m[1] : "";
}
var KOTLIN_COROUTINE_BUILDERS = /* @__PURE__ */ new Set([
  "launch",
  "async",
  "withContext",
  "runBlocking",
  "coroutineScope",
  "supervisorScope"
]);
var kotlinCallbackRules = {
  classify(ctx) {
    const base = classifyGenericCallback(ctx);
    const call = ancestorOfType(ctx.node, /* @__PURE__ */ new Set(["call_expression"]), 4);
    const callee = call?.children?.find((c) => c.type === "navigation_expression" || c.type === "simple_identifier");
    const name = lastCalleeSegment(callee?.text);
    if (KOTLIN_COROUTINE_BUILDERS.has(name)) {
      return {
        kind: "coroutine",
        flags: withFlag(base.flags, "coroutine"),
        include: true,
        nameBase: name
      };
    }
    return base;
  }
};
var JAVA_TASK_METHODS = /^(?:submit|execute|schedule|scheduleAtFixedRate|scheduleWithFixedDelay|invokeLater|invokeAndWait)$/;
var JAVA_LISTENER_METHODS = /^(?:add\w*Listener|set\w*Listener|subscribe|addCallback|then\w*)$/;
var javaCallbackRules = {
  classify(ctx) {
    const base = classifyGenericCallback(ctx);
    const obj = ancestorOfType(ctx.node, /* @__PURE__ */ new Set(["object_creation_expression"]), 3);
    const created = obj?.children?.find((c) => c.type === "type_identifier");
    if (created && created.text.endsWith("Thread")) {
      return {
        kind: "task",
        flags: withFlag(base.flags, "thread"),
        include: true,
        nameBase: `new ${created.text}`
      };
    }
    const inv = ancestorOfType(ctx.node, /* @__PURE__ */ new Set(["method_invocation"]), 3);
    if (inv) {
      const named = (inv.children ?? []).filter((c) => c.isNamed);
      const argIdx = named.findIndex((c) => c.type === "argument_list");
      const nameNode = argIdx > 0 ? named[argIdx - 1] : void 0;
      const name = nameNode?.type === "identifier" ? nameNode.text : "";
      if (JAVA_TASK_METHODS.test(name)) {
        return {
          kind: "task",
          flags: withFlag(base.flags, "submitted"),
          include: true,
          nameBase: name
        };
      }
      if (JAVA_LISTENER_METHODS.test(name)) {
        return {
          kind: "event_handler",
          flags: withFlag(base.flags, "listener"),
          include: true,
          nameBase: name
        };
      }
    }
    return base;
  }
};
var csharpCallbackRules = {
  classify(ctx) {
    const base = classifyGenericCallback(ctx);
    const parent = ctx.node.parent;
    if (parent?.type === "assignment_expression" && /\+=/.test(parent.text)) {
      return {
        kind: "event_handler",
        flags: withFlag(base.flags, "event +="),
        include: true
      };
    }
    const inv = ancestorOfType(ctx.node, /* @__PURE__ */ new Set(["invocation_expression"]), 4);
    if (inv) {
      const callee = inv.children?.find((c) => c.type === "member_access_expression" || c.type === "identifier");
      const name = lastCalleeSegment(callee?.text);
      if (/^(?:Run|StartNew|Start)$/.test(name)) {
        return {
          kind: "task",
          flags: withFlag(base.flags, "task"),
          include: true,
          nameBase: name
        };
      }
    }
    if (/^\s*async\b/.test(ctx.node.text)) {
      return {
        ...base,
        flags: withFlag(base.flags, "async"),
        include: true
      };
    }
    return base;
  }
};
var CALLBACK_RULES = {
  typescript: jstsCallbackRules,
  tsx: jstsCallbackRules,
  javascript: jstsCallbackRules,
  go: goCallbackRules,
  python: pythonCallbackRules,
  rust: rustCallbackRules,
  swift: swiftCallbackRules,
  cpp: cppCallbackRules,
  kotlin: kotlinCallbackRules,
  java: javaCallbackRules,
  csharp: csharpCallbackRules
};
function callbackRulesFor(languageId) {
  return (languageId ? CALLBACK_RULES[languageId] : void 0) ?? jstsCallbackRules;
}
function callbackSupportFor(languageId) {
  return languageId && CALLBACK_RULES[languageId] ? "tuned" : "generic";
}
function extractCallbacks(root, entries, languageId, warnings) {
  if (!root)
    return [];
  const rules = callbackRulesFor(languageId);
  const callbacks = [];
  const maxDepth = 1e3;
  let depthTruncated = false;
  const visit = (node, depth = 0) => {
    if (depth > maxDepth) {
      depthTruncated = true;
      return;
    }
    if (INLINE_EXECUTABLE_NODE_KINDS.has(node.type)) {
      const startLine = nodeLine(node);
      const endLine = nodeEndLine(node);
      const callName = callNameForCallback(node);
      const eventName = eventNameForCallback(node);
      const propertyName = propertyNameForCallback(node);
      const assignedName = assignedNameForCallback(node);
      const owner = findNearestSymbolName(entries, startLine, endLine);
      const cls = rules.classify({ node, callName, eventName, propertyName, assignedName }, owner);
      if (cls.include) {
        const base = cls.nameBase ?? (propertyName ? owner ? `${owner}.${propertyName}` : propertyName : assignedName ? owner ? `${owner}.${assignedName}` : assignedName : eventName && callName ? `${callName}(${eventName})` : callName ?? "callback");
        callbacks.push({
          name: `${base}@${startLine}`,
          kind: cls.kind,
          rawKind: node.type,
          startLine,
          endLine,
          signature: firstLine(node.text),
          ...owner ? { parentChain: [owner] } : {},
          ...cls.flags ? { flags: cls.flags } : {}
        });
      }
    }
    for (const child of node.children ?? [])
      visit(child, depth + 1);
  };
  visit(root);
  if (depthTruncated) {
    warnings?.push(`Callback extraction stopped at AST depth ${maxDepth}`);
  }
  const callbackCap = 25;
  if (callbacks.length > callbackCap) {
    warnings?.push(`Callback list truncated to ${callbackCap} of ${callbacks.length} entries`);
  }
  return callbacks.slice(0, callbackCap);
}
var CALL_GRAPH_DEFAULT_ENTRY_CAP = 20;
var CALL_GRAPH_MAX_ENTRY_CAP = 100;
function unavailableCallGraph(reason) {
  return {
    available: false,
    reason,
    callers: [],
    callees: [],
    truncated: false,
    coverage: { status: "unavailable", complete: false }
  };
}
function callGraphCoverage(coverage) {
  const languages = coverage.languages;
  const complete = coverage.complete && Object.values(languages ?? {}).every((status) => status === "complete");
  return {
    status: complete ? "complete" : "partial",
    complete,
    totalEvidence: coverage.totalEvidence,
    callsEvidence: coverage.callsEvidence,
    referencesEvidence: coverage.referencesEvidence,
    eligibleEvidence: coverage.eligibleEvidence,
    resolvedEvidence: coverage.resolvedEvidence,
    unresolvedEvidence: coverage.unresolvedEvidence,
    typeOnlyEvidence: coverage.typeOnlyEvidence,
    unsupportedEvidence: coverage.unsupportedEvidence,
    sameFileEvidence: coverage.sameFileEvidence,
    duplicateEvidence: coverage.duplicateEvidence,
    ...languages ? { languages } : {}
  };
}
function callGraphRelation(edge, kind, projectRoot) {
  const caller = kind === "caller";
  return {
    symbolId: caller ? edge.callerKey : edge.calleeKey,
    targetSymbolId: caller ? edge.calleeKey : edge.callerKey,
    file: toDisplayPath(caller ? edge.callerFile : edge.calleeFile, projectRoot),
    ...caller ? {
      symbol: edge.callerSymbol,
      kind: edge.callerKind,
      line: edge.callerLine
    } : {
      symbol: edge.calleeSymbol,
      kind: edge.calleeKind,
      line: edge.calleeLine
    },
    ...edge.evidenceKind ? { evidenceKind: edge.evidenceKind } : {},
    ...edge.resolution ? { resolution: edge.resolution } : {},
    ...edge.evidenceCount && edge.evidenceCount > 1 ? { evidenceCount: edge.evidenceCount } : {},
    ...edge.weight !== 1 ? { weight: edge.weight } : {}
  };
}
function readCallGraph(projectRoot, normalizedPath, maxEntries, graphFileCap, identity, reviewGraph) {
  if (graphFileCap !== void 0)
    return unavailableCallGraph("file-cap");
  if (!reviewGraph)
    return unavailableCallGraph("review-graph-missing");
  if (reviewGraph.persistCoverage?.partial || reviewGraph.persistCoverage?.inProgress) {
    return unavailableCallGraph("partial");
  }
  if (!identity)
    return unavailableCallGraph("identity-missing");
  if (!reviewGraph.fileNodes.has(normalizedPath))
    return unavailableCallGraph("stale");
  const cached = loadCallGraph(projectRoot, identity);
  if (!cached)
    return unavailableCallGraph("stale");
  const callers = [];
  const callees = [];
  for (const edge of cached.graph.edges) {
    if (normalizeMapKey(edge.calleeFile) === normalizedPath) {
      callers.push(callGraphRelation(edge, "caller", projectRoot));
    }
    if (normalizeMapKey(edge.callerFile) === normalizedPath) {
      callees.push(callGraphRelation(edge, "callee", projectRoot));
    }
  }
  const stable = (left, right) => left.targetSymbolId.localeCompare(right.targetSymbolId) || (left.line ?? 0) - (right.line ?? 0) || left.symbolId.localeCompare(right.symbolId);
  callers.sort(stable);
  callees.sort(stable);
  return {
    available: true,
    callers: callers.slice(0, maxEntries),
    callees: callees.slice(0, maxEntries),
    truncated: callers.length > maxEntries || callees.length > maxEntries,
    coverage: callGraphCoverage(cached.graph.coverage ?? {
      totalEvidence: cached.graph.totalRefs,
      callsEvidence: cached.graph.totalRefs,
      referencesEvidence: 0,
      eligibleEvidence: cached.graph.totalRefs,
      resolvedEvidence: cached.graph.totalRefs,
      unresolvedEvidence: cached.graph.unresolvedRefs,
      typeOnlyEvidence: 0,
      unsupportedEvidence: 0,
      sameFileEvidence: 0,
      duplicateEvidence: 0,
      complete: false
    })
  };
}
function unavailableReport(displayPath, error) {
  return {
    available: false,
    staleness: "unavailable",
    path: displayPath,
    ...error ? { error } : {},
    summary: { imports: 0, exports: 0, symbols: 0 },
    imports: { external: [], internal: [] },
    api: [],
    internal: [],
    callbacks: [],
    recommendedReads: [],
    semantic: { source: "none", references: false, implementations: false }
  };
}
async function moduleReport(file, cwd, options) {
  const startedAt = Date.now();
  const maxRefs = normalizeMaxRefsPerSymbol(options?.maxRefsPerSymbol);
  const maxCallGraphEntries = Math.min(CALL_GRAPH_MAX_ENTRY_CAP, Math.max(1, Math.floor(options?.maxCallGraphEntries ?? CALL_GRAPH_DEFAULT_ENTRY_CAP)));
  const absPath = path25.resolve(cwd, file);
  const normalizedPath = normalizeMapKey(absPath);
  let content;
  try {
    content = fs19.readFileSync(absPath, "utf-8");
  } catch {
    return unavailableReport(toDisplayPath(absPath, cwd));
  }
  const kind = detectFileKind(absPath);
  const languageId = tsLangForFile(absPath, kind);
  const lineCount3 = content.split(/\r?\n/).length;
  const { symbols: extracted, imports: extractedImports, callbacks, callbackError, error: extractionError, warnings: extractionWarnings } = languageId ? await extractFile(absPath, languageId, content) : {
    symbols: [],
    imports: [],
    callbacks: [],
    callbackError: void 0,
    error: void 0,
    warnings: void 0
  };
  if (extractionError) {
    return unavailableReport(toDisplayPath(absPath, cwd), extractionError);
  }
  let graph;
  let graphFileCap;
  let callGraphIdentity;
  let revisionDriftNote;
  try {
    const { getCachedReviewGraph, getReviewGraphCacheIdentity, getReviewGraphSizeSkipVerdict, getReviewGraphRevisionDrift, formatReviewGraphRevisionDriftNote: formatReviewGraphRevisionDriftNote2 } = await import("../chunk-VD4JQCQC.js");
    graph = getCachedReviewGraph(cwd);
    graphFileCap = getReviewGraphSizeSkipVerdict(cwd)?.maxFileCount;
    const drift = graph ? getReviewGraphRevisionDrift(cwd) : void 0;
    if (drift)
      revisionDriftNote = formatReviewGraphRevisionDriftNote2(drift);
    callGraphIdentity = graph ? (() => {
      const identity = getReviewGraphCacheIdentity(cwd, graph);
      return identity ? {
        reviewGraphVersion: identity.version,
        reviewGraphSignature: identity.signature
      } : void 0;
    })() : void 0;
  } catch {
    graph = void 0;
    callGraphIdentity = void 0;
  }
  const outlineSymbols = extracted.filter((sym) => !sym.local);
  const entries = outlineSymbols.map((sym) => toEntry(sym, normalizedPath, graph, maxRefs, cwd, kind));
  const topLevel = nestEntries(entries);
  annotateMiddleMan(entries, content, languageId);
  const api = topLevel.filter((entry) => entry.exported);
  const internal = topLevel.filter((entry) => !entry.exported);
  const warnings = [...extractionWarnings ?? []];
  if (revisionDriftNote)
    warnings.push(revisionDriftNote);
  if (callbackError) {
    warnings.push(`Failed to extract callbacks: ${callbackError}`);
    logLatency({
      type: "phase",
      phase: "module_report_callback_extract_error",
      filePath: absPath,
      durationMs: 0,
      metadata: { error: callbackError }
    });
  }
  if (graphFileCap !== void 0) {
    warnings.push(`who-uses-this is unavailable: review graph disabled because the project has more than ${graphFileCap} files (cap ${graphFileCap}) \u2014 raise maxProjectFiles in .pi-lens.json or set PI_LENS_REVIEW_GRAPH_MAX_FILES; for CI/cron, run npx pi-lens build-graph after configuring the cap`);
  }
  const warmImports = graph ? collectImports(graph, normalizedPath, cwd) : { external: [], internal: [] };
  const coldImportResult = warmImports.external.length + warmImports.internal.length > 0 || !languageId ? void 0 : coldImports(extractedImports, languageId, absPath, cwd);
  if (coldImportResult?.warnings.length)
    warnings.push(...coldImportResult.warnings);
  const imports = coldImportResult ? {
    external: coldImportResult.external,
    internal: coldImportResult.internal
  } : warmImports;
  const hasGraphNode = graph?.fileNodes.has(normalizedPath) ?? false;
  if (graph && !hasGraphNode) {
    warnings.push("who-uses-this is unavailable for this file: the cached review graph exists but has no node for it (likely added/changed after the graph was last built). Run pilens_rebuild to refresh it.");
  }
  const blastRadius = options?.blastRadius && graph && hasGraphNode ? await computeBlastRadius(graph, normalizedPath, cwd, Math.max(1, options.blastRadiusDepth ?? 3)) : void 0;
  const callGraph = options?.callGraph ? readCallGraph(cwd, normalizedPath, maxCallGraphEntries, graphFileCap, callGraphIdentity, graph) : void 0;
  const view = options?.view ?? "default";
  const summaryView = view === "summary";
  const compactView = view === "compact";
  let importsProvenance = "none";
  if (coldImportResult) {
    importsProvenance = "syntax";
  } else if (graph) {
    importsProvenance = "cached-review-graph";
  }
  const unavailableGraphProvenance = graphFileCap !== void 0 ? "unavailable:file-cap" : "none";
  const blastRadiusProvenance = blastRadius ? "cached-review-graph" : unavailableGraphProvenance;
  const callGraphProvenance = callGraph?.available ? "cached-call-graph" : callGraph?.reason === "file-cap" ? "unavailable:file-cap" : "none";
  const report = {
    available: entries.length > 0 || hasGraphNode,
    staleness: entries.length === 0 && !hasGraphNode ? "unavailable" : "fresh",
    path: toDisplayPath(absPath, cwd),
    language: kind ?? void 0,
    lineCount: lineCount3,
    summary: {
      imports: imports.external.length + imports.internal.length,
      exports: api.length,
      symbols: entries.length
    },
    imports,
    ...warnings.length > 0 ? { warnings } : {},
    api: summaryView ? summarizeEntries(api) : api,
    internal: summaryView ? summarizeEntries(internal) : internal,
    callbacks: summaryView ? [] : callbacks,
    callbackSupport: callbackSupportFor(languageId),
    recommendedReads: rankRecommendedReads(entries, callbacks, 5, options?.focus),
    ...summaryView ? { view: "summary" } : {},
    ...compactView ? { view: "compact" } : {},
    ...blastRadius && !summaryView ? { blastRadius } : {},
    ...callGraph ? { callGraph } : {},
    ...graph ? { graphBuiltAt: graph.builtAt } : {},
    provenance: {
      symbols: languageId ? "syntax" : "none",
      imports: importsProvenance,
      usedBy: hasGraphNode ? "cached-review-graph" : unavailableGraphProvenance,
      callbacks: languageId && !summaryView ? "heuristic-tree-sitter" : "none",
      ...options?.blastRadius ? { blastRadius: blastRadiusProvenance } : {},
      ...options?.callGraph ? { callGraph: callGraphProvenance } : {}
    },
    semantic: {
      // Provenance of who-uses-this / references. The AST review graph is the
      // only source on this read path; "graph-lsp" is reserved for #236 (LSP
      // writes provenance edges INTO the graph). Cold cache → "none".
      source: hasGraphNode ? "review-graph" : unavailableGraphProvenance,
      references: hasGraphNode,
      implementations: false
    }
  };
  logLatency({
    type: "phase",
    phase: "module_report",
    filePath: absPath,
    durationMs: Date.now() - startedAt,
    metadata: {
      graph: graph ? "cached" : "cold",
      symbols: entries.length,
      exported: api.length
    }
  });
  return report;
}
function padRange(startLine, endLine, width) {
  return `${startLine}-${endLine}`.padEnd(width);
}
var KIND_ABBREV = {
  function: "fn",
  method: "fn",
  class: "class",
  interface: "iface",
  type: "type",
  variable: "var",
  property: "prop"
};
function compactKind(kind) {
  return KIND_ABBREV[kind] ?? kind;
}
function compactUsedBySuffix(usedBy) {
  if (!usedBy || usedBy.length === 0)
    return "";
  const counts = /* @__PURE__ */ new Map();
  for (const u of usedBy)
    counts.set(u.file, (counts.get(u.file) ?? 0) + 1);
  const parts = [...counts.entries()].map(([file, n]) => n > 1 ? `${file}\xD7${n}` : file);
  return `  used-by: ${parts.join(", ")}`;
}
function compactEntryLine(entry, width) {
  const range = padRange(entry.startLine, entry.endLine, width);
  const kind = compactKind(entry.kind).padEnd(6);
  const sig = entry.signature ? `${entry.name}${entry.signature}` : entry.name;
  const flagsSuffix = entry.flags && entry.flags.length > 0 ? `  [${entry.flags.join(", ")}]` : "";
  const docSuffix = entry.doc ? `  \u2014 ${entry.doc}` : "";
  const usedBySuffix = compactUsedBySuffix(entry.usedBy);
  return `  ${range}${kind}${sig}${flagsSuffix}${usedBySuffix}${docSuffix}`;
}
function compactMemberLines(entry, width, indent) {
  if (!entry.members || entry.members.length === 0)
    return [];
  return entry.members.map((m) => `${indent}${compactEntryLine(m, width).slice(2)}`);
}
function compactCallbackLine(callback, width) {
  const range = padRange(callback.startLine, callback.endLine, width);
  const kind = callback.kind.padEnd(20);
  const flagsSuffix = callback.flags && callback.flags.length > 0 ? `  [${callback.flags.join(", ")}]` : "";
  const ownerSuffix = callback.parentChain?.length ? `  (in ${callback.parentChain.join(".")})` : "";
  return `  ${range}${kind}${callback.name}${flagsSuffix}${ownerSuffix}`;
}
function renderCompactModuleReport(report) {
  if (!report.available) {
    return `${report.path} \u2014 unavailable${report.error ? `: ${report.error}` : ""}`;
  }
  const allEntries = [...report.api, ...report.internal];
  const allRanges = allEntries.flatMap((e) => [e, ...e.members ?? []]);
  const width = Math.max(5, ...allRanges.map((e) => `${e.startLine}-${e.endLine}`.length), ...report.callbacks.map((c) => `${c.startLine}-${c.endLine}`.length)) + 2;
  const importsList = [...report.imports.internal, ...report.imports.external];
  const importsSuffix = importsList.length > 0 ? ` | imports: ${importsList.slice(0, 6).map((i) => baseNameNoExt(i)).join(", ")}${importsList.length > 6 ? ", \u2026" : ""}` : "";
  const lines = [
    `${report.path} ${report.language ?? "?"} ${report.lineCount ?? "?"}L \u2014 ${report.summary.symbols} symbols, ${report.summary.exports} exported${importsSuffix}`
  ];
  for (const warning of report.warnings ?? []) {
    lines.push(`WARNING: ${warning}`);
  }
  if (report.api.length > 0) {
    lines.push("API:");
    for (const entry of report.api) {
      lines.push(compactEntryLine(entry, width));
      lines.push(...compactMemberLines(entry, width, "    "));
    }
  }
  if (report.internal.length > 0) {
    lines.push("INTERNAL:");
    for (const entry of report.internal) {
      lines.push(compactEntryLine(entry, width));
      lines.push(...compactMemberLines(entry, width, "    "));
    }
  }
  if (report.callbacks.length > 0) {
    lines.push("CALLBACKS:");
    for (const callback of report.callbacks) {
      lines.push(compactCallbackLine(callback, width));
    }
  }
  if (report.callGraph) {
    const callGraph = report.callGraph;
    const suffix = callGraph.truncated ? " (truncated)" : "";
    lines.push("CALL GRAPH:");
    if (!callGraph.available) {
      lines.push(`  unavailable (${callGraph.reason ?? "unknown"})`);
    } else {
      lines.push(`  callers: ${callGraph.callers.length} \xB7 callees: ${callGraph.callees.length} \xB7 coverage: ${callGraph.coverage.status}${suffix}`);
    }
  }
  if (report.recommendedReads.length > 0) {
    lines.push("RECOMMENDED:");
    for (const r of report.recommendedReads) {
      lines.push(`  ${padRange(r.startLine, r.endLine, width)}${r.symbol ?? ""}  \u2014 ${r.reason}`);
    }
  }
  return lines.join("\n");
}
function baseNameNoExt(p) {
  const base = p.split(/[\\/]/).pop() ?? p;
  return base.replace(/\.[^./\\]+$/, "");
}
function selectMatch(candidates, kind) {
  if (candidates.length === 0)
    return {};
  const filtered = kind ? candidates.filter((c) => c.kind === kind) : candidates;
  const pool = filtered.length > 0 ? filtered : candidates;
  const match = pool[0];
  if (pool.length <= 1)
    return { match };
  return {
    match,
    ambiguous: {
      count: pool.length,
      kinds: [...new Set(pool.map((c) => c.kind))]
    }
  };
}
function resolveQualifiedMatch(symbols, qualifiedName, kind) {
  const dotIdx = qualifiedName.lastIndexOf(".");
  if (dotIdx <= 0 || dotIdx === qualifiedName.length - 1)
    return void 0;
  const parentName = qualifiedName.slice(0, dotIdx);
  const memberName = qualifiedName.slice(dotIdx + 1);
  const parent = symbols.find((candidate) => candidate.name === parentName);
  if (!parent)
    return void 0;
  const parentStart = parent.line;
  const parentEnd = parent.endLine ?? parent.line;
  const members = symbols.filter((candidate) => candidate !== parent && candidate.name === memberName && candidate.line >= parentStart && (candidate.endLine ?? candidate.line) <= parentEnd);
  if (members.length === 0)
    return void 0;
  return selectMatch(members, kind);
}
function levenshteinDistance(a, b) {
  const al = a.length;
  const bl = b.length;
  if (al === 0)
    return bl;
  if (bl === 0)
    return al;
  let prev = [];
  prev.length = bl + 1;
  let curr = [];
  curr.length = bl + 1;
  for (let j = 0; j <= bl; j++)
    prev[j] = j;
  for (let i = 1; i <= al; i++) {
    curr[0] = i;
    for (let j = 1; j <= bl; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
    }
    [prev, curr] = [curr, prev];
  }
  return prev[bl];
}
function nameSimilarity(a, b) {
  const maxLen = Math.max(a.length, b.length);
  if (maxLen === 0)
    return 1;
  return 1 - levenshteinDistance(a.toLowerCase(), b.toLowerCase()) / maxLen;
}
var SIMILAR_NAME_MIN_SCORE = 0.45;
var SIMILAR_NAME_MAX_SUGGESTIONS = 3;
function suggestSimilarNames(candidates, target) {
  const unique = [...new Set(candidates)].filter((name) => name !== target);
  return unique.map((name) => ({ name, score: nameSimilarity(target, name) })).filter((entry) => entry.score >= SIMILAR_NAME_MIN_SCORE).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, SIMILAR_NAME_MAX_SUGGESTIONS).map((entry) => entry.name);
}
async function readSymbol(file, symbolName, cwd, options = {}) {
  const { kind: symbolKindFilter } = options;
  const startedAt = Date.now();
  const absPath = path25.resolve(cwd, file);
  const log = (found) => {
    logLatency({
      type: "phase",
      phase: "read_symbol",
      filePath: absPath,
      durationMs: Date.now() - startedAt,
      metadata: { symbol: symbolName, found }
    });
  };
  let content;
  try {
    content = fs19.readFileSync(absPath, "utf-8");
  } catch {
    log(false);
    return { found: false, path: absPath, name: symbolName };
  }
  const kind = detectFileKind(absPath);
  const languageId = tsLangForFile(absPath, kind);
  if (!languageId) {
    log(false);
    return { found: false, path: absPath, name: symbolName };
  }
  const { symbols, callbacks: allCallbacks, callbackError, error: extractionError, warnings: extractionWarnings } = await extractFile(absPath, languageId, content);
  if (extractionError) {
    log(false);
    return {
      found: false,
      path: absPath,
      name: symbolName,
      error: extractionError
    };
  }
  const lines = content.split(/\r?\n/);
  const qualified = resolveQualifiedMatch(symbols, symbolName, symbolKindFilter);
  const unqualifiedMatches = symbols.filter((candidate) => candidate.name === symbolName);
  const selection = qualified ?? selectMatch(unqualifiedMatches, symbolKindFilter);
  if (selection.match) {
    const sym = selection.match;
    const startLine = sym.docStartLine ?? sym.line;
    const endLine = sym.endLine ?? sym.line;
    const source2 = lines.slice(startLine - 1, endLine).join("\n");
    log(true);
    return {
      found: true,
      path: absPath,
      name: sym.name,
      kind: sym.kind,
      startLine,
      endLine,
      signature: sym.signature,
      source: source2,
      ...selection.ambiguous ? { ambiguous: selection.ambiguous } : {}
    };
  }
  if (callbackError) {
    const message = `Callback extraction failed: ${callbackError}`;
    logLatency({
      type: "phase",
      phase: "read_symbol_callback_extract_error",
      filePath: absPath,
      durationMs: Date.now() - startedAt,
      metadata: { symbol: symbolName, error: message }
    });
    log(false);
    return { found: false, path: absPath, name: symbolName, error: message };
  }
  const callbackWarnings = [...extractionWarnings ?? []];
  const callback = allCallbacks.find((candidate) => candidate.name === symbolName);
  if (!callback) {
    log(false);
    const corpus = [
      ...symbols.filter((candidate) => !candidate.local).map((c) => c.name),
      ...allCallbacks.map((c) => c.name)
    ];
    const suggestions = suggestSimilarNames(corpus, symbolName);
    return {
      found: false,
      path: absPath,
      name: symbolName,
      ...callbackWarnings.length > 0 ? { warnings: callbackWarnings } : {},
      ...suggestions.length > 0 ? { suggestions } : {}
    };
  }
  const source = lines.slice(callback.startLine - 1, callback.endLine).join("\n");
  log(true);
  return {
    found: true,
    path: absPath,
    name: callback.name,
    kind: callback.kind,
    startLine: callback.startLine,
    endLine: callback.endLine,
    signature: callback.signature,
    source
  };
}
function symbolMatchesKind(kind, filters) {
  if (filters.size === 0)
    return true;
  const normalized = kind.toLowerCase();
  if (filters.has(normalized))
    return true;
  if (filters.has("function") && normalized.includes("function"))
    return true;
  if (filters.has("method") && normalized.includes("method"))
    return true;
  if (filters.has("type") && ["class", "interface", "struct", "enum", "trait", "type"].includes(normalized)) {
    return true;
  }
  return false;
}
function callbackMatchesKind(kind, filters) {
  if (filters.size === 0)
    return true;
  const normalized = kind.toLowerCase();
  return filters.has(normalized) || filters.has("callback") || filters.has("closure") || filters.has("lambda") || normalized.includes("callback") && filters.has("function");
}
function symbolParentChain(symbol, symbols) {
  const chain = symbols.filter((candidate) => {
    if (candidate === symbol)
      return false;
    const start = candidate.line;
    const end = candidate.endLine ?? candidate.line;
    const symEnd = symbol.endLine ?? symbol.line;
    return start <= symbol.line && end >= symEnd && end - start > symEnd - symbol.line;
  }).sort((a, b) => a.line - b.line || (b.endLine ?? b.line) - b.line - ((a.endLine ?? a.line) - a.line)).map((candidate) => candidate.name);
  return chain.length > 0 ? chain : void 0;
}
function clampSliceRange(targetLine, startLine, endLine, limit) {
  const boundedLimit = Math.max(1, Math.min(endLine - startLine + 1, limit));
  let sliceStart = targetLine - Math.floor(boundedLimit / 2);
  sliceStart = Math.max(startLine, Math.min(sliceStart, endLine - boundedLimit + 1));
  return { startLine: sliceStart, endLine: sliceStart + boundedLimit - 1 };
}
function enclosingOutline(selected, symbols, callbacks, filters, filePath) {
  const items = [];
  for (const sym of symbols) {
    const startLine = sym.line;
    const endLine = sym.endLine ?? sym.line;
    if (startLine === selected.startLine && endLine === selected.endLine)
      continue;
    if (startLine < selected.startLine || endLine > selected.endLine)
      continue;
    if (!symbolMatchesKind(sym.kind, filters))
      continue;
    items.push({
      name: sym.name,
      kind: sym.kind,
      startLine,
      endLine,
      signature: sym.signature,
      parentChain: symbolParentChain(sym, symbols),
      read: readArgsFor(filePath, startLine, endLine)
    });
  }
  for (const callback of callbacks) {
    if (callback.startLine < selected.startLine || callback.endLine > selected.endLine)
      continue;
    if (!callbackMatchesKind(callback.kind, filters))
      continue;
    items.push({
      name: callback.name,
      kind: callback.kind,
      startLine: callback.startLine,
      endLine: callback.endLine,
      signature: callback.signature,
      parentChain: callback.parentChain,
      read: readArgsFor(filePath, callback.startLine, callback.endLine)
    });
  }
  return items.sort((a, b) => a.startLine - b.startLine || a.endLine - a.startLine - (b.endLine - b.startLine)).slice(0, 25);
}
async function readEnclosing(file, line, cwd, options) {
  const startedAt = Date.now();
  const absPath = path25.resolve(cwd, file);
  const targetLine = Math.max(1, Math.floor(line));
  const log = (found) => {
    logLatency({
      type: "phase",
      phase: "read_enclosing",
      filePath: absPath,
      durationMs: Date.now() - startedAt,
      metadata: { line: targetLine, found }
    });
  };
  let content;
  try {
    content = fs19.readFileSync(absPath, "utf-8");
  } catch {
    log(false);
    return { found: false, path: absPath, line: targetLine };
  }
  const kind = detectFileKind(absPath);
  const languageId = tsLangForFile(absPath, kind);
  if (!languageId) {
    log(false);
    return { found: false, path: absPath, line: targetLine };
  }
  const { symbols, callbacks, callbackError, error: extractionError, warnings: extractionWarnings } = await extractFile(absPath, languageId, content);
  if (extractionError) {
    log(false);
    return {
      found: false,
      path: absPath,
      line: targetLine,
      error: extractionError
    };
  }
  const filters = new Set((options?.kinds ?? []).map((value) => value.toLowerCase()));
  const warnings = [...extractionWarnings ?? []];
  if (callbackError) {
    const message = `Callback extraction failed: ${callbackError}`;
    warnings.push(message);
    logLatency({
      type: "phase",
      phase: "read_enclosing_callback_extract_error",
      filePath: absPath,
      durationMs: Date.now() - startedAt,
      metadata: { line: targetLine, error: message }
    });
  }
  const candidates = [];
  for (const sym of symbols.filter((candidate) => !candidate.local)) {
    const startLine = sym.line;
    const endLine = sym.endLine ?? sym.line;
    if (targetLine < startLine || targetLine > endLine)
      continue;
    if (!symbolMatchesKind(sym.kind, filters))
      continue;
    candidates.push({
      name: sym.name,
      kind: sym.kind,
      startLine,
      endLine,
      signature: sym.signature,
      parentChain: symbolParentChain(sym, symbols),
      priority: 1
    });
  }
  for (const callback of callbacks) {
    if (targetLine < callback.startLine || targetLine > callback.endLine)
      continue;
    if (!callbackMatchesKind(callback.kind, filters))
      continue;
    candidates.push({
      name: callback.name,
      kind: callback.kind,
      startLine: callback.startLine,
      endLine: callback.endLine,
      signature: callback.signature,
      parentChain: callback.parentChain,
      priority: 0
    });
  }
  candidates.sort((a, b) => {
    const span = a.endLine - a.startLine - (b.endLine - b.startLine);
    return span || a.priority - b.priority || a.startLine - b.startLine;
  });
  const selected = candidates[0];
  if (!selected) {
    log(false);
    return {
      found: false,
      path: absPath,
      line: targetLine,
      ...warnings.length > 0 ? { warnings } : {}
    };
  }
  const lines = content.split(/\r?\n/);
  const limit = selected.endLine - selected.startLine + 1;
  if (options?.maxLines && limit > options.maxLines) {
    const oversize = options.onOversize ?? "error";
    const base = {
      path: absPath,
      line: targetLine,
      name: selected.name,
      kind: selected.kind,
      startLine: selected.startLine,
      endLine: selected.endLine,
      signature: selected.signature,
      parentChain: selected.parentChain,
      enclosingStartLine: selected.startLine,
      enclosingEndLine: selected.endLine,
      ...warnings.length > 0 ? { warnings } : {}
    };
    if (oversize === "slice") {
      const sliceLimit = Math.max(1, Math.floor(options.aroundLine ?? options.maxLines ?? 80));
      const slice = clampSliceRange(targetLine, selected.startLine, selected.endLine, sliceLimit);
      const source2 = lines.slice(slice.startLine - 1, slice.endLine).join("\n");
      log(true);
      return {
        ...base,
        found: true,
        startLine: slice.startLine,
        endLine: slice.endLine,
        partial: true,
        selection: {
          strategy: "oversize-slice",
          source: "tree-sitter",
          confidence: "medium"
        },
        source: source2
      };
    }
    if (oversize === "outline") {
      log(false);
      return {
        ...base,
        found: false,
        selection: {
          strategy: "oversize-outline",
          source: "tree-sitter",
          confidence: "medium"
        },
        outline: enclosingOutline(selected, symbols, callbacks, filters, absPath),
        error: `Enclosing ${selected.kind} spans ${limit} lines, above maxLines ${options.maxLines}`
      };
    }
    log(false);
    return {
      ...base,
      found: false,
      error: `Enclosing ${selected.kind} spans ${limit} lines, above maxLines ${options.maxLines}`
    };
  }
  const source = lines.slice(selected.startLine - 1, selected.endLine).join("\n");
  log(true);
  return {
    found: true,
    path: absPath,
    line: targetLine,
    name: selected.name,
    kind: selected.kind,
    startLine: selected.startLine,
    endLine: selected.endLine,
    enclosingStartLine: selected.startLine,
    enclosingEndLine: selected.endLine,
    signature: selected.signature,
    parentChain: selected.parentChain,
    selection: {
      strategy: "range-containment",
      source: "tree-sitter",
      confidence: "high"
    },
    ...warnings.length > 0 ? { warnings } : {},
    source
  };
}

// dist/clients/project-report.js
import * as path26 from "node:path";
var DEFAULT_LIMIT = 10;
var STALE_THRESHOLD_MS = 15 * 6e4;
var LOW_COVERAGE_THRESHOLD = 0.8;
var DEAD_WEIGHT_DISCLAIMER = "Low confidence: dynamic imports, runtime registration, and test-only reachability all produce false positives here \u2014 verify with a real usage search (symbol_search/grep) before deleting anything listed.";
function clampLimit(limit) {
  return Math.max(1, Math.floor(limit ?? DEFAULT_LIMIT));
}
function toDisplayPath2(p, projectRoot) {
  return toProjectRelativePath(p, projectRoot);
}
function suggestedNext(displayPath) {
  return { tool: "module_report", path: displayPath };
}
function normalizeFocus2(focus) {
  return (focus ?? "").toLowerCase().split(/[^a-z0-9_.]+/).map((part) => part.trim()).filter((part) => part.length >= 3).slice(0, 8);
}
function focusScore2(text, terms) {
  if (terms.length === 0)
    return 0;
  const haystack = text.toLowerCase();
  return terms.reduce((score, term) => score + (haystack.includes(term) ? 6 : 0), 0);
}
function buildFileDegrees(graph) {
  const fanIn = /* @__PURE__ */ new Map();
  const fanOut = /* @__PURE__ */ new Map();
  for (const edge of graph.edges) {
    if (edge.kind !== "imports")
      continue;
    if (edge.from === edge.to)
      continue;
    const fromNode = graph.nodes.get(edge.from);
    const toNode = graph.nodes.get(edge.to);
    if (!fromNode || fromNode.kind !== "file")
      continue;
    if (!toNode || toNode.kind !== "file")
      continue;
    let outSet = fanOut.get(edge.from);
    if (!outSet) {
      outSet = /* @__PURE__ */ new Set();
      fanOut.set(edge.from, outSet);
    }
    outSet.add(edge.to);
    let inSet = fanIn.get(edge.to);
    if (!inSet) {
      inSet = /* @__PURE__ */ new Set();
      fanIn.set(edge.to, inSet);
    }
    inSet.add(edge.from);
  }
  return { fanIn, fanOut };
}
function computeTrust(graph, cwd, drift) {
  const filesCovered = graph.fileNodes.size;
  const snapshot = loadProjectSnapshotWithoutWordIndex(cwd);
  const snapshotFileCount = snapshot ? Object.keys(snapshot.files).length : 0;
  const persistedTotal = graph.persistCoverage?.totalFiles ?? 0;
  const filesTotal = Math.max(filesCovered, snapshotFileCount, persistedTotal);
  const coverage = filesTotal > 0 ? filesCovered / filesTotal : 1;
  let exact = 0;
  let imp = 0;
  let receiverType = 0;
  let nameOnly = 0;
  for (const edge of graph.edges) {
    if (edge.kind !== "calls" && edge.kind !== "references")
      continue;
    switch (edge.resolution) {
      case "exact":
        exact += 1;
        break;
      case "import":
        imp += 1;
        break;
      case "receiver-type":
        receiverType += 1;
        break;
      case "name-only":
        nameOnly += 1;
        break;
      default:
        break;
    }
  }
  const sampleSize = exact + imp + receiverType + nameOnly;
  const frac = (n) => sampleSize > 0 ? n / sampleSize : 0;
  const ageMs = Date.now() - Date.parse(graph.builtAt);
  const stale = Number.isFinite(ageMs) && ageMs > STALE_THRESHOLD_MS;
  const lowCoverage = coverage < LOW_COVERAGE_THRESHOLD || graph.persistCoverage?.partial === true;
  const notes = [];
  if (drift)
    notes.push(formatReviewGraphRevisionDriftNote(drift));
  if (stale) {
    const ageMin = Math.round(ageMs / 6e4);
    notes.push(`Graph is stale: built ${ageMin}m ago. Sections below may miss recent edits \u2014 run pilens_rebuild or re-analyze to refresh.`);
  }
  if (lowCoverage) {
    if (graph.persistCoverage?.partial) {
      const p = graph.persistCoverage;
      const totalFiles = p.totalFiles ?? filesCovered;
      const persistedFiles = p.persistedFiles ?? filesCovered;
      if (p.sourceFilesTruncated) {
        notes.push(`Partial source walk: at least ${totalFiles} source files were represented before the visited-entry budget stopped enumeration; ${persistedFiles} files are in this graph.`);
      } else {
        notes.push(`Partial persisted graph: ${p.persistedNodes}/${p.totalNodes} nodes, ${p.persistedEdges}/${p.totalEdges} edges, and ${persistedFiles}/${totalFiles} files were retained under the ${p.cap}-element cap \u2014 whole subsystems may be invisible below.`);
      }
    } else {
      notes.push(`Low coverage: only ${filesCovered}/${filesTotal} project files (${Math.round(coverage * 100)}%) are in the graph \u2014 whole subsystems may be invisible below.`);
    }
  }
  return {
    graphBuiltAt: graph.builtAt,
    filesCovered,
    filesTotal,
    coverage,
    resolution: {
      exact: frac(exact),
      import: frac(imp),
      receiverType: frac(receiverType),
      nameOnly: frac(nameOnly),
      sampleSize
    },
    stale,
    ...drift ? { revisionDrift: drift } : {},
    lowCoverage,
    persistCoverage: graph.persistCoverage?.partial ? graph.persistCoverage : void 0,
    notes
  };
}
function roleFor(graph, filePath) {
  const symbolIds = graph.symbolNodesByFile.get(filePath) ?? [];
  const scored = [];
  for (const symbolId of symbolIds) {
    const node = graph.nodes.get(symbolId);
    if (!node || !node.exported || !node.symbolName)
      continue;
    const refs = (graph.edgesByTo.get(symbolId) ?? []).filter((e) => e.kind === "calls" || e.kind === "references").length;
    scored.push({ name: node.qualifiedName ?? node.symbolName, refs });
  }
  scored.sort((a, b) => b.refs - a.refs);
  const top = scored.slice(0, 3).filter((s) => s.refs > 0);
  return top.length > 0 ? top.map((s) => s.name).join(", ") : void 0;
}
async function computeHubs(graph, degrees, cwd, limit, focusTerms) {
  const { computeTransitiveImpact } = await import("../chunk-34U7PLGE.js");
  const ranked = [...graph.fileNodes.entries()].map(([filePath, fileNodeId]) => ({
    filePath,
    fileNodeId,
    fanIn: degrees.fanIn.get(fileNodeId)?.size ?? 0
  })).filter((f) => f.fanIn > 0).sort((a, b) => {
    const focusDelta = focusScore2(a.filePath, focusTerms) - focusScore2(b.filePath, focusTerms);
    if (focusDelta !== 0)
      return -focusDelta;
    return b.fanIn - a.fanIn || a.filePath.localeCompare(b.filePath);
  }).slice(0, limit);
  return ranked.map((f) => {
    const impact = computeTransitiveImpact(graph, f.filePath, { maxDepth: 3 });
    const display = toDisplayPath2(f.filePath, cwd);
    return {
      file: display,
      fanIn: f.fanIn,
      blastRadius: impact.hits.length,
      role: roleFor(graph, f.filePath),
      suggestedNext: suggestedNext(display)
    };
  });
}
function computeEntryPoints(graph, degrees, cwd, limit, focusTerms) {
  const candidates = [...graph.fileNodes.entries()].map(([filePath, fileNodeId]) => ({
    filePath,
    fanIn: degrees.fanIn.get(fileNodeId)?.size ?? 0,
    fanOut: degrees.fanOut.get(fileNodeId)?.size ?? 0
  })).filter((f) => f.fanIn === 0 && f.fanOut > 0).sort((a, b) => {
    const focusDelta = focusScore2(a.filePath, focusTerms) - focusScore2(b.filePath, focusTerms);
    if (focusDelta !== 0)
      return -focusDelta;
    return b.fanOut - a.fanOut || a.filePath.localeCompare(b.filePath);
  });
  const entryPointFiles = new Set(candidates.map((c) => c.filePath));
  const entryPoints = candidates.slice(0, limit).map((f) => {
    const display = toDisplayPath2(f.filePath, cwd);
    return {
      file: display,
      fanIn: f.fanIn,
      fanOut: f.fanOut,
      suggestedNext: suggestedNext(display)
    };
  });
  return { entryPoints, entryPointFiles };
}
var DOMINANCE_THRESHOLD = 0.4;
function directoryClusters(filePaths, cwd) {
  const segmentsOf = (filePath) => toDisplayPath2(filePath, cwd).split("/").filter(Boolean);
  const topCounts = /* @__PURE__ */ new Map();
  const perFileSegments = /* @__PURE__ */ new Map();
  for (const filePath of filePaths) {
    const segments = segmentsOf(filePath);
    perFileSegments.set(filePath, segments);
    const top = segments.length > 1 ? segments[0] : "(root)";
    topCounts.set(top, (topCounts.get(top) ?? 0) + 1);
  }
  const total = filePaths.length || 1;
  const dominant = new Set([...topCounts.entries()].filter(([, count]) => count / total >= DOMINANCE_THRESHOLD).map(([seg]) => seg));
  const clusterOf = /* @__PURE__ */ new Map();
  for (const filePath of filePaths) {
    const segments = perFileSegments.get(filePath) ?? [];
    if (segments.length <= 1) {
      clusterOf.set(filePath, "(root)");
      continue;
    }
    const top = segments[0];
    if (dominant.has(top) && segments.length > 2) {
      clusterOf.set(filePath, `${segments[0]}/${segments[1]}`);
    } else {
      clusterOf.set(filePath, top);
    }
  }
  return clusterOf;
}
function tarjanSCCs(nodes, adjacency) {
  let index = 0;
  const indices = /* @__PURE__ */ new Map();
  const lowlink = /* @__PURE__ */ new Map();
  const onStack = /* @__PURE__ */ new Set();
  const stack = [];
  const sccs = [];
  function strongConnect(v) {
    indices.set(v, index);
    lowlink.set(v, index);
    index += 1;
    stack.push(v);
    onStack.add(v);
    for (const w of adjacency.get(v) ?? []) {
      if (!indices.has(w)) {
        strongConnect(w);
        lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
      } else if (onStack.has(w)) {
        lowlink.set(v, Math.min(lowlink.get(v), indices.get(w)));
      }
    }
    if (lowlink.get(v) === indices.get(v)) {
      const scc = [];
      let w;
      do {
        w = stack.pop();
        onStack.delete(w);
        scc.push(w);
      } while (w !== v);
      sccs.push(scc);
    }
  }
  for (const v of nodes) {
    if (!indices.has(v))
      strongConnect(v);
  }
  return sccs;
}
function computeSubsystems(graph, cwd, limit) {
  const filePaths = [...graph.fileNodes.keys()];
  const clusterOf = directoryClusters(filePaths, cwd);
  const fileNodeIdToCluster = /* @__PURE__ */ new Map();
  for (const [filePath, nodeId] of graph.fileNodes) {
    const cluster = clusterOf.get(filePath);
    if (cluster)
      fileNodeIdToCluster.set(nodeId, cluster);
  }
  const edgeCounts = /* @__PURE__ */ new Map();
  for (const edge of graph.edges) {
    if (edge.kind !== "imports")
      continue;
    const fromCluster = fileNodeIdToCluster.get(edge.from);
    const toCluster = fileNodeIdToCluster.get(edge.to);
    if (!fromCluster || !toCluster || fromCluster === toCluster)
      continue;
    const key = `${fromCluster}|${toCluster}`;
    edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);
  }
  const directories = [...new Set(clusterOf.values())].sort((a, b) => a.localeCompare(b));
  const edges = [...edgeCounts.entries()].map(([key, count]) => {
    const [from, to] = key.split("|");
    return { from, to, count };
  }).sort((a, b) => b.count - a.count || a.from.localeCompare(b.from));
  const seenPairs = /* @__PURE__ */ new Set();
  const violations = [];
  for (const edge of edges) {
    const pairKey = [edge.from, edge.to].sort((a, b) => a.localeCompare(b)).join("|");
    if (seenPairs.has(pairKey))
      continue;
    const reverseKey = `${edge.to}|${edge.from}`;
    const reverseCount = edgeCounts.get(reverseKey);
    if (reverseCount === void 0 || reverseCount === edge.count)
      continue;
    seenPairs.add(pairKey);
    if (edge.count < reverseCount) {
      violations.push({
        from: edge.from,
        to: edge.to,
        count: edge.count,
        dominantCount: reverseCount
      });
    } else {
      violations.push({
        from: edge.to,
        to: edge.from,
        count: reverseCount,
        dominantCount: edge.count
      });
    }
  }
  violations.sort((a, b) => b.count - a.count || a.from.localeCompare(b.from));
  const adjacency = /* @__PURE__ */ new Map();
  for (const dir of directories)
    adjacency.set(dir, /* @__PURE__ */ new Set());
  for (const edge of edges) {
    adjacency.get(edge.from)?.add(edge.to);
  }
  const sccs = tarjanSCCs(directories, adjacency).filter((scc) => scc.length > 1);
  const cycles = sccs.map((scc) => {
    const members = new Set(scc);
    let edgeCount = 0;
    for (const edge of edges) {
      if (members.has(edge.from) && members.has(edge.to))
        edgeCount += edge.count;
    }
    return { dirs: [...scc].sort((a, b) => a.localeCompare(b)), edgeCount };
  }).sort((a, b) => b.edgeCount - a.edgeCount);
  return {
    directories,
    edges: edges.slice(0, limit),
    cycles: cycles.slice(0, limit),
    violations: violations.slice(0, limit)
  };
}
function computeRiskHotspots(graph, degrees, cwd, limit, focusTerms) {
  const ranked = [...graph.fileNodes.entries()].map(([filePath, fileNodeId]) => {
    const symbolIds = graph.symbolNodesByFile.get(filePath) ?? [];
    let maxComplexity = 0;
    for (const symbolId of symbolIds) {
      const complexity = graph.nodes.get(symbolId)?.metadata?.cyclomaticComplexity;
      if (typeof complexity === "number" && complexity > maxComplexity) {
        maxComplexity = complexity;
      }
    }
    const fanIn = degrees.fanIn.get(fileNodeId)?.size ?? 0;
    return { filePath, fanIn, maxComplexity, score: fanIn * maxComplexity };
  }).filter((f) => f.score > 0).sort((a, b) => {
    const focusDelta = focusScore2(a.filePath, focusTerms) - focusScore2(b.filePath, focusTerms);
    if (focusDelta !== 0)
      return -focusDelta;
    return b.score - a.score || a.filePath.localeCompare(b.filePath);
  }).slice(0, limit);
  return ranked.map((f) => {
    const display = toDisplayPath2(f.filePath, cwd);
    return {
      file: display,
      fanIn: f.fanIn,
      maxComplexity: f.maxComplexity,
      score: f.score,
      suggestedNext: suggestedNext(display)
    };
  });
}
function computeDeadWeight(graph, degrees, entryPointFiles, cwd, limit) {
  const candidates = [...graph.fileNodes.entries()].filter(([filePath, nodeId]) => {
    if (entryPointFiles.has(filePath))
      return false;
    const fanIn = degrees.fanIn.get(nodeId)?.size ?? 0;
    return fanIn === 0;
  }).map(([filePath, fileNodeId]) => ({
    filePath,
    fanOut: degrees.fanOut.get(fileNodeId)?.size ?? 0
  })).sort((a, b) => a.fanOut - b.fanOut || a.filePath.localeCompare(b.filePath)).slice(0, limit);
  return {
    files: candidates.map((f) => {
      const display = toDisplayPath2(f.filePath, cwd);
      return { file: display, suggestedNext: suggestedNext(display) };
    }),
    disclaimer: DEAD_WEIGHT_DISCLAIMER
  };
}
var inFlightGraphBuilds = /* @__PURE__ */ new Set();
function triggerBackgroundGraphBuild(cwd, deps) {
  const key = normalizeMapKey(path26.resolve(cwd));
  if (inFlightGraphBuilds.has(key))
    return "already_running";
  const deduped = deps.isGraphBuildInFlight(key, []);
  inFlightGraphBuilds.add(key);
  const build = deps.buildOrUpdateGraph(key, [], new deps.FactStore()).catch(() => {
  });
  void build.finally(() => inFlightGraphBuilds.delete(key));
  return deduped ? "already_running" : "started";
}
async function projectReport(cwd, options) {
  const limit = clampLimit(options?.limit);
  const focusTerms = normalizeFocus2(options?.focus);
  const view = options?.view;
  const { getCachedReviewGraph, getReviewGraphSizeSkipVerdict, getLastReviewGraphBuildAttempt, buildOrUpdateGraph, isGraphBuildInFlight, getReviewGraphRevisionDrift } = await import("../chunk-VD4JQCQC.js");
  let graph;
  try {
    graph = getCachedReviewGraph(cwd);
  } catch {
    graph = void 0;
  }
  if (!graph) {
    let sizeSkip;
    try {
      sizeSkip = getReviewGraphSizeSkipVerdict(cwd);
    } catch {
      sizeSkip = void 0;
    }
    if (sizeSkip) {
      const lastBuildAttempt3 = getLastReviewGraphBuildAttempt(cwd);
      return {
        available: false,
        hint: `review graph disabled: project has more than ${sizeSkip.maxFileCount} files (cap ${sizeSkip.maxFileCount}) \u2014 raise maxProjectFiles in .pi-lens.json or set PI_LENS_REVIEW_GRAPH_MAX_FILES; for CI/cron, run npx pi-lens build-graph after configuring the cap`,
        ...lastBuildAttempt3 ? { lastBuildAttempt: lastBuildAttempt3 } : {},
        ...view ? { view } : {}
      };
    }
    const previousAttempt = getLastReviewGraphBuildAttempt(cwd);
    const { FactStore: FactStore2 } = await import("../chunk-YSNFMJKN.js");
    const trigger = triggerBackgroundGraphBuild(cwd, {
      buildOrUpdateGraph,
      isGraphBuildInFlight,
      FactStore: FactStore2
    });
    const started = trigger === "started";
    const lastBuildAttempt2 = started ? getLastReviewGraphBuildAttempt(cwd) ?? previousAttempt : previousAttempt;
    const terminal = previousAttempt?.outcome === "failed" || previousAttempt?.outcome === "skipped";
    const cause = terminal ? `Review graph unavailable: ${previousAttempt?.reason ?? previousAttempt?.outcome}. ` : "";
    return {
      available: false,
      hint: started ? terminal ? `${cause}A retry was started.` : "No review graph cached for this workspace yet \u2014 a build was kicked off in the background; retry this call shortly." : `${cause}A build is already running for this workspace; retry this call shortly.`,
      ...lastBuildAttempt2 ? { lastBuildAttempt: lastBuildAttempt2 } : {},
      ...view ? { view } : {}
    };
  }
  const degrees = buildFileDegrees(graph);
  const trust = computeTrust(graph, cwd, getReviewGraphRevisionDrift(cwd));
  const hubs = await computeHubs(graph, degrees, cwd, limit, focusTerms);
  const { entryPoints, entryPointFiles } = computeEntryPoints(graph, degrees, cwd, limit, focusTerms);
  const subsystems = computeSubsystems(graph, cwd, limit);
  const riskHotspots = computeRiskHotspots(graph, degrees, cwd, limit, focusTerms);
  const deadWeight = computeDeadWeight(graph, degrees, entryPointFiles, cwd, limit);
  const lastBuildAttempt = getLastReviewGraphBuildAttempt(cwd);
  return {
    available: true,
    ...lastBuildAttempt ? { lastBuildAttempt } : {},
    ...view ? { view } : {},
    trust,
    hubs,
    entryPoints,
    subsystems,
    riskHotspots,
    deadWeight
  };
}
function fmtPct(n) {
  return `${Math.round(n * 100)}%`;
}
function renderCompactProjectReport(report) {
  if (!report.available) {
    return `project_report \u2014 unavailable${report.hint ? `: ${report.hint}` : ""}`;
  }
  const lines = [];
  if (report.lastBuildAttempt?.reason) {
    lines.push(`! build: ${report.lastBuildAttempt.reason}`);
  }
  const t = report.trust;
  if (t) {
    lines.push(`TRUST: built ${t.graphBuiltAt} \u2014 ${t.filesCovered}/${t.filesTotal} files (${fmtPct(t.coverage)} coverage)` + (t.resolution.sampleSize > 0 ? ` \u2014 resolution: exact ${fmtPct(t.resolution.exact)}, import ${fmtPct(t.resolution.import)}, receiver-type ${fmtPct(t.resolution.receiverType)}, name-only ${fmtPct(t.resolution.nameOnly)}` : " \u2014 no resolution-tagged edges yet"));
    for (const note of t.notes)
      lines.push(`  ! ${note}`);
  }
  if (report.hubs?.length) {
    lines.push("HUBS:");
    for (const h of report.hubs) {
      const role = h.role ? ` \u2014 ${h.role}` : "";
      lines.push(`  ${h.file}${role}; ${h.fanIn} importer(s), blastRadius ${h.blastRadius}`);
    }
  }
  if (report.entryPoints?.length) {
    lines.push("ENTRY POINTS:");
    for (const e of report.entryPoints) {
      lines.push(`  ${e.file}; fan-out ${e.fanOut}`);
    }
  }
  if (report.subsystems) {
    const s = report.subsystems;
    lines.push(`SUBSYSTEMS: ${s.directories.length} directories, ${s.edges.length} cross-dir edge group(s)`);
    if (s.cycles.length > 0) {
      lines.push("  CYCLES:");
      for (const c of s.cycles) {
        lines.push(`    ${c.dirs.join(" <-> ")} (${c.edgeCount} edges)`);
      }
    }
    if (s.violations.length > 0) {
      lines.push("  LAYERING VIOLATIONS:");
      for (const v of s.violations) {
        lines.push(`    ${v.from} -> ${v.to} (${v.count} edge(s), against the dominant ${v.to} -> ${v.from} direction, ${v.dominantCount} edge(s))`);
      }
    }
  }
  if (report.riskHotspots?.length) {
    lines.push("RISK HOTSPOTS:");
    for (const r of report.riskHotspots) {
      lines.push(`  ${r.file}; fan-in ${r.fanIn} \xD7 max complexity ${r.maxComplexity} = ${r.score}`);
    }
  }
  if (report.deadWeight) {
    lines.push(`DEAD WEIGHT (${report.deadWeight.disclaimer}):`);
    for (const d of report.deadWeight.files) {
      lines.push(`  ${d.file}`);
    }
    if (report.deadWeight.files.length === 0)
      lines.push("  (none found)");
  }
  return lines.join("\n");
}

// dist/clients/effective-config.js
import * as os3 from "node:os";
import * as path27 from "node:path";
function isEffectiveFileViewError(file) {
  return "error" in file;
}
function countBy(items, key) {
  const counts = {};
  for (const item of items) {
    const name = key(item);
    counts[name] = (counts[name] ?? 0) + 1;
  }
  return counts;
}
function redactServerSpec(entry) {
  if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
    return void 0;
  }
  const record = entry;
  const command = typeof record.command === "string" ? record.command : void 0;
  const args = Array.isArray(record.args) ? record.args : [];
  const env = typeof record.env === "object" && record.env !== null ? Object.keys(record.env).sort(compareOrdinal) : [];
  const rawCovers = record.covers;
  let covers;
  if (Array.isArray(rawCovers)) {
    covers = [];
    for (const member of rawCovers) {
      if (typeof member === "string")
        covers.push(member);
    }
  }
  return {
    ...command === void 0 ? {} : { command },
    argvCount: (command === void 0 ? 0 : 1) + args.length,
    envNames: env,
    ...covers === void 0 || covers.length === 0 ? {} : { covers }
  };
}
function provenanceOfSubtree(resolved, pointer) {
  const own = provenanceFor(resolved, pointer);
  if (own)
    return own;
  const prefix = `${pointer}/`;
  let best;
  for (const entry of resolved.provenance.values()) {
    if (!entry.key.startsWith(prefix))
      continue;
    if (!best || compareKeys(entry.key, best.key) < 0)
      best = entry;
  }
  return best;
}
function viewOf(entry, homeDir) {
  if (!entry)
    return void 0;
  return {
    key: entry.key,
    tier: entry.tier,
    ...entry.file === void 0 ? {} : { file: homeRelativePath(entry.file, homeDir) },
    ...entry.trust === void 0 ? {} : { trust: entry.trust }
  };
}
function decidedByOrNothing(entry) {
  return entry === void 0 ? {} : { decidedBy: entry };
}
async function effectiveConfig(options = {}) {
  const cwd = options.cwd ?? process.cwd();
  const homeDir = options.homeDir ?? os3.homedir();
  const resolveAt = (dir) => resolvePiLensConfig({
    cwd: dir,
    globalDir: getGlobalPiLensDir(),
    // Production resolution for the global tier — see loadLSPConfig's
    // comment (global-config-location PR, refs #2457).
    globalConfigPath: getPiLensGlobalConfigPath(),
    homeDir
  });
  const requestedFile = options.file === void 0 ? void 0 : path27.resolve(cwd, options.file);
  const fileOutsideCwd = requestedFile !== void 0 && !isSameOrWithin(cwd, requestedFile);
  const absolute = fileOutsideCwd ? void 0 : requestedFile;
  const resolution = resolveAt(absolute === void 0 ? cwd : path27.dirname(absolute));
  const resolved = {
    value: resolution.value,
    provenance: resolution.provenance
  };
  const summary = summarizeConfigResolution(resolution, homeDir);
  const homeRelativeCwd = homeRelativePath(cwd, homeDir);
  const view = {
    cwd: homeRelativeCwd,
    documents: summary.documents,
    provenance: provenanceView(resolved, homeDir).entries,
    provenanceCounts: summary.countsByTier,
    recordCounts: countBy(resolution.records, (record) => record.code),
    tools: LENS_TOOL_NAMES.map((name) => ({
      name,
      enabled: resolveLensToolEnabled(name, resolved.value, void 0, options.noTools)
    })),
    ...fileOutsideCwd ? {
      file: {
        error: `file is outside cwd (${homeRelativeCwd}); query with cwd set to the file's own workspace instead`
      }
    } : absolute === void 0 ? {} : {
      file: await fileView(
        absolute,
        resolved,
        homeDir,
        cwd,
        // The gates read the LSP slice of that SAME resolution, through
        // the same `registerLSPConfig` conversion `initLSPConfig` uses —
        // no session-root registration, no per-root config-store write
        // (P11/P12). Computed here rather than hoisted into a
        // `{ absolute, lspConfig }` struct (#2520): `absolute` is already
        // this branch's narrowed local, so the struct's own field was
        // carrying the same fact twice.
        registerLSPConfig(lspConfigOf(resolution.value))
      )
    }
  };
  return view;
}
function toolReason(selected, kind) {
  if (selected)
    return "selected";
  return kind === void 0 ? "no-dispatch-plan" : "not-registered-for-kind";
}
async function fileView(absolute, resolved, homeDir, workspaceCwd, lspConfig) {
  const language = resolveLanguage(absolute);
  const kind = detectFileKind(absolute);
  const section = lspSectionOf(resolved.value);
  const customServers = customServerSpecsOf(resolved.value);
  const denied = Array.isArray(section.disabledServers) ? section.disabledServers : [];
  const denyPointer = `/${LSP_NAMESPACE_KEY}/disabledServers`;
  const decidedByDeny = (id) => {
    const index = denied.indexOf(id);
    return viewOf(provenanceFor(resolved, index >= 0 ? `${denyPointer}/${index}` : denyPointer), homeDir);
  };
  const servers = explainServersForFile(absolute, lspConfig).map((entry) => {
    const spec = redactServerSpec(customServers[entry.server.id]);
    return {
      id: entry.server.id,
      selected: entry.selected,
      reason: entry.reason,
      ...entry.server.role === void 0 ? {} : { role: entry.server.role },
      ...entry.reason === "disabled-by-config" ? decidedByOrNothing(decidedByDeny(entry.server.id)) : {},
      ...spec === void 0 ? {} : {
        spec,
        ...entry.reason === "disabled-by-config" ? {} : {
          decidedBy: viewOf(provenanceOfSubtree(resolved, `/${LSP_NAMESPACE_KEY}/servers/${entry.server.id}`) ?? provenanceOfSubtree(resolved, `/servers/${entry.server.id}`), homeDir)
        }
      }
    };
  });
  const available = new Set(await getAvailableRunners(absolute));
  const planned = kind ? getToolPlan(kind)?.groups.flatMap((group) => group.runnerIds) ?? [] : [];
  const ids = [.../* @__PURE__ */ new Set([...planned, ...available])].sort(compareOrdinal);
  const tools = ids.map((id) => ({
    id,
    selected: available.has(id),
    reason: toolReason(available.has(id), kind),
    resolvedCwd: homeRelativePath(resolveToolCwd("runner", id, absolute, {
      cwd: workspaceCwd,
      suppressTelemetry: true
    }).cwd, homeDir)
  }));
  return {
    path: homeRelativePath(absolute, homeDir),
    ...language === void 0 ? {} : { language: language.id },
    ...kind === void 0 ? {} : { kind },
    servers,
    tools
  };
}

// dist/clients/lens-engine.js
function recentLatency(limit = 5, fileFilter) {
  let reports = getLatencyReports();
  if (fileFilter) {
    const needle = fileFilter.replace(/\\/g, "/");
    reports = reports.filter((report) => report.filePath.replace(/\\/g, "/").endsWith(needle));
  }
  return reports.slice(-limit).reverse();
}
function projectScan(cwd, maxFiles, includeGenerated) {
  return scanProjectDiagnostics({
    cwd,
    tier: "cheap",
    maxFiles,
    includeGenerated
  });
}
function scanTruncationNotice(snapshot) {
  if (!snapshot.scanTruncated)
    return void 0;
  if (snapshot.treeSitterStatus === "wasm_aborted_restart_required") {
    return `\u26A0 Scan stopped after ${snapshot.filesScanned} complete file(s): the tree-sitter WASM runtime aborted. Results are partial and were not cached; restart the pi-lens extension/MCP server to restore structural analysis.`;
  }
  return `\u26A0 Scan truncated at ${snapshot.filesScanned} file(s) \u2014 results are partial; raise maxProjectFiles in .pi-lens.json to scan fully.`;
}
function generatedSkipNotice(snapshot, host = "pi") {
  const files = snapshot.generatedNameOnlySkips ?? 0;
  const dirs = snapshot.generatedDirSkips ?? 0;
  if (files === 0 && dirs === 0)
    return void 0;
  const parts = [];
  if (files > 0)
    parts.push(`${files} file(s)`);
  if (dirs > 0)
    parts.push(`${dirs} director${dirs === 1 ? "y" : "ies"}`);
  const targets = [
    resolveLensToolName("project_scan", host),
    resolveLensToolName("lens_diagnostics", host)
  ].filter((tool) => tool !== void 0);
  const scanRef = targets.length > 0 ? ` to ${targets.join(" / ")}` : "";
  return `\u2139 ${parts.join(" and ")} excluded by generated-name heuristics with no confirming evidence \u2014 a real hand-written file/directory whose name looks generated (e.g. \`gen.ts\`, \`generated/\`) can be excluded too; pass includeGenerated: true${scanRef} to scan without this filter if you suspect a false skip.`;
}
function treeSitterRuntimeStatus() {
  return getTreeSitterRuntimeStatus();
}
function lspStatus() {
  const lsp = getLSPService();
  return {
    aliveClients: lsp.getAliveClientCount(),
    servers: lsp.getStatus(),
    brokenServers: lsp.getBrokenStatus()
  };
}
function renderLspBrokenStatusLines(brokenServers) {
  return brokenServers.map((server) => server.permanentlyBroken ? `  \u2717 ${server.serverId} \u2014 disabled after ${server.failures} failures (${server.root})` : `  \u2717 ${server.serverId} \u2014 cooling down after ${server.failures} failure(s) until ${new Date(server.cooldownUntil).toISOString()} (${server.root})`);
}
function diagnosticStats() {
  return getDiagnosticTracker().getStats();
}
function ensureLspConfig(cwd) {
  return initLSPConfig(cwd);
}
function resourceFootprint() {
  return getResourceFootprint();
}
function symbolSearchFileMatchesLang(file, lang) {
  const exts = extensionsForLanguageToken(lang);
  if (exts.length === 0)
    return false;
  return exts.includes(path28.extname(file).toLowerCase());
}
function fileMatchesPathGlobs(file, cwd, globs) {
  const rel = path28.relative(cwd, file).split(path28.sep).join("/");
  const options = { dot: true, nocase: process.platform === "win32" };
  return globs.some((raw) => {
    const pattern = raw.split("\\").join("/");
    return minimatch(rel, pattern, options) || minimatch(rel, `${pattern}/**`, options);
  });
}
function buildSymbolSearchFileFilter(cwd, options) {
  const paths = options.paths?.filter((p) => p.trim().length > 0);
  const lang = options.lang?.trim();
  if (!paths?.length && !lang)
    return void 0;
  return (file) => {
    if (paths?.length && !fileMatchesPathGlobs(file, cwd, paths))
      return false;
    if (lang && !symbolSearchFileMatchesLang(file, lang))
      return false;
    return true;
  };
}
function annotateSymbolSearchHitsWithGraph(hits, centrality, graph) {
  for (const hit of hits) {
    const normalized = normalizeMapKey(path28.resolve(hit.file));
    const fanIn = centrality.get(hit.file) ?? 0;
    let complexity;
    for (const symbolId of graph.symbolNodesByFile.get(normalized) ?? []) {
      const raw = graph.nodes.get(symbolId)?.metadata?.cyclomaticComplexity;
      if (typeof raw === "number" && (complexity === void 0 || raw > complexity)) {
        complexity = raw;
      }
    }
    hit.annotations = {
      fanIn,
      ...complexity !== void 0 ? { complexity } : {}
    };
  }
}
function toSymbolSearchHit(result) {
  const line = result.lines[0] ?? 1;
  return {
    file: result.file,
    score: result.score,
    hits: result.hits,
    startLine: line,
    endLine: line
  };
}
async function symbolSearch(query, cwd, limit = 20, options = {}) {
  const warmLease = acquireWarmWordIndex(cwd);
  const snapshot = loadProjectSnapshotWithoutWordIndex(cwd);
  const index = warmLease.index;
  if (!index) {
    warmLease.release();
    const priorStatus = getWordIndexBuildStatus(cwd);
    const status = priorStatus?.state === "refused" ? priorStatus : triggerBackgroundWordIndexBuild(cwd);
    const unavailableReason = priorStatus?.state === "failed" ? "last-build-failed" : status.state === "refused" ? "refused" : "building";
    const hint = unavailableReason === "refused" ? `Word index build was refused for safety: ${status.state === "refused" ? status.reason : "unsafe workspace root"}. Run symbol_search from inside a project directory.` : unavailableReason === "last-build-failed" ? `The last word index build failed: ${priorStatus?.state === "failed" ? priorStatus.reason : "unknown error"}. A retry is now running in the background.` : "Word index is building in the background for this workspace \u2014 retry this query shortly.";
    return {
      available: false,
      query,
      results: [],
      unavailableReason,
      hint
    };
  }
  let centrality;
  let results;
  try {
    centrality = centralityFromReverseDeps(index, snapshot?.reverseDeps, (file) => normalizeMapKey(path28.resolve(file)));
    const fileFilter = buildSymbolSearchFileFilter(cwd, options);
    results = searchWordIndex(index, query, { limit, centrality, fileFilter });
  } finally {
    warmLease.release();
  }
  const hits = results.map(toSymbolSearchHit);
  const { getCachedReviewGraph } = await import("../chunk-VD4JQCQC.js");
  const graph = getCachedReviewGraph(cwd);
  if (graph)
    annotateSymbolSearchHitsWithGraph(hits, centrality, graph);
  return {
    available: true,
    query,
    results: hits,
    coverage: { files: index.docCount, truncated: index.truncated === true },
    snapshotGeneratedAt: snapshot?.generatedAt
  };
}

// dist/clients/deps/typebox.js
import { Type } from "typebox";

// dist/clients/ast-grep-tool-logger.js
import * as path29 from "node:path";
var AG_LOG_DIR = getGlobalPiLensLogDir();
var AG_LOG_FILE = path29.join(AG_LOG_DIR, "ast-grep-tools.log");
var AG_LOG_BACKUP_FILE = path29.join(AG_LOG_DIR, "ast-grep-tools.log.1");
var MAX_LOG_BYTES3 = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_AST_GREP_LOG_MAX_BYTES ?? "1048576", 10) || 1048576);
var writer3 = createNdjsonLogger({
  filePath: AG_LOG_FILE,
  maxBytes: MAX_LOG_BYTES3,
  backupPath: AG_LOG_BACKUP_FILE
});
var PATTERN_TRUNCATE_AT = 500;
var ERROR_TRUNCATE_AT = 300;
function truncate(value, max) {
  if (value === void 0)
    return void 0;
  if (value.length <= max)
    return value;
  return `${value.slice(0, max)}\u2026[truncated ${value.length - max} chars]`;
}
function classifyAstGrepError(errorMessage) {
  if (!errorMessage)
    return "other";
  const lower = errorMessage.toLowerCase();
  if (lower.includes("multiple ast nodes are detected") || lower.includes("the pattern appears to contain multiple ast nodes")) {
    return "multiple_ast_nodes";
  }
  if (lower.includes("cannot parse query") || lower.includes("pattern syntax error") || lower.includes("could not be parsed as valid code")) {
    return "cannot_parse_query";
  }
  if (lower.includes("cli not found") || lower.includes("enoent") || lower.includes("not installed")) {
    return "tool_not_found";
  }
  if (lower.includes("timed out") || lower.includes("timeout")) {
    return "timeout";
  }
  if (lower.includes("failed to parse output")) {
    return "json_parse_failed";
  }
  return "other";
}
function astGrepRemediationHint(kind) {
  switch (kind) {
    case "multiple_ast_nodes":
    case "cannot_parse_query":
      return null;
    case "tool_not_found":
      return "Hint: the ast-grep CLI is missing or not on PATH \u2014 install it with `npm i -D @ast-grep/cli`.";
    case "timeout":
      return "Hint: the operation timed out \u2014 scope `paths` to specific files/folders, or narrow the pattern.";
    case "json_parse_failed":
      return "Hint: ast-grep produced output that could not be parsed \u2014 retry with a simpler pattern; the installed CLI version may be incompatible.";
    default:
      return "Hint: verify the pattern is a single valid AST node for this `lang` (use ast_grep_search with dump=true to discover node kinds), or fall back to grep for plain-text search.";
  }
}
function logAstGrepToolEvent(event) {
  if (isTestMode())
    return;
  const payload = {
    ...event,
    pattern: truncate(event.pattern, PATTERN_TRUNCATE_AT) ?? "",
    patternLineCount: event.patternLineCount,
    rewrite: truncate(event.rewrite, PATTERN_TRUNCATE_AT),
    rewriteLineCount: event.rewriteLineCount,
    errorRaw: truncate(event.errorRaw, ERROR_TRUNCATE_AT)
  };
  writer3.log({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...payload });
}

// dist/clients/ast-grep-yaml-synth.js
var MAX_SYNTHESIZED_PATTERN_CHARS = 4e3;
var MAX_NODE_KIND_CHARS = 80;
var NODE_KIND_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
function hasStructuralIntent(intent) {
  return !!(intent.nodeKind || intent.insideKind || intent.hasKind || intent.hasDescendantKind || intent.follows || intent.precedes);
}
function synthesizeReplaceRule(intent) {
  const base = synthesizeRule(intent);
  return `${base}fix: ${JSON.stringify(intent.rewrite)}
`;
}
function synthesizeRule(intent) {
  const hasPattern = !!intent.pattern?.trim();
  const hasNodeKind = !!intent.nodeKind?.trim();
  if (hasPattern === hasNodeKind) {
    throw new Error("provide exactly one of pattern or nodeKind");
  }
  const pattern = intent.pattern;
  if (hasPattern && pattern)
    assertSafePattern(pattern, "pattern");
  if (intent.hasKind?.trim() && intent.hasDescendantKind?.trim()) {
    throw new Error("hasKind and hasDescendantKind are mutually exclusive");
  }
  const language = canonicalLanguage(intent.lang);
  const rule = hasNodeKind ? { kind: assertSafeNodeKind(intent.nodeKind ?? "", "nodeKind") } : { pattern };
  if (intent.insideKind) {
    rule.inside = {
      kind: assertSafeNodeKind(intent.insideKind, "insideKind"),
      stopBy: "end"
    };
  }
  if (intent.hasKind) {
    rule.has = { kind: assertSafeNodeKind(intent.hasKind, "hasKind") };
  }
  if (intent.hasDescendantKind) {
    rule.has = {
      kind: assertSafeNodeKind(intent.hasDescendantKind, "hasDescendantKind"),
      stopBy: "end"
    };
  }
  if (intent.follows) {
    assertSafePattern(intent.follows, "follows");
    rule.follows = { pattern: intent.follows };
  }
  if (intent.precedes) {
    assertSafePattern(intent.precedes, "precedes");
    rule.precedes = { pattern: intent.precedes };
  }
  const doc = {
    id: "agent-rule",
    language,
    rule
  };
  return dump(doc, { lineWidth: -1 });
}
function assertSafePattern(value, field) {
  if (!value.trim()) {
    throw new Error(`${field} is required for YAML synthesis`);
  }
  if (value.length > MAX_SYNTHESIZED_PATTERN_CHARS) {
    throw new Error(`${field} is too long for YAML synthesis`);
  }
  if (value.includes("\0")) {
    throw new Error(`${field} contains a NUL byte`);
  }
}
function assertSafeNodeKind(value, field) {
  const kind = value.trim();
  if (kind.length === 0) {
    throw new Error(`${field} is required for YAML synthesis`);
  }
  if (kind.length > MAX_NODE_KIND_CHARS || !NODE_KIND_RE.test(kind)) {
    throw new Error(`${field} must be a single AST node kind like function_declaration`);
  }
  return kind;
}
function canonicalLanguage(lang) {
  const map = {
    typescript: "TypeScript",
    tsx: "Tsx",
    javascript: "JavaScript",
    jsx: "JavaScript",
    python: "Python",
    rust: "Rust",
    go: "Go",
    java: "Java",
    kotlin: "Kotlin",
    swift: "Swift",
    csharp: "CSharp",
    cpp: "Cpp",
    c: "C",
    ruby: "Ruby",
    php: "Php",
    dart: "Dart",
    elixir: "Elixir",
    lua: "Lua",
    ocaml: "OCaml",
    zig: "Zig",
    bash: "Bash",
    css: "Css",
    html: "Html",
    json: "Json",
    yaml: "Yaml",
    toml: "Toml",
    vue: "Vue"
  };
  return map[lang.toLowerCase()] ?? lang;
}

// dist/tools/shared.js
var LANGUAGES = [
  "bash",
  "c",
  "cpp",
  "csharp",
  "css",
  "elixir",
  "go",
  "haskell",
  "html",
  "java",
  "javascript",
  "json",
  "kotlin",
  "lua",
  "nix",
  "php",
  "python",
  "ruby",
  "rust",
  "scala",
  "swift",
  "tsx",
  "typescript",
  "yaml"
];

// dist/tools/ast-grep-replace.js
function errorTextWithHint(raw) {
  const hint = astGrepRemediationHint(classifyAstGrepError(raw));
  return hint ? `Error: ${raw}

${hint}` : `Error: ${raw}`;
}
function lineCount(value) {
  if (!value)
    return 0;
  let lines = 1;
  for (let i = 0; i < value.length; i++) {
    if (value.charCodeAt(i) === 10)
      lines++;
  }
  return lines;
}
function createAstGrepReplaceTool(astGrepClient2, captureLineage) {
  return {
    name: "ast_grep_replace",
    label: "AST Replace",
    description: 'Find and rewrite code by AST structure; preview by default. Example: `{pattern: "var $X", rewrite: "let $X", apply: false}`.',
    promptSnippet: "Preview an AST-aware code rewrite",
    renderResult: compactRenderResult(({ details, isError, text }) => {
      if (details?.stalePreview) {
        return `ast_grep_replace \u2014 stale preview, re-run with apply:false`;
      }
      if (isError) {
        return `ast_grep_replace \u2014 ${text.split("\n")[0] ?? "error"}`;
      }
      const n = details?.matchCount ?? 0;
      const state = details?.applied ? "applied" : "preview";
      return `ast_grep_replace \u2014 ${n} match${n === 1 ? "" : "es"}, ${state}`;
    }),
    parameters: Type.Object({
      pattern: Type.String({
        description: "AST pattern to match (be specific with context)"
      }),
      rewrite: Type.String({
        description: "Replacement using meta-variables from pattern"
      }),
      lang: Type.String({
        enum: [...LANGUAGES],
        description: "Target language"
      }),
      paths: Type.Optional(Type.Array(Type.String(), { description: "Specific files/folders" })),
      insideKind: Type.Optional(Type.String({
        description: "Restrict matches to nodes inside an ancestor of this AST node kind. Synthesizes a YAML rule."
      })),
      hasKind: Type.Optional(Type.String({
        description: 'Restrict matches to nodes whose immediate child has this AST node kind (ast-grep default stopBy: neighbor). Example: `hasKind: "await_expression"`.'
      })),
      hasDescendantKind: Type.Optional(Type.String({
        description: "Restrict matches to nodes containing this AST node kind anywhere in their descendants. Explicit recursive form (`stopBy: end`); use this instead of `hasKind` when nesting is not immediate."
      })),
      follows: Type.Optional(Type.String({
        description: "Restrict matches to nodes that immediately follow a sibling matching this pattern."
      })),
      precedes: Type.Optional(Type.String({
        description: "Restrict matches to nodes that immediately precede a sibling matching this pattern."
      })),
      apply: Type.Optional(Type.Boolean({ description: "Apply changes (default: false)" })),
      strictness: Type.Optional(Type.String({
        enum: ["smart", "relaxed", "ast", "cst", "signature", "template"],
        description: "Pattern matching strictness. 'smart' (default) ignores comments and whitespace. 'relaxed' also ignores unnamed nodes like punctuation. 'ast' ignores all whitespace."
      }))
    }),
    async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
      const lineage = captureLineage?.();
      const startedAt = Date.now();
      const { pattern, rewrite, paths, apply, strictness, insideKind, hasKind, hasDescendantKind, follows, precedes } = params;
      const lang = (params.lang ?? "").replace(/^"|"$/g, "");
      const pathsCount = paths?.length ?? 1;
      const applyFlag = apply ?? false;
      const resultMaxItems = ctx.resultMaxItems ?? 50;
      function logOutcome(outcome, details = {}) {
        try {
          logAstGrepToolEvent({
            tool: "ast_grep_replace",
            lang,
            pattern,
            patternLineCount: lineCount(pattern),
            rewrite,
            rewriteLineCount: lineCount(rewrite ?? ""),
            pathsCount,
            applied: applyFlag,
            outcome,
            errorKind: outcome === "error" ? classifyAstGrepError(details.errorRaw) : void 0,
            errorRaw: details.errorRaw,
            matchCount: details.matchCount ?? 0,
            truncated: details.truncated ?? false,
            durationMs: Date.now() - startedAt
          });
        } catch {
        }
      }
      if (!await astGrepClient2.ensureAvailable()) {
        logOutcome("error", { errorRaw: "ast-grep CLI not found" });
        return {
          content: [
            {
              type: "text",
              text: "ast-grep CLI not found. Install: npm i -D @ast-grep/cli"
            }
          ],
          isError: true,
          details: {}
        };
      }
      const searchPaths = paths?.length ? paths : [ctx.cwd || "."];
      if (hasStructuralIntent({
        insideKind,
        hasKind,
        hasDescendantKind,
        follows,
        precedes
      })) {
        let ruleYaml;
        try {
          ruleYaml = synthesizeReplaceRule({
            pattern,
            lang,
            rewrite,
            insideKind,
            hasKind,
            hasDescendantKind,
            follows,
            precedes
          });
        } catch (err) {
          logOutcome("error", { errorRaw: String(err) });
          return {
            content: [
              {
                type: "text",
                text: `Error synthesizing rule: ${err}`
              }
            ],
            isError: true,
            details: {}
          };
        }
        const ruleResult = await astGrepClient2.replaceWithRule(ruleYaml, searchPaths, applyFlag);
        if (ruleResult.stalePreview) {
          logOutcome("error", { errorRaw: "stale_preview" });
          return {
            content: [
              {
                type: "text",
                text: "Stale preview \u2014 pattern no longer matches. Re-run with apply: false."
              }
            ],
            isError: true,
            details: { stalePreview: true }
          };
        }
        if (ruleResult.error) {
          logOutcome("error", { errorRaw: ruleResult.error });
          return {
            content: [
              {
                type: "text",
                text: errorTextWithHint(ruleResult.error)
              }
            ],
            isError: true,
            details: {}
          };
        }
        const output2 = astGrepClient2.formatMatches(ruleResult.matches, !applyFlag, true, resultMaxItems);
        logOutcome(ruleResult.matches.length === 0 ? "no_matches" : "success", {
          matchCount: ruleResult.matches.length
        });
        return {
          content: [{ type: "text", text: output2 }],
          details: {
            matchCount: ruleResult.matches.length,
            applied: applyFlag
          }
        };
      }
      const result = await astGrepClient2.replace(pattern, rewrite, lang, searchPaths, applyFlag, { strictness, lineage });
      if (result.stalePreview) {
        logOutcome("error", { errorRaw: "stale_preview" });
        return {
          content: [
            {
              type: "text",
              text: "Stale preview \u2014 the pattern no longer matches any files. The file content has changed since your last apply: false preview. Re-run with apply: false to get a fresh preview before applying."
            }
          ],
          isError: true,
          details: { stalePreview: true }
        };
      }
      if (result.error) {
        logOutcome("error", { errorRaw: result.error });
        return {
          content: [
            { type: "text", text: errorTextWithHint(result.error) }
          ],
          isError: true,
          details: {}
        };
      }
      const isDryRun = !applyFlag;
      const output = astGrepClient2.formatMatches(
        result.matches,
        isDryRun,
        true,
        // showModeIndicator
        resultMaxItems
      );
      logOutcome(result.matches.length === 0 ? "no_matches" : "success", {
        matchCount: result.matches.length,
        truncated: result.truncated
      });
      return {
        content: [{ type: "text", text: output }],
        details: {
          matchCount: result.matches.length,
          totalMatches: result.totalMatches,
          truncated: result.truncated,
          applied: applyFlag
        }
      };
    }
  };
}

// dist/tools/ast-grep-search.js
function errorTextWithHint2(raw) {
  const hint = astGrepRemediationHint(classifyAstGrepError(raw));
  return hint ? `Error: ${raw}

${hint}` : `Error: ${raw}`;
}
function _telemetryErrorForTest(raw) {
  if (!raw)
    return void 0;
  return raw.replace(/\0/g, "\\0").slice(0, 2e3);
}
function _telemetryClassificationErrorForTest(raw) {
  return raw?.replace(/\0/g, "");
}
function toSearchReads(matches) {
  const out = [];
  for (const m of matches) {
    const span = toLineSpan(m);
    if (!span)
      continue;
    out.push({
      file: span.file,
      startLine: span.startLine,
      endLine: span.endLine
    });
  }
  return out;
}
var DEFAULT_PAGE_SIZE = 50;
var MAX_PAGE_SIZE = 200;
var MAX_PATHS = 200;
var AST_GREP_SEARCH_TIMEOUT_MS = 3e4;
function formatGroupedByFile(matches) {
  const byFile = /* @__PURE__ */ new Map();
  for (const m of matches) {
    if (!m.file)
      continue;
    const list = byFile.get(m.file);
    if (list)
      list.push(m);
    else
      byFile.set(m.file, [m]);
  }
  const total = matches.length;
  const fileCount = byFile.size;
  const lines = [
    `${fileCount} file${fileCount === 1 ? "" : "s"}, ${total} match${total === 1 ? "" : "es"}:`
  ];
  for (const [file, group] of byFile) {
    const locs = group.map((m) => {
      const line = (m.range?.start?.line ?? 0) + 1;
      const col = (m.range?.start?.column ?? 0) + 1;
      return `L${line}:${col}`;
    }).join(", ");
    lines.push(`${file} (${group.length}): ${locs}`);
  }
  return lines.join("\n");
}
var DEFAULT_READ_SLICE_MARGIN = 3;
var MAX_READ_SLICE_MARGIN = 20;
var MAX_RAW_RULE_CHARS = 1e5;
function rawRuleValidationError(rule) {
  if (rule === void 0)
    return null;
  if (rule.includes("\0"))
    return "rule contains a NUL byte";
  if (rule.length > MAX_RAW_RULE_CHARS)
    return "rule is too long";
  return null;
}
function patternValidationError(pattern) {
  if (pattern.includes("\0"))
    return "pattern contains a NUL byte";
  if (pattern.length > 4e3)
    return "pattern is too long";
  return null;
}
function toLineSpan(match) {
  const start = match.range?.start?.line;
  if (!match.file || typeof start !== "number")
    return null;
  const end = match.range?.end?.line;
  return {
    file: match.file,
    startLine: start + 1,
    endLine: (typeof end === "number" ? end : start) + 1
  };
}
function toMatchLocations(matches, contextLines) {
  const margin = typeof contextLines === "number" && Number.isFinite(contextLines) ? Math.min(MAX_READ_SLICE_MARGIN, Math.max(0, Math.floor(contextLines))) : DEFAULT_READ_SLICE_MARGIN;
  const out = [];
  for (const match of matches) {
    const span = toLineSpan(match);
    if (!span)
      continue;
    const offset = Math.max(1, span.startLine - margin);
    out.push({
      file: span.file,
      line: span.startLine,
      endLine: span.endLine,
      readSlice: {
        path: span.file,
        offset,
        limit: span.endLine - offset + 1 + margin
      }
    });
  }
  return out;
}
function suggestedDump(lang) {
  return {
    tool: "ast_grep_search",
    mode: "dump",
    lang,
    note: "Run ast_grep_search with dump=true on a small representative source snippet (not a whole file) to inspect AST node kinds before retrying the search."
  };
}
function lineCount2(value) {
  if (!value)
    return 0;
  let lines = 1;
  for (let i = 0; i < value.length; i++) {
    if (value.charCodeAt(i) === 10)
      lines++;
  }
  return lines;
}
function looksLikeRuleYamlOrPlainText(pattern) {
  const text = pattern.trim();
  if (!text)
    return true;
  const lower = text.toLowerCase();
  if (/(^|\n)\s*(id|language|rule|rules|kind|pattern|message|severity)\s*:/.test(lower)) {
    return true;
  }
  if (/\b(id|language|rule|rules|kind|pattern|message|severity)\s*:\s*[a-z0-9_-]+/i.test(text)) {
    return true;
  }
  if (/^[-*]\s+/.test(text))
    return true;
  const hasAstSignals = /[$(){}[\].;:'"`]/.test(text);
  const hasWhitespace = /\s/.test(text);
  if (hasWhitespace && !hasAstSignals)
    return true;
  return false;
}
function getPatternHint(pattern, lang, selector) {
  const src = pattern.trim();
  if (selector) {
    return `Hint: selector=${JSON.stringify(selector)} narrows the AST node kind searched; it does not extract fields from matches. Retry once without selector, or use a selector that is the outer node kind you want to match.`;
  }
  if (/\\[wWdDsSbB]/.test(src)) {
    return 'Hint: "\\w", "\\d", "\\s", "\\b" are regex escapes. ast-grep matches AST nodes, not text \u2014 use $VAR for identifiers, $$$ for node lists, or switch to grep for text search.';
  }
  if (/\[[a-zA-Z0-9]-[a-zA-Z0-9]\]/.test(src)) {
    return 'Hint: "[a-z]" and similar character classes are regex, not AST. Use $VAR to match any identifier, or switch to grep for text search.';
  }
  if (!src.includes("$") && /\w\.[*+]/.test(src)) {
    return 'Hint: ".*" and ".+" are regex wildcards. In ast-grep use $$$ for multiple AST nodes and $VAR for a single node. For text patterns, switch to grep.';
  }
  if (/^[-\w.*]+\|[-\w.*|]+$/.test(src)) {
    return 'Hint: "|" is regex alternation and does NOT work in ast-grep patterns. Options: (a) fire one ast_grep_search per alternative, or (b) switch to grep with a regex pattern like "foo|bar".';
  }
  if (lang === "python") {
    if ((src.startsWith("def ") || src.startsWith("async def ")) && src.endsWith(":")) {
      return `Hint: Remove trailing colon from Python patterns. Try: "${src.slice(0, -1)}"`;
    }
    if (src.startsWith("class ") && src.endsWith(":")) {
      return `Hint: Remove trailing colon from class patterns. Try: "${src.slice(0, -1)}"`;
    }
  }
  if (["javascript", "typescript", "tsx"].includes(lang)) {
    if (/^(export\s+)?(async\s+)?function\s+\$[A-Z_]+\s*$/i.test(src)) {
      return 'Hint: Function patterns need params and body. Try "function $NAME($$$) { $$$ }"';
    }
  }
  if (lang === "go") {
    if (/^func\s+\$[A-Z_]+\s*$/i.test(src)) {
      return 'Hint: Go function patterns need params and body. Try "func $NAME($$$) { $$$ }"';
    }
  }
  if (lang === "rust") {
    if (/^fn\s+\$[A-Z_]+\s*$/i.test(src)) {
      return 'Hint: Rust fn patterns need params and body. Try "fn $NAME($$$) { $$$ }"';
    }
  }
  return "Hint: No matches. Retry once with a smaller valid AST pattern scoped to the same paths (for example a call like `foo($$$ARGS)`, an import statement, or `function $NAME($$$ARGS) { $$$BODY }`). If you're actually looking for a name/usage rather than a structural pattern, prefer symbol_search (ranked identifier search) or module_report (file outline) over another AST retry; lsp_navigation findReferences finds exact call sites once you have a definition. If that also fails, use grep for text search, or ast_grep_search with dump=true on a small representative snippet to inspect node kinds.";
}
function astGrepDumpCompatibilityResult(params, surface) {
  return {
    content: [
      {
        type: "text",
        text: `The ast_grep_dump tool was retired. Call ${surface === "mcp" ? "pilens_" : ""}ast_grep_search with dump=true and put the snippet in pattern instead.`
      }
    ],
    isError: true,
    details: {
      compatibility: "ast_grep_dump -> ast_grep_search",
      call: {
        tool: surface === "mcp" ? "pilens_ast_grep_search" : "ast_grep_search",
        arguments: {
          dump: true,
          pattern: params.source,
          lang: params.lang
        }
      }
    }
  };
}
function createAstGrepSearchTool(astGrepClient2) {
  return {
    name: "ast_grep_search",
    label: "AST Search",
    description: "Search source by AST structure rather than text. Example: find calls with `console.log($MSG)`.",
    promptSnippet: "Search source by AST structure",
    renderResult: compactRenderResult(({ details, isError, text }) => {
      if (details?.validateOnly) {
        const mode = details.mode ?? "pattern";
        return details.valid ? `ast_grep_search \u2014 valid ${mode}` : `ast_grep_search \u2014 invalid ${mode}`;
      }
      if (isError) {
        return `ast_grep_search \u2014 ${text.split("\n")[0] ?? "error"}`;
      }
      const count = details?.matchCount ?? 0;
      const total = details?.totalMatches;
      const ofTotal = typeof total === "number" && total > count ? ` of ${total}` : "";
      const applied = details?.applied ? " (applied)" : "";
      return `ast_grep_search \u2014 ${count}${ofTotal} match${count === 1 && !ofTotal ? "" : "es"}${applied}`;
    }),
    parameters: Type.Object({
      dump: Type.Optional(Type.Boolean()),
      pattern: Type.Optional(Type.String({ description: "AST pattern." })),
      lang: Type.String({
        enum: [...LANGUAGES],
        description: "Target language"
      }),
      paths: Type.Optional(Type.Array(Type.String(), {
        maxItems: MAX_PATHS,
        description: "Files or directories to search."
      })),
      selector: Type.Optional(Type.String({
        description: "AST node kind filter."
      })),
      context: Type.Optional(Type.Number({
        description: "Context lines around matches."
      })),
      nodeKind: Type.Optional(Type.String({
        description: "AST node kind without a pattern."
      })),
      insideKind: Type.Optional(Type.String({
        description: "Ancestor AST node kind filter."
      })),
      hasKind: Type.Optional(Type.String({
        description: "Immediate-child AST node kind filter."
      })),
      hasDescendantKind: Type.Optional(Type.String({
        description: "Recursive descendant AST node kind filter."
      })),
      follows: Type.Optional(Type.String({
        description: "Preceding sibling pattern filter."
      })),
      precedes: Type.Optional(Type.String({
        description: "Following sibling pattern filter."
      })),
      rule: Type.Optional(Type.String({
        description: "Raw ast-grep YAML rule."
      })),
      skip: Type.Optional(Type.Number({
        description: "Matches to skip for pagination."
      })),
      maxMatches: Type.Optional(Type.Number({
        description: "Maximum matches to return."
      })),
      groupByFile: Type.Optional(Type.Boolean({
        description: "Group matches by file."
      })),
      strictness: Type.Optional(Type.String({
        enum: ["smart", "relaxed", "ast", "cst", "signature", "template"],
        description: "Pattern matching strictness."
      })),
      validateOnly: Type.Optional(Type.Boolean({
        description: "Validate without scanning."
      }))
    }),
    async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
      const abortSignal = combineAbortSignals(_signal, ctx.signal);
      const startedAt = Date.now();
      const deadlineAt = startedAt + AST_GREP_SEARCH_TIMEOUT_MS;
      const { paths, nodeKind, selector, context, skip, maxMatches, groupByFile, strictness, rule, insideKind, hasKind, hasDescendantKind, follows, precedes, validateOnly, dump: dump2 } = params;
      const pattern = typeof params.pattern === "string" ? params.pattern : "";
      const rawLang = typeof params.lang === "string" ? params.lang : "";
      const normalizedNodeKind = typeof nodeKind === "string" ? nodeKind.trim() : "";
      const skipOffset = Math.max(0, Math.floor(skip ?? 0));
      const lang = rawLang.replace(/^"|"$/g, "");
      const searchPathsCount = paths?.length ?? 1;
      const executionOptions = { signal: abortSignal, deadlineAt };
      const dumpMode = dump2 === true;
      function logOutcome(outcome, details = {}) {
        try {
          const errorRaw = _telemetryErrorForTest(details.errorRaw);
          const classificationError = _telemetryClassificationErrorForTest(details.errorRaw);
          logAstGrepToolEvent({
            tool: "ast_grep_search",
            lang,
            pattern,
            patternLineCount: lineCount2(pattern),
            pathsCount: searchPathsCount,
            outcome,
            errorKind: outcome === "error" ? classifyAstGrepError(classificationError) : void 0,
            errorRaw,
            matchCount: details.matchCount ?? 0,
            truncated: details.truncated ?? false,
            durationMs: Date.now() - startedAt
          });
        } catch (err) {
          try {
            process.emitWarning(`ast_grep_search telemetry failed: ${err}`, {
              code: "PI_LENS_AST_GREP_SEARCH_TELEMETRY_FAILED"
            });
          } catch {
            void err;
          }
        }
      }
      function abortError() {
        logOutcome("error", { errorRaw: "operation aborted" });
        return {
          content: [
            { type: "text", text: "Error: operation aborted" }
          ],
          isError: true,
          details: {}
        };
      }
      try {
        const rawRule = typeof rule === "string" ? rule : void 0;
        const hasRawRule = Boolean(rawRule?.trim());
        const hasPattern = Boolean(pattern.trim());
        const hasNodeKind = normalizedNodeKind.length > 0;
        const rawPaths = params.paths;
        if (rawPaths !== void 0 && (!Array.isArray(rawPaths) || rawPaths.length > MAX_PATHS || rawPaths.some((value) => typeof value !== "string"))) {
          const errorRaw = `paths must contain at most ${MAX_PATHS} string entries`;
          logOutcome("error", { errorRaw });
          return {
            content: [{ type: "text", text: `Error: ${errorRaw}` }],
            isError: true,
            details: {}
          };
        }
        const rawRuleError = rawRuleValidationError(rawRule);
        if (rawRuleError) {
          logOutcome("error", { errorRaw: rawRuleError });
          return {
            content: [
              { type: "text", text: `Error: ${rawRuleError}` }
            ],
            isError: true,
            details: {}
          };
        }
        if (!dumpMode && !hasPattern && !hasRawRule && !hasNodeKind) {
          logOutcome("error", {
            errorRaw: "pattern is required unless rule or nodeKind is provided"
          });
          return {
            content: [
              {
                type: "text",
                text: "Error: pattern is required unless rule or nodeKind is provided"
              }
            ],
            isError: true,
            details: {}
          };
        }
        const patternError = patternValidationError(pattern);
        if (pattern.trim() && patternError) {
          logOutcome("error", { errorRaw: patternError });
          return {
            content: [
              { type: "text", text: `Error: ${patternError}` }
            ],
            isError: true,
            details: {}
          };
        }
        if (!lang.trim()) {
          logOutcome("error", { errorRaw: "lang is required" });
          return {
            content: [
              { type: "text", text: "Error: lang is required" }
            ],
            isError: true,
            details: {}
          };
        }
        if (abortSignal?.aborted)
          return abortError();
        if (!await astGrepClient2.ensureAvailable()) {
          logOutcome("error", {
            errorRaw: "ast-grep CLI not found"
          });
          return {
            content: [
              {
                type: "text",
                text: "ast-grep CLI not found. Install: npm i -D @ast-grep/cli"
              }
            ],
            isError: true,
            details: {}
          };
        }
        if (abortSignal?.aborted)
          return abortError();
        if (dumpMode) {
          if (!pattern.trim()) {
            const errorRaw = "source is required when dump is true";
            logOutcome("error", { errorRaw });
            return {
              content: [{ type: "text", text: `Error: ${errorRaw}` }],
              isError: true,
              details: { mode: "dump", lang }
            };
          }
          const result2 = await astGrepClient2.dumpAst(pattern, lang);
          if (result2.error) {
            logOutcome("error", { errorRaw: result2.error });
            return {
              content: [
                { type: "text", text: `Error: ${result2.error}` }
              ],
              isError: true,
              details: { mode: "dump", lang }
            };
          }
          const output2 = result2.output ?? "";
          logOutcome("success", { matchCount: lineCount2(output2) });
          return {
            content: [{ type: "text", text: output2 }],
            details: {
              mode: "dump",
              lang,
              matchCount: lineCount2(output2),
              totalMatches: lineCount2(output2),
              truncated: false
            }
          };
        }
        if (!hasRawRule && hasPattern && looksLikeRuleYamlOrPlainText(pattern)) {
          logOutcome("error", {
            errorRaw: "pattern looks like rule YAML or plain text (rejected pre-spawn)"
          });
          return {
            content: [
              {
                type: "text",
                text: "Error: ast_grep_search expects a valid AST code pattern, not plain text/rule YAML. Use patterns like `function $NAME($$$ARGS) { $$$BODY }` or use grep/read for plain text diagnostics."
              }
            ],
            isError: true,
            details: {}
          };
        }
        if (!paths?.length && isAtOrAboveHomeDir(ctx.cwd || ".")) {
          logOutcome("error", {
            errorRaw: "defaulted cwd resolves at or above home directory"
          });
          return {
            content: [
              {
                type: "text",
                text: `Refused: no \`paths\` given and the working directory (${ctx.cwd || "."}) resolves at or above the home directory \u2014 searching it would scan every unrelated tree under your home. Pass explicit \`paths\`, or run from a project directory.`
              }
            ],
            isError: true,
            details: {}
          };
        }
        if (hasNodeKind && hasPattern) {
          const errorRaw = "nodeKind cannot be combined with pattern";
          logOutcome("error", { errorRaw });
          return {
            content: [{ type: "text", text: `Error: ${errorRaw}` }],
            isError: true,
            details: {}
          };
        }
        if (hasNodeKind && hasRawRule) {
          const errorRaw = "nodeKind cannot be combined with rule";
          logOutcome("error", { errorRaw });
          return {
            content: [{ type: "text", text: `Error: ${errorRaw}` }],
            isError: true,
            details: {}
          };
        }
        const searchPaths = paths?.length ? paths : [ctx.cwd || "."];
        const PAGE_SIZE = Math.max(1, Math.min(MAX_PAGE_SIZE, Number.isFinite(maxMatches) ? Math.floor(maxMatches) : DEFAULT_PAGE_SIZE));
        let effectiveRule = hasRawRule ? rawRule : void 0;
        if (!effectiveRule && (hasNodeKind || hasStructuralIntent({
          nodeKind: normalizedNodeKind,
          insideKind,
          hasKind,
          hasDescendantKind,
          follows,
          precedes
        }))) {
          try {
            effectiveRule = synthesizeRule({
              pattern: hasPattern ? pattern : void 0,
              nodeKind: hasNodeKind ? normalizedNodeKind : void 0,
              lang,
              insideKind,
              hasKind,
              hasDescendantKind,
              follows,
              precedes
            });
          } catch (err) {
            logOutcome("error", { errorRaw: String(err) });
            return {
              content: [
                {
                  type: "text",
                  text: `Error synthesizing rule: ${err}`
                }
              ],
              isError: true,
              details: {}
            };
          }
        }
        if (validateOnly) {
          const validation = effectiveRule?.trim() ? await astGrepClient2.validateRule(effectiveRule, executionOptions) : await astGrepClient2.validatePattern(pattern, lang, {
            selector,
            strictness,
            ...executionOptions
          });
          if (!validation.valid) {
            logOutcome("error", { errorRaw: validation.error });
            return {
              content: [
                {
                  type: "text",
                  text: `Invalid ast-grep ${effectiveRule ? "rule" : "pattern"}: ${validation.error ?? "unknown error"}`
                }
              ],
              isError: true,
              details: { valid: false, validateOnly: true }
            };
          }
          logOutcome("success", { matchCount: 0 });
          const warning = "warning" in validation ? validation.warning : void 0;
          return {
            content: [
              {
                type: "text",
                text: `Valid ast-grep ${effectiveRule ? "rule" : "pattern"}.${warning ? ` Warning: ${warning}` : ""}`
              }
            ],
            details: {
              valid: true,
              validateOnly: true,
              mode: effectiveRule ? "rule" : "pattern",
              ...warning ? { warning } : {}
            }
          };
        }
        if (effectiveRule && effectiveRule.trim().length > 0) {
          if (abortSignal?.aborted)
            return abortError();
          const ruleResult = await astGrepClient2.searchWithRule(effectiveRule, searchPaths, executionOptions);
          if (abortSignal?.aborted)
            return abortError();
          if (ruleResult.error) {
            logOutcome("error", { errorRaw: ruleResult.error });
            return {
              content: [
                {
                  type: "text",
                  text: errorTextWithHint2(ruleResult.error)
                }
              ],
              isError: true,
              details: {}
            };
          }
          const afterSkip2 = ruleResult.matches.slice(skipOffset);
          const page2 = afterSkip2.slice(0, PAGE_SIZE);
          const hasMore2 = afterSkip2.length > PAGE_SIZE;
          const output2 = groupByFile ? formatGroupedByFile(page2) : astGrepClient2.formatMatches(page2, false, false, PAGE_SIZE);
          const paginationNote2 = hasMore2 && page2.length > 0 ? `

(Showing ${page2.length} of ${ruleResult.matches.length - skipOffset} remaining matches. Use skip=${skipOffset + PAGE_SIZE} for the next page.)` : "";
          logOutcome(page2.length === 0 ? "no_matches" : "success", {
            matchCount: page2.length,
            truncated: hasMore2
          });
          const matchLocations2 = toMatchLocations(page2, context);
          return {
            content: [
              { type: "text", text: `${output2}${paginationNote2}` }
            ],
            details: {
              matchCount: page2.length,
              totalMatches: ruleResult.totalMatches,
              truncated: hasMore2,
              hasMore: hasMore2,
              skip: skipOffset,
              groupByFile: groupByFile === true,
              // Lines shown to the agent — the read-guard registers these so a
              // follow-up edit to a match isn't blocked (#169). 1-based.
              searchReads: toSearchReads(page2),
              // Agent-facing follow-up handles for bounded context reads.
              matchLocations: matchLocations2,
              suggestedDump: page2.length === 0 ? suggestedDump(lang) : void 0
            }
          };
        }
        if (abortSignal?.aborted)
          return abortError();
        const result = await astGrepClient2.search(pattern, lang, searchPaths, {
          selector,
          context,
          strictness,
          ...executionOptions
        });
        if (abortSignal?.aborted)
          return abortError();
        if (result.error) {
          logOutcome("error", { errorRaw: result.error });
          return {
            content: [
              { type: "text", text: errorTextWithHint2(result.error) }
            ],
            isError: true,
            details: {}
          };
        }
        const afterSkip = result.matches.slice(skipOffset);
        const page = afterSkip.slice(0, PAGE_SIZE);
        const hasMore = afterSkip.length > PAGE_SIZE || result.truncated;
        const output = groupByFile ? formatGroupedByFile(page) : astGrepClient2.formatMatches(page, false, false, PAGE_SIZE);
        const hint = page.length === 0 && !result.error ? getPatternHint(pattern, lang, selector) : void 0;
        const paginationNote = hasMore && page.length > 0 ? `

(Showing ${page.length} of ${result.matches.length - skipOffset} remaining matches. Use skip=${skipOffset + PAGE_SIZE} for the next page.)` : "";
        const finalOutput = hint ? `${output}

${hint}` : `${output}${paginationNote}`;
        logOutcome(page.length === 0 ? "no_matches" : "success", {
          matchCount: page.length,
          truncated: hasMore
        });
        const matchLocations = toMatchLocations(page, context);
        return {
          content: [{ type: "text", text: finalOutput }],
          details: {
            matchCount: page.length,
            totalMatches: result.matches.length,
            truncated: hasMore,
            hasMore,
            skip: skipOffset,
            groupByFile: groupByFile === true,
            // Lines shown to the agent — registered as reads by the read-guard
            // so a follow-up edit to a match isn't blocked (#169). 1-based.
            searchReads: toSearchReads(page),
            // Agent-facing follow-up handles for bounded context reads.
            matchLocations,
            suggestedDump: page.length === 0 ? suggestedDump(lang) : void 0
          }
        };
      } catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        logOutcome("error", { errorRaw: message });
        return {
          content: [{ type: "text", text: `Error: ${message}` }],
          isError: true,
          details: {}
        };
      }
    }
  };
}

// dist/tools/lens-diagnostics.js
import { promises as fs23 } from "node:fs";
import * as fsSync from "node:fs";
import * as path36 from "node:path";

// dist/clients/project-diagnostics/extractors.js
var WARM_TRIGGER = {
  knip: "runs at session-start",
  jscpd: "runs at session-start",
  madge: "runs at session-start",
  gitleaks: "runs at session-start (config opt-in), or on any git repo via mode=full (#608)",
  govulncheck: "runs at session-start (Go projects only)",
  opengrep: "runs at session-start",
  trivy: "runs at session-start",
  "dead-code": "runs at session-start (Python projects only)",
  "test-runner": "fires per-edit at turn_end (only after a source file with a discoverable test companion is edited)"
};
function warmTriggerFor(analyzerId) {
  return WARM_TRIGGER[analyzerId] ?? "runs at session-start";
}
function formatNotRunEntry(analyzerId, coldReasons) {
  const reason = coldReasons?.[analyzerId];
  return reason ? `${analyzerId} \u2014 not run (${reason})` : `${analyzerId} \u2014 not run (${warmTriggerFor(analyzerId)})`;
}
function formatCacheAge(ms) {
  if (!Number.isFinite(ms))
    return "age unknown";
  if (ms < 0)
    return "0m";
  const totalMinutes = Math.round(ms / 6e4);
  if (totalMinutes < 1)
    return "under 1m";
  if (totalMinutes < 60)
    return `${totalMinutes}m`;
  const hours = Math.floor(totalMinutes / 60);
  const minutes = totalMinutes % 60;
  return minutes > 0 ? `${hours}h${minutes}m` : `${hours}h`;
}
function formatCacheAgeOld(ms) {
  const age = formatCacheAge(ms);
  return Number.isFinite(ms) ? `${age} old` : age;
}
function reasonFromAvailabilityVerdict(tool, verdict) {
  if (!verdict || verdict.outcome === null)
    return `${tool} binary unavailable`;
  const { outcome, cause } = verdict;
  if (outcome === "transient") {
    if (cause === "install-retry-exhausted") {
      return `${tool} install kept timing out and hit its retry ceiling this session (not reported missing)`;
    }
    const remainingMs = verdict.retryAtMs - Date.now();
    const remainingS = remainingMs > 0 ? Math.round(remainingMs / 1e3) : 0;
    return `${tool} availability probe \u2014 retry cooldown (${remainingS}s), not a missing install`;
  }
  if (outcome === "non-installable") {
    return `${tool} unavailable on this host (non-installable)`;
  }
  return `${tool} binary unavailable`;
}

// dist/clients/project-diagnostics/fresh-fetch.js
import * as fs21 from "node:fs";
import * as os4 from "node:os";
import * as path34 from "node:path";

// dist/clients/project-diagnostics/runner-adapters/jscpd.js
import * as path30 from "node:path";
function resolveClonePath(cwd, file) {
  return path30.isAbsolute(file) ? file : path30.resolve(cwd, file);
}
function jscpdCloneToProjectDiagnostics(cwd, clone) {
  const a = resolveClonePath(cwd, clone.fileA);
  const b = resolveClonePath(cwd, clone.fileB);
  const make = (filePath, line, otherFile, otherLine) => ({
    filePath,
    line,
    severity: "warning",
    semantic: "warning",
    tool: "jscpd",
    runner: "jscpd",
    rule: "jscpd:duplicate",
    message: `Duplicate code (${clone.lines} lines) \u2014 also at ${path30.relative(cwd, otherFile)}:${otherLine}`,
    source: "project-scan"
  });
  return [
    make(a, clone.startA, b, clone.startB),
    make(b, clone.startB, a, clone.startA)
  ];
}
function jscpdResultToProjectDiagnostics(cwd, result) {
  if (!result.success || result.clones.length === 0)
    return [];
  return result.clones.flatMap((clone) => jscpdCloneToProjectDiagnostics(cwd, clone));
}

// dist/clients/project-diagnostics/runner-adapters/madge.js
import * as path31 from "node:path";
function resolveCyclePath(cwd, file) {
  return path31.isAbsolute(file) ? file : path31.resolve(cwd, file);
}
function cycleMembers(dep) {
  return dep.path.length > 0 ? dep.path : [dep.file];
}
function circularDepToProjectDiagnostics(cwd, dep) {
  const members = cycleMembers(dep);
  const rendered = `${members.map((f) => path31.basename(f)).join(" \u2192 ")} \u2192 ${path31.basename(members[0] ?? "")}`;
  return members.map((file) => ({
    filePath: resolveCyclePath(cwd, file),
    severity: "warning",
    semantic: "warning",
    tool: "madge",
    runner: "madge",
    rule: "madge:circular",
    message: `Part of circular dependency: ${rendered}`,
    source: "project-scan"
  }));
}
function circularDepsToProjectDiagnostics(cwd, circular) {
  const seen = /* @__PURE__ */ new Set();
  const out = [];
  for (const dep of circular) {
    const key = cycleMembers(dep).map((f) => resolveCyclePath(cwd, f)).sort(compareOrdinal).join("|");
    if (seen.has(key))
      continue;
    seen.add(key);
    out.push(...circularDepToProjectDiagnostics(cwd, dep));
  }
  return out;
}

// dist/clients/project-diagnostics/runner-adapters/opengrep.js
import * as path32 from "node:path";
function severityFor(raw) {
  switch (raw.toUpperCase()) {
    case "ERROR":
      return { severity: "error", blocking: true };
    case "INFO":
      return { severity: "info", blocking: false };
    default:
      return { severity: "warning", blocking: false };
  }
}
function opengrepFindingToProjectDiagnostic(cwd, finding) {
  const { severity, blocking } = severityFor(finding.severity);
  const cweSuffix = finding.cwe?.length ? ` (${finding.cwe[0]})` : "";
  return {
    filePath: path32.isAbsolute(finding.path) ? finding.path : path32.resolve(cwd, finding.path),
    line: finding.startLine,
    column: finding.startCol,
    severity,
    semantic: blocking ? "blocking" : "warning",
    tool: "opengrep",
    runner: "opengrep",
    rule: `opengrep:${finding.checkId}`,
    message: `${finding.message}${cweSuffix}`,
    source: "project-scan"
  };
}
function opengrepResultToProjectDiagnostics(cwd, result) {
  if (!result.success || result.findings.length === 0)
    return [];
  return result.findings.map((finding) => opengrepFindingToProjectDiagnostic(cwd, finding));
}

// dist/clients/project-diagnostics/runner-adapters/runner-findings.js
import * as fs20 from "node:fs";
import * as path33 from "node:path";
function failureMessage(failure) {
  const firstLine2 = failure.message.split("\n")[0]?.slice(0, 300) ?? "";
  return firstLine2 ? `${failure.name}: ${firstLine2}` : failure.name;
}
var LOCATION_LINE_COL_RE = /:(\d+)(?::(\d+))?$/;
function parseLocation(location) {
  if (!location)
    return {};
  const match = LOCATION_LINE_COL_RE.exec(location);
  if (!match)
    return {};
  return {
    line: Number.parseInt(match[1], 10),
    ...match[2] ? { column: Number.parseInt(match[2], 10) } : {}
  };
}
function testResultToProjectDiagnostics(result, stale = false) {
  if (result.failed === 0 && !result.error)
    return [];
  const stalePrefix = stale ? "[stale \u2014 from a prior turn] " : "";
  if (result.failures.length > 0) {
    return result.failures.map((failure) => ({
      filePath: result.file,
      ...parseLocation(failure.location),
      severity: "error",
      semantic: "blocking",
      tool: "test-runner",
      runner: result.runner,
      rule: `test:${result.runner}`,
      message: `${stalePrefix}${failureMessage(failure)}`,
      source: "project-scan"
    }));
  }
  if (isRunnerErrorResult(result)) {
    return [
      {
        filePath: result.file,
        severity: "info",
        semantic: "none",
        tool: "test-runner",
        runner: result.runner,
        rule: `test:${result.runner}`,
        message: `${stalePrefix}Test run error: ${result.error}`,
        source: "project-scan"
      }
    ];
  }
  return [
    {
      filePath: result.file,
      severity: "error",
      semantic: "blocking",
      tool: "test-runner",
      runner: result.runner,
      rule: `test:${result.runner}`,
      message: `${stalePrefix}${result.failed} test(s) failed${result.error ? ` (runner also reported: ${result.error})` : ""}`,
      source: "project-scan"
    }
  ];
}
function testRunnerFindingsToProjectDiagnostics(cache, cwd, runtime) {
  if (!cache.results || cache.results.length === 0)
    return [];
  const validation = cwd ? validateAdvisoryProvenance(cache, cwd, runtime) : { status: "unknown", reasons: ["validation-context-missing"] };
  const root = cwd ?? process.cwd();
  const missingKeys = new Set((cache.provenance?.files ?? []).filter((file) => !fs20.existsSync(path33.resolve(root, file.path))).map((file) => advisoryPathKey(file.path, root)));
  const historical = cache.superseded === true || cache.stale === true || validation.status !== "current";
  return cache.results.filter((result) => !missingKeys.has(advisoryPathKey(result.file, root))).flatMap((result) => testResultToProjectDiagnostics(result, historical)).map((diagnostic) => historical ? {
    ...diagnostic,
    severity: "info",
    semantic: "none",
    message: diagnostic.message.startsWith("[stale") ? diagnostic.message : `[historical \u2014 re-run to confirm] ${diagnostic.message}`
  } : diagnostic);
}

// dist/clients/project-diagnostics/fresh-fetch.js
var ANALYZER_IDS = [
  "knip",
  "jscpd",
  "madge",
  "gitleaks",
  "govulncheck",
  "opengrep",
  "trivy",
  "dead-code",
  "test-runner"
];
function pushUnique(list, id) {
  if (!list.includes(id))
    list.push(id);
}
function laneObservedAtMs(scannedAt, startedAtMs) {
  return parseScannedAtMs(scannedAt) ?? startedAtMs;
}
async function fetchFreshProjectDiagnostics(cacheManager2, cwd, clients, signal, options = {}) {
  const requestedRoot = options.analysisRoot === void 0 ? cwd : path34.resolve(cwd, options.analysisRoot);
  let analysisRoot;
  let analysisRootValidation;
  if (options.analysisRoot !== void 0) {
    try {
      if (!fs21.statSync(requestedRoot).isDirectory()) {
        throw new Error("is not a directory");
      }
      analysisRoot = fs21.realpathSync(requestedRoot);
    } catch (error) {
      const detail = error instanceof Error ? error.message : String(error);
      const reason = `the explicit analysis root ${requestedRoot} is unavailable or ${detail}`;
      analysisRootValidation = {
        state: "undecided",
        root: requestedRoot,
        reason
      };
      incrementDegradationCount({
        kind: "lens-diagnostics-analysis-root-rejected",
        subject: requestedRoot,
        reason
      });
      return {
        analysisRootValidation,
        diagnostics: [],
        runners: [],
        analyzed: [],
        authoritativeCoverage: [],
        cold: [...ANALYZER_IDS],
        coldReasons: Object.fromEntries(ANALYZER_IDS.map((id) => [id, reason])),
        failed: [],
        timings: {},
        analysisRootError: reason
      };
    }
  } else {
    analysisRoot = realpathOrResolve(cwd);
  }
  const unsafeRootReason = "the analysis root resolves at or above the home directory; heavyweight analyzers refuse to walk from there (#747)";
  const homeRoot = realpathOrResolve(options.homeDir ?? os4.homedir());
  if (options.analysisRoot !== void 0) {
    const unsafeExplicitRoot = !isSameOrWithin(homeRoot, analysisRoot) || isSameOrWithin(analysisRoot, homeRoot);
    analysisRootValidation = unsafeExplicitRoot ? {
      state: "unsafe",
      root: analysisRoot,
      reason: "explicit analysis root must be strictly contained by the canonical home directory"
    } : { state: "safe", root: analysisRoot };
  } else if (isAtOrAboveHomeDir(analysisRoot, options.homeDir)) {
    analysisRootValidation = {
      state: "unsafe",
      root: analysisRoot,
      reason: unsafeRootReason
    };
  } else {
    analysisRootValidation = { state: "safe", root: analysisRoot };
  }
  if (analysisRootValidation.state !== "safe") {
    incrementDegradationCount({
      kind: "lens-diagnostics-analysis-root-rejected",
      subject: analysisRoot,
      reason: analysisRootValidation.reason
    });
    return {
      analysisRootValidation,
      diagnostics: [],
      runners: [],
      analyzed: [],
      authoritativeCoverage: [],
      cold: [...ANALYZER_IDS],
      coldReasons: Object.fromEntries(ANALYZER_IDS.map((id) => [id, unsafeRootReason])),
      failed: [],
      timings: {},
      unsafeRoot: analysisRootValidation.state === "unsafe"
    };
  }
  const diagnostics = [];
  const runners = [];
  const analyzed = [];
  const authoritativeCoverage = [];
  const cold = [];
  const coldReasons = {};
  const partial = [];
  const partialReasons = {};
  const failed = [];
  const timings = {};
  const cachedAgeMs = {};
  const settledIds = /* @__PURE__ */ new Set();
  let dispositionSuppressed = 0;
  const dispositionSuppressedByLane = {};
  function markCold(id, reason) {
    pushUnique(cold, id);
    coldReasons[id] = reason;
  }
  function markPartial(id, reason) {
    pushUnique(partial, id);
    partialReasons[id] = reason;
  }
  function record(id, adapted, elapsedMs, analysedRoot, analysis, observedAt) {
    if (analysedRoot) {
      pushUnique(analyzed, id);
      if (analysis?.analyzedFiles !== void 0) {
        const root = realpathOrResolve(analysisRoot);
        authoritativeCoverage.push({
          runnerId: id,
          root,
          files: new Set(analysis.analyzedFiles.map((file) => {
            return realpathOrResolve(file);
          }))
        });
        if (analysis.analyzedFiles.length === 0) {
          recordDegradationOnce({
            kind: "runner-coverage-empty",
            subject: `${id}:${root}`,
            reason: `${id} analysed this root but declared zero scanned files, so retained ${id} findings are kept rather than retired`
          });
        }
      }
    }
    timings[id] = (timings[id] ?? 0) + elapsedMs;
    const stamped = observedAt === void 0 ? adapted : adapted.map((diagnostic) => ({ ...diagnostic, observedAt }));
    const kept = applyDispositionsMultiFile(stamped, analysisRoot, (d) => d.filePath);
    const suppressedHere = adapted.length - kept.length;
    dispositionSuppressed += suppressedHere;
    if (suppressedHere > 0) {
      dispositionSuppressedByLane[id] = (dispositionSuppressedByLane[id] ?? 0) + suppressedHere;
    }
    if (kept.length > 0) {
      diagnostics.push(...kept);
      pushUnique(runners, id);
    }
  }
  function recordFailed(id, result) {
    failed.push({
      id,
      ..."reason" in result && result.reason !== void 0 ? { reason: result.reason } : {},
      summary: "summary" in result && typeof result.summary === "string" ? result.summary : "analyzer reported an unsuccessful run"
    });
  }
  function task(id, run) {
    return run().finally(() => settledIds.add(id));
  }
  const tasks = [
    // knip — always applicable to probe (KnipClient.analyze itself no-ops
    // when no project root marker is found, matching session_start).
    task("knip", async () => {
      const startMs = Date.now();
      const result = await clients.knipClient.analyze(analysisRoot, getKnipIgnorePatterns());
      if (!result.success) {
        recordFailed("knip", result);
        return;
      }
      if (result.analyzed !== true) {
        markCold("knip", result.summary ?? "knip did not analyse this root");
        return;
      }
      cacheManager2.writeCache("knip", result, analysisRoot, {
        scanDurationMs: Date.now() - startMs
      });
      record("knip", knipIssuesToProjectDiagnostics(analysisRoot, result.issues ?? []), Date.now() - startMs, true, result, laneObservedAtMs(result.scannedAt, startMs));
    }),
    // jscpd — duplicate code detection. Cache key varies with TS-project
    // detection, exactly mirroring session_start's own logic.
    task("jscpd", async () => {
      if (!await clients.jscpdClient.ensureAvailable()) {
        markCold("jscpd", reasonFromAvailabilityVerdict("jscpd", clients.jscpdClient.getAvailabilityVerdict?.()));
        return;
      }
      const isTsProject = fs21.existsSync(path34.join(analysisRoot, "tsconfig.json"));
      const scannerKey = isTsProject ? "jscpd-ts" : "jscpd";
      const startMs = Date.now();
      const result = await clients.jscpdClient.scan(analysisRoot, void 0, void 0, isTsProject);
      if (!result.success) {
        recordFailed("jscpd", result);
        return;
      }
      if (result.analyzed !== true) {
        markCold("jscpd", "jscpd did not analyse this root (no source files, or the scan produced no report)");
        return;
      }
      cacheManager2.writeCache(scannerKey, result, analysisRoot, {
        scanDurationMs: Date.now() - startMs
      });
      record("jscpd", jscpdResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, true, result, laneObservedAtMs(result.scannedAt, startMs));
    }),
    // madge — circular-dependency detection.
    task("madge", async () => {
      if (!await clients.depChecker.ensureAvailable()) {
        markCold("madge", reasonFromAvailabilityVerdict("madge", clients.depChecker.getAvailabilityVerdict?.()));
        return;
      }
      const startMs = Date.now();
      const result = await clients.depChecker.scanProject(analysisRoot);
      cacheManager2.writeCache("madge", result, analysisRoot, {
        scanDurationMs: Date.now() - startMs
      });
      record("madge", circularDepsToProjectDiagnostics(analysisRoot, result.circular ?? []), Date.now() - startMs, result.analyzed === true, result, laneObservedAtMs(result.scannedAt, startMs));
    }),
    // gitleaks — committed-secrets detection. session_start/per-edit stay
    // config-gated per #130's strict default (GitleaksClient.hasGitleaksSignal),
    // but mode=full is an explicitly-requested comprehensive review — use
    // #130's own considered-but-unshipped "smart-default" tier instead (any
    // tracked git repo, GitleaksClient.hasGitRepo): gitleaks is cheap (~10MB
    // binary, no external DB pull) and findings are advisory-only, so the
    // stricter opt-in gate is needlessly conservative for this call. Refs #608
    // dogfooding finding that flagged gitleaks/trivy/govulncheck/dead-code as
    // "cold" on a project with no explicit gitleaks config.
    task("gitleaks", async () => {
      if (!GitleaksClient.hasGitRepo(analysisRoot)) {
        markCold("gitleaks", "not a git repository (no .git found)");
        return;
      }
      if (!await clients.gitleaksClient.ensureAvailable()) {
        markCold("gitleaks", reasonFromAvailabilityVerdict("gitleaks", clients.gitleaksClient.getAvailabilityVerdict?.()));
        return;
      }
      const startMs = Date.now();
      const result = await clients.gitleaksClient.scan(analysisRoot, {
        requireSignal: false
      });
      if (!result.success) {
        recordFailed("gitleaks", result);
        return;
      }
      cacheManager2.writeCache("gitleaks", result, analysisRoot, {
        scanDurationMs: Date.now() - startMs
      });
      record("gitleaks", gitleaksResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, result.analyzed === true, result, laneObservedAtMs(result.scannedAt, startMs));
    }),
    // govulncheck — Go module CVE detection. Go-module-gated per #132.
    task("govulncheck", async () => {
      if (!GovulncheckClient.hasGoModule(analysisRoot)) {
        markCold("govulncheck", "no go.mod found (not a Go module)");
        return;
      }
      if (!await clients.govulncheckClient.ensureAvailable()) {
        markCold("govulncheck", isToolInstallAllowedByTrust() ? reasonFromAvailabilityVerdict("govulncheck", clients.govulncheckClient.getAvailabilityVerdict?.()) : projectTrustDenialReason() ?? "govulncheck binary unavailable");
        return;
      }
      const startMs = Date.now();
      const result = await clients.govulncheckClient.analyze(analysisRoot);
      if (!result.success) {
        recordFailed("govulncheck", result);
        return;
      }
      cacheManager2.writeCache("govulncheck", result, analysisRoot, {
        scanDurationMs: Date.now() - startMs
      });
      record("govulncheck", govulncheckResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, result.analyzed === true, result, laneObservedAtMs(result.scannedAt, startMs));
    }),
    // opengrep — full-workspace semgrep-grade security/quality findings via a
    // single project-wide CLI scan (#584). Structurally always-on: mirrors
    // session_start (`runtime-session.ts`) and the LSP auxiliary's own
    // enablement (`OpengrepClient.resolveConfig` only picks WHICH rules run,
    // never whether opengrep runs at all), so unlike gitleaks/govulncheck/trivy
    // it carries NO static project-type gate — only an availability probe.
    // Re-entrancy-safe like the other SecurityScanClient-family analyzers:
    // `OpengrepClient.scan` routes through `SecurityScanClient.dedupeScan`, so a
    // call here that races the session_start whole-tree scan of the same root
    // JOINS the in-flight run instead of paying a second heavy scan (#883 single
    // source of truth — the exact wiring gitleaks/trivy use above). #585: this
    // was the one extractor registered in `extractors.ts` but MISSING here, so
    // opengrep scanned+cached yet nothing production read it back into
    // `lens_diagnostics mode=full` — the honesty gap (#533) this task closes.
    task("opengrep", async () => {
      if (!await clients.opengrepClient.ensureAvailable()) {
        markCold("opengrep", reasonFromAvailabilityVerdict("opengrep", clients.opengrepClient.getAvailabilityVerdict?.()));
        return;
      }
      const startMs = Date.now();
      const result = await clients.opengrepClient.scan(analysisRoot);
      if (!result.success) {
        recordFailed("opengrep", result);
        return;
      }
      if (result.analyzed !== true) {
        const reason = result.summary ?? result.reason ?? "opengrep did not analyse this root";
        if (result.partial)
          markPartial("opengrep", reason);
        else
          markCold("opengrep", reason);
        record("opengrep", opengrepResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, false, result, laneObservedAtMs(result.scannedAt, startMs));
        return;
      }
      if (result.partial) {
        markPartial("opengrep", result.summary ?? result.reason ?? "opengrep scan coverage is incomplete");
        record("opengrep", opengrepResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, true, result, laneObservedAtMs(result.scannedAt, startMs));
        return;
      }
      cacheManager2.writeCache("opengrep", result, analysisRoot, {
        scanDurationMs: Date.now() - startMs
      });
      record("opengrep", opengrepResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, result.analyzed === true, result, laneObservedAtMs(result.scannedAt, startMs));
    }),
    // trivy — dependency CVE detection. Explicit opt-in per #131.
    task("trivy", async () => {
      if (!TrivyClient.shouldScan(analysisRoot)) {
        markCold("trivy", "not opted in (.pi-lens.json trivy.enabled) or no dependency manifest found");
        return;
      }
      if (!await clients.trivyClient.ensureAvailable()) {
        markCold("trivy", reasonFromAvailabilityVerdict("trivy", clients.trivyClient.getAvailabilityVerdict?.()));
        return;
      }
      const startMs = Date.now();
      const result = await clients.trivyClient.scan(analysisRoot);
      if (!result.success) {
        recordFailed("trivy", result);
        return;
      }
      cacheManager2.writeCache("trivy", result, analysisRoot, {
        scanDurationMs: Date.now() - startMs
      });
      record("trivy", trivyResultToProjectDiagnostics(analysisRoot, result), Date.now() - startMs, result.analyzed === true, result, laneObservedAtMs(result.scannedAt, startMs));
    }),
    // dead-code — cross-file dead-code for non-JS/TS languages (#127).
    // Each client self-gates via detect(); only matching-language projects
    // incur the whole-tree scan. Run the applicable ones in parallel too.
    task("dead-code", async () => {
      const applicable = clients.deadCodeClients.filter((c) => c.detect(analysisRoot));
      if (applicable.length === 0) {
        markCold("dead-code", "no dead-code client detected an applicable language for this project");
        return;
      }
      await Promise.all(applicable.map(async (client) => {
        const cacheKey = `dead-code-${client.id}`;
        const startMs = Date.now();
        const result = await client.analyze(analysisRoot);
        if (!result.success) {
          recordFailed("dead-code", result);
          return;
        }
        if (result.analyzed === true) {
          cacheManager2.writeCache(cacheKey, result, analysisRoot, {
            scanDurationMs: Date.now() - startMs
          });
        }
        const adapted = deadCodeResultToProjectDiagnostics(analysisRoot, result);
        record("dead-code", adapted, Date.now() - startMs, result.analyzed === true, result, laneObservedAtMs(result.scannedAt, startMs));
      }));
    }),
    // test-runner — CACHE-READ only, unlike every task above (#1004). Its
    // session cadence doesn't fit the "trigger-or-join a fresh run" shape the
    // rest of this module uses: the actual scan is the per-edit turn_end fire
    // in `runtime-turn.ts`, which only ever runs the (targeted, cascade-aware)
    // test files touched by THIS turn's edits — there is no "whole project"
    // test run to (re-)trigger here, and unconditionally spawning a full suite
    // on every mode=full call would be the exact "heavy re-run on every call"
    // cost this module's other tasks avoid via de-dupe/gating. So this task
    // instead peeks at the `"test-runner-findings"` cache turn_end already
    // wrote — the same cache key, the same adapter
    // (`testRunnerFindingsToProjectDiagnostics`), and the same cache-only
    // contract the pre-#585 `extractCachedProjectDiagnostics` registry's
    // "test-runner" row used (see extractors.ts's removal note) — before #585
    // dropped that reader without replacing this one row's semantics here.
    // Deliberately never calls `writeCache`: there is nothing fresher to write
    // back, only what turn_end already produced.
    //
    // No double-count / honesty gap (#533): explicit `pilens_turn_end`
    // delivery may call `consumeTestFindings` (`runtime-context.ts`) to
    // read-and-clear this SAME cache key once. Automatic Pi delivery uses a
    // non-context custom entry and never consumes this pull-diagnostics cache.
    // This task only ever reads (never clears) it, so it can't race that
    // consumption into re-delivering a message twice — at most it surfaces
    // the same underlying failures a second time, through a different
    // surface (the mode=full project snapshot) that was previously silently
    // empty for this analyzer. If `consumeTestFindings` already cleared the
    // cache before this runs, this task correctly sees nothing and reports
    // `cold` rather than inventing stale data.
    task("test-runner", async () => {
      const startMs = Date.now();
      const cached = cacheManager2.readCache("test-runner-findings", analysisRoot);
      if (!cached?.data) {
        markCold("test-runner", "no turn_end test-runner cache entry yet this session");
        return;
      }
      cachedAgeMs["test-runner"] = Date.now() - new Date(cached.meta.timestamp).getTime();
      record(
        "test-runner",
        testRunnerFindingsToProjectDiagnostics(cached.data, analysisRoot, options.runtime),
        Date.now() - startMs,
        // Never authoritative: this lane reads the cache turn_end wrote,
        // it never runs a suite over the root this call (#2154).
        false,
        void 0,
        // #3600: the batch's own launch stamp (`launchedFrom`), taken at
        // turn_end BEFORE the suite ran — the safe (at-or-before the read)
        // direction. `meta.timestamp` is the cache WRITE time, i.e. AFTER
        // the suite read its files, so an edit during the run would look
        // older than the row and escape the widget gate. A legacy entry
        // with no `launchedFrom` falls back to its write time, then to the
        // lane start, rather than dropping the row.
        laneObservedAtMs(cached.data.launchedFrom?.revision.capturedAt ?? cached.meta.timestamp, startMs)
      );
    })
  ];
  const allSettled = Promise.all(tasks).then(() => "completed").catch(() => "completed");
  const outcome = signal ? await Promise.race([
    allSettled,
    new Promise((resolve30) => {
      if (signal.aborted) {
        resolve30("aborted");
        return;
      }
      signal.addEventListener("abort", () => resolve30("aborted"), {
        once: true
      });
    })
  ]) : await allSettled;
  if (outcome === "aborted") {
    const abortedIds = ANALYZER_IDS.filter((id) => !settledIds.has(id));
    for (const id of abortedIds)
      pushUnique(cold, id);
    return {
      analysisRootValidation,
      diagnostics,
      runners,
      analyzed,
      authoritativeCoverage,
      cold,
      coldReasons,
      partial,
      partialReasons,
      failed,
      timings,
      cachedAgeMs,
      aborted: true,
      abortedIds,
      dispositionSuppressed,
      dispositionSuppressedByLane
    };
  }
  return {
    analysisRootValidation,
    diagnostics,
    runners,
    analyzed,
    authoritativeCoverage,
    cold,
    coldReasons,
    partial,
    partialReasons,
    failed,
    timings,
    cachedAgeMs,
    dispositionSuppressed,
    dispositionSuppressedByLane
  };
}

// dist/tools/scan-progress.js
function scanningSummaryLine(details, text) {
  const d = details;
  if (d?.phase !== "scanning")
    return null;
  return text || `Scanning\u2026 ${d.completed ?? 0}/${d.total ?? 0}`;
}
function renderScanProgress(completed, total, label = "Scanning project diagnostics") {
  const pct = total > 0 ? Math.min(100, Math.round(completed / total * 100)) : 0;
  const width = 20;
  const filled = Math.round(pct / 100 * width);
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(Math.max(0, width - filled));
  return `${label}\u2026 [${bar}] ${completed}/${total} (${pct}%)`;
}
function makeProgressReporter(onUpdate, label, throttleMs = 250) {
  const emit = onUpdate;
  if (typeof emit !== "function")
    return void 0;
  let lastEmit = 0;
  return (completed, total) => {
    const now = Date.now();
    if (completed < total && now - lastEmit < throttleMs)
      return;
    lastEmit = now;
    emit({
      content: [
        { type: "text", text: renderScanProgress(completed, total, label) }
      ],
      details: { phase: "scanning", completed, total }
    });
  };
}

// dist/tools/lsp-diagnostics.js
import * as fs22 from "node:fs";
import * as path35 from "node:path";
var MAX_FILES = 100;
var MAX_BATCH_FILES = 100;
var MAX_DIAGNOSTICS = 200;
var DEFAULT_BATCH_CONCURRENCY = 8;
var MAX_BATCH_CONCURRENCY = 16;
var DEFAULT_BATCH_FILE_DEADLINE_MS = 15e3;
var SEVERITY_NAMES = {
  1: "error",
  2: "warning",
  3: "information",
  4: "hint"
};
var LSP_SEVERITY_FILTERS = [
  ...Object.values(SEVERITY_NAMES),
  "all"
];
function assertNeverBatchFileOutcome(value) {
  throw new Error(`Unhandled batch file outcome: ${String(value)}`);
}
function renderBatchOutcomeLines(outcomes, formatFile = (file) => file) {
  const lines = [];
  for (const outcome of outcomes) {
    switch (outcome.outcome) {
      case "too_large":
        lines.push(`${formatFile(outcome.file)}: ${outcome.reason ?? "file too large for LSP diagnostics"}`);
        break;
      case "clean":
      case "findings":
      case "unsupported":
      case "unavailable":
      case "failed":
      case "inconclusive":
        break;
      default:
        assertNeverBatchFileOutcome(outcome.outcome);
    }
  }
  return lines;
}
function batchFileDeadlineMs() {
  const raw = Number(process.env.PI_LENS_LSP_BATCH_FILE_MS);
  return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_BATCH_FILE_DEADLINE_MS;
}
function lspUnavailableMessage(filePath, health) {
  if (!health || !String(health.health ?? "").startsWith("no_clients")) {
    return void 0;
  }
  const candidates = health.candidateServerIds?.length ? ` candidates=${health.candidateServerIds.join(",")}` : "";
  const reason = (health.serverCountAttempted ?? 0) === 0 ? "no LSP server configured" : "no LSP client is currently ready";
  const stale = (health.mergedCount ?? 0) > 0 ? " Showing stale last-known diagnostics below." : " No diagnostics were collected.";
  return `LSP unavailable for ${filePath}: ${reason}; ready=${health.serverCountReady ?? 0}/${health.serverCountAttempted ?? 0}.${candidates}.${stale}`;
}
function boundedPositiveInt(value, fallback, min, max) {
  const parsed = typeof value === "number" ? Math.floor(value) : Number.NaN;
  if (!Number.isFinite(parsed))
    return fallback;
  return Math.max(min, Math.min(max, parsed));
}
function projectIgnorePredicate(root) {
  try {
    const matcher = getProjectIgnoreMatcher(root);
    return (fullPath, isDir) => matcher.isIgnored(fullPath, isDir);
  } catch {
    return () => false;
  }
}
async function collectFiles(dir, extensions, maxFiles, isIgnored = () => false) {
  const files = [];
  async function walk(current) {
    if (files.length >= maxFiles)
      return;
    let entries;
    try {
      entries = await fs22.promises.readdir(current, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      if (files.length >= maxFiles)
        return;
      if (entry.isSymbolicLink())
        continue;
      const full = path35.join(current, entry.name);
      if (entry.isDirectory()) {
        if (!isExcludedDirName(entry.name) && !isIgnored(full, true))
          await walk(full);
      } else if (entry.isFile() && extensions.includes(path35.extname(full))) {
        if (isIgnored(full, false))
          continue;
        files.push(full);
      }
    }
  }
  await walk(dir);
  return files;
}
function createLspDiagnosticsTool(nextWriteIndex, onConfirmedNoBlockers, getService = getLSPService) {
  return {
    async execute(_toolCallId, params, _signal, onUpdate, ctx) {
      const signal = combineAbortSignals(_signal, ctx.signal);
      const onProgress = makeProgressReporter(onUpdate, "Scanning LSP diagnostics");
      const typedParams = params;
      const severity = typedParams.severity ?? "all";
      const cwd = ctx.cwd ?? process.cwd();
      const concurrency = boundedPositiveInt(typedParams.concurrency, DEFAULT_BATCH_CONCURRENCY, 1, MAX_BATCH_CONCURRENCY);
      const waitMs = typeof typedParams.waitMs === "number" && typedParams.waitMs >= 0 ? Math.floor(typedParams.waitMs) : void 0;
      const serverScope = typedParams.serverScope === "primary" ? "primary" : "all";
      const lspService = getService();
      if (!lspService) {
        return {
          content: [
            { type: "text", text: "LSP service not available." }
          ],
          isError: true,
          details: {}
        };
      }
      if (Array.isArray(typedParams.paths) && typedParams.paths.length > 0) {
        if (typedParams.paths.length > MAX_BATCH_FILES) {
          return {
            content: [
              {
                type: "text",
                text: `paths accepts at most ${MAX_BATCH_FILES} files; received ${typedParams.paths.length}.`
              }
            ],
            isError: true,
            details: { mode: "batch", filesChecked: 0 }
          };
        }
        const rawPaths = typedParams.paths.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
        if (rawPaths.length !== typedParams.paths.length) {
          return {
            content: [
              {
                type: "text",
                text: "paths must contain non-empty file paths."
              }
            ],
            isError: true,
            details: { mode: "batch", filesChecked: 0 }
          };
        }
        const expanded = await expandPathsToFiles(cwd, rawPaths);
        if (expanded.files.length === 0) {
          const { listed, text } = describeDirectoriesWithoutFiles(expanded.directoriesWithoutFiles);
          return {
            content: [{ type: "text", text }],
            details: {
              mode: "batch",
              severity,
              filesChecked: 0,
              directoriesWithoutFiles: listed
            }
          };
        }
        return runBatchFileDiagnostics(expanded.files, severity, lspService, {
          concurrency,
          waitMs,
          signal,
          onProgress,
          nextWriteIndex,
          serverScope,
          cwd,
          onConfirmedNoBlockers
        }, expanded);
      }
      const rawPath = typedParams.path;
      if (!rawPath || rawPath.trim().length === 0) {
        return {
          content: [
            {
              type: "text",
              text: "path or paths is required."
            }
          ],
          isError: true,
          details: {}
        };
      }
      const absPath = normalizeMapKey(path35.resolve(cwd, rawPath));
      let stat;
      try {
        stat = fs22.statSync(absPath);
      } catch {
        return {
          content: [
            { type: "text", text: `Path not found: ${absPath}` }
          ],
          isError: true,
          details: {}
        };
      }
      if (stat.isDirectory()) {
        return runDirectoryDiagnostics(absPath, severity, lspService, {
          concurrency,
          waitMs,
          signal,
          onProgress,
          nextWriteIndex,
          serverScope,
          cwd,
          onConfirmedNoBlockers
        });
      }
      return runFileDiagnostics(absPath, severity, lspService, waitMs, nextWriteIndex, serverScope, cwd, onConfirmedNoBlockers);
    }
  };
}
async function collectDiagnosticsForFile(absPath, lspService, scanRoot, waitMs, serverScope = "all") {
  let timedOut = false;
  let content;
  let touched;
  try {
    content = fs22.readFileSync(absPath, "utf-8");
    const contentLimit = exceedsLspSyncLimits(content);
    if (contentLimit.tooLarge) {
      const reason = `file too large for LSP diagnostics (${contentLimit.reason})`;
      recordDegradationOnce({
        kind: "lsp-diagnostics-file-too-large",
        subject: absPath,
        reason
      });
      return {
        diagnostics: [],
        timedOut: false,
        confirmedByTouch: false,
        unconfirmedServerIds: [],
        diagnosticsUnsupportedServerIds: [],
        content,
        tooLargeReason: reason
      };
    }
    if (isWarmAttached()) {
      const attachedBudgetMs = waitMs ?? 15e3;
      const attached = await tryWarmAttachedDiagnostics(absPath, content, attachedBudgetMs, "sweep");
      if (attached?.available) {
        const scopedDiagnostics = serverScope === "primary" ? attached.response.diagnostics.filter((item) => item.serverId === primaryServerId(absPath)) : attached.response.diagnostics;
        const filtered2 = applyAuxiliarySuppressions(scopedDiagnostics, content, {
          fileRole: detectFileRole(absPath, content),
          filePath: absPath,
          scanRoot
        });
        return {
          diagnostics: filtered2,
          timedOut: false,
          // #1253: the incumbent's touch carries the same confirmation
          // provenance a local touch does (an `available: true` answer is
          // already gated on `fresh && !inconclusive`, but that is NOT
          // evidence a silent-on-clean server was confirmed — only the
          // explicit flag is). The incumbent always touches with
          // `clientScope: "with-auxiliary"`, so this is an AGGREGATE
          // confirmation: it carries the same caveat all-scope local
          // touches do, and classic TypeScript still needs the primary-only
          // tsserver sync check below rather than this verdict.
          // #1470: `"partial"` counts, for the same reason the local touch
          // branch below accepts it — the incumbent's primary confirmed; only
          // a named auxiliary did not.
          confirmedByTouch: attached.response.confirmation !== void 0 && primaryServerId(absPath) !== "typescript",
          // #1470: the incumbent's narrowed verdict, carried as an explicit DTO
          // field across the socket. An older incumbent omits it → empty → the
          // pre-#1470 handling, unchanged.
          unconfirmedServerIds: attached.response.unconfirmedServerIds ?? [],
          diagnosticsUnsupportedServerIds: [],
          content
        };
      }
    }
    touched = await lspService.touchFile(absPath, content, {
      diagnostics: "document",
      collectDiagnostics: true,
      maxClientWaitMs: waitMs,
      source: "lsp_diagnostics",
      clientScope: serverScope,
      // #3405: the caller asked whether THIS file is clean right now, and the
      // content above was just read from disk — so it is the file's saved
      // state. Without the save a save-triggered server answers zero
      // diagnostics for every file in the query, which this path would report
      // as clean.
      saved: true
    });
    timedOut = touched?.inconclusive === true;
  } catch {
  }
  const diagnostics = touched !== void 0 ? touched.diags : await lspService.getDiagnostics(absPath, waitMs !== void 0 ? "document" : "full");
  const filtered = content !== void 0 ? applyAuxiliarySuppressions(diagnostics, content, {
    fileRole: detectFileRole(absPath, content),
    filePath: absPath,
    scanRoot
  }) : diagnostics;
  const binding = touched?.binding;
  const confirmedByTouch = touchCompletedConfirmationPolicy(touched);
  return {
    diagnostics: filtered,
    timedOut,
    skipReason: touched?.skipReason,
    confirmedByTouch,
    // #1470: only a touch that resolved clients contributes a coverage gap;
    // the getDiagnostics fallback never reports one, which is honest — that
    // path claims no confirmation at all.
    unconfirmedServerIds: touchCoverageGap(touched),
    diagnosticsUnsupportedServerIds: touched?.diagnosticsUnsupportedServerIds ?? [],
    content,
    binding
  };
}
function diagnosticsToFileDiags(file, diagnostics) {
  return diagnostics.map((d) => ({
    file,
    line: d.range?.start?.line,
    character: d.range?.start?.character,
    severity: d.severity,
    message: d.message,
    source: d.source,
    serverId: d.serverId,
    code: d.code
  }));
}
async function classifyEmptyResult(file, lspService) {
  try {
    const snapshots = await lspService.getCapabilitySnapshots(file);
    const tier = classifyCascadeWaitTier(lspService, file, snapshots);
    return tier === "tier3-silent" ? "unconfirmed" : "clean";
  } catch {
    return "clean";
  }
}
async function primaryRootFallback(file, cwd) {
  const seen = [];
  await resolveLspCwdForFile(file, cwd, (fallback) => seen.push(fallback));
  return seen.find((fallback) => fallback.requiresProjectRoot);
}
async function resolveEmptyResult(file, lspService) {
  try {
    const snapshots = await lspService.getCapabilitySnapshots(file);
    const tier = classifyCascadeWaitTier(lspService, file, snapshots);
    if (tier !== "tier3-silent") {
      return { confirmed: true, diagnostics: [] };
    }
    const syncDiagnostics = await attemptTsserverSyncDiagnostics(file, lspService);
    if (syncDiagnostics === void 0) {
      return { confirmed: false, diagnostics: [] };
    }
    return { confirmed: true, diagnostics: syncDiagnostics };
  } catch {
    return { confirmed: true, diagnostics: [] };
  }
}
function canTrustTouchConfirmation(file, serverScope, confirmedByTouch) {
  return confirmedByTouch && (serverScope === "primary" || primaryServerId(file) !== "typescript");
}
function coveredSourcesForCheck(file, serverScope, unconfirmedServerIds) {
  const unconfirmed = new Set(unconfirmedServerIds);
  const covered = /* @__PURE__ */ new Set();
  let servers = [];
  try {
    servers = getServersForFileWithConfig(file) ?? [];
  } catch {
    return [];
  }
  for (const server of servers) {
    const auxiliary = server.role === "auxiliary";
    if (auxiliary && serverScope === "primary")
      continue;
    if (unconfirmed.has(server.id))
      continue;
    covered.add(auxiliary ? server.id : "lsp");
  }
  return [...covered];
}
function demoteInferredForFile(file, diagnostics, cwd, lspService) {
  return demoteInferredProjectDiagnostics(diagnostics, {
    filePath: file,
    cwd,
    service: lspService
  });
}
function applyProbeFindingPolicy(file, diagnostics, cwd, content, severity) {
  const started = Date.now();
  try {
    const result = applyLspFindingPolicy(diagnostics, {
      cwd,
      filePath: file,
      content,
      policyMap: loadProjectRulePolicyMap(cwd),
      fileRole: detectFileRole(file, content)
    });
    const inScopeBefore = applySeverityFilter(diagnostics, severity).length;
    const suppressed = inScopeBefore - applySeverityFilter(result.kept, severity).length;
    if (suppressed > 0) {
      logLatency({
        type: "phase",
        toolName: "lsp_diagnostics",
        filePath: file,
        phase: "lsp_probe_disposition_filter",
        durationMs: Date.now() - started,
        metadata: {
          suppressed,
          total: inScopeBefore,
          retainedSuppressed: countRetainedSuppressedRows(file)
        }
      });
    }
    return { kept: result.kept, inlineKept: result.inlineKept, suppressed };
  } catch (err) {
    recordDegradationOnce({
      kind: "lsp-probe-finding-policy",
      subject: cwd,
      reason: err instanceof Error ? err.message : String(err)
    });
    return { kept: diagnostics, inlineKept: diagnostics, suppressed: 0 };
  }
}
function replayContentForStrictMarks(file, cwd) {
  if (!hasStrictDispositionMarks(cwd))
    return void 0;
  try {
    return fs22.readFileSync(file, "utf-8");
  } catch {
    return void 0;
  }
}
function reconcileWidgetFromLspResult(file, rawDiags, confirmation, writeIndex, cwd, content, boundMismatch = false, observedAt) {
  const confirmed = (rawDiags.length > 0 || confirmation !== "unconfirmed") && !boundMismatch;
  if (!confirmed)
    return { confirmed: false, blocking: true };
  try {
    const diagnostics = convertLspDiagnostics(rawDiags, file, {
      scanOrigin: "lsp_diagnostics"
    });
    const retagged = retagAuxiliaryDiagnostics(diagnostics, rawDiags, content ?? "", { cwd, fileRole: detectFileRole(file, content) });
    const reconciled = reconcileScanDiagnostics(file, retagged, true, writeIndex, observedAt);
    if (!reconciled)
      return { confirmed: false, blocking: true };
    return { confirmed: true, blocking: retagged.some(isBlocking) };
  } catch {
    return { confirmed: false, blocking: true };
  }
}
async function collectFileDiagnosticResult(file, severity, lspService, waitMs, nextWriteIndex, serverScope = "all", cacheCtx, scopeKey, cwd = process.cwd(), onConfirmedNoBlockers) {
  const writeIndex = nextWriteIndex?.();
  const scannedAt = Date.now();
  let stat;
  try {
    stat = fs22.statSync(file);
    if (!stat.isFile()) {
      return { file, diagnostics: [], error: `${file}: not a file` };
    }
  } catch {
    return { file, diagnostics: [], error: `${file}: path not found` };
  }
  if (cacheCtx && scopeKey !== void 0) {
    const cached = cacheCtx.lookup(file, scopeKey);
    if (cached && cached.binding.boundToCurrentDisk !== false) {
      const confirmation2 = cached.diagnostics.length === 0 ? "clean" : void 0;
      const demotedCached = await demoteInferredForFile(file, cached.diagnostics, cwd, lspService);
      const cachedPolicy = applyProbeFindingPolicy(file, demotedCached, cwd, replayContentForStrictMarks(file, cwd), severity);
      const cachedDiags = cachedPolicy.kept;
      const filteredDiags2 = applySeverityFilter(cachedDiags, severity);
      reconcileWidgetFromLspResult(
        file,
        cachedDiags,
        confirmation2,
        writeIndex,
        cwd,
        void 0,
        // This branch only runs when the cache binding is not a mismatch
        // (guarded above), so boundMismatch is false; #1093's observedAt is
        // the cache entry's original scan time.
        false,
        cached.scannedAt
      );
      return {
        file,
        diagnostics: diagnosticsToFileDiags(file, filteredDiags2),
        confirmation: confirmation2,
        primaryServerId: primaryServerId(file),
        ...cachedPolicy.suppressed > 0 && {
          dispositionSuppressed: cachedPolicy.suppressed
        }
      };
    }
  }
  const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, tooLargeReason, skipReason } = await collectDiagnosticsForFile(file, lspService, cwd, waitMs, serverScope);
  if (tooLargeReason !== void 0) {
    return { file, diagnostics: [], outcome: "too_large", tooLargeReason };
  }
  const health = lspService.getDiagnosticsHealth?.(file);
  let effectiveRawDiags = rawDiags;
  let confirmation;
  if (diagnosticsUnsupportedServerIds.length > 0) {
    confirmation = void 0;
  } else if (skipReason !== void 0) {
    confirmation = "unconfirmed";
  } else if (timedOut) {
    if (applySeverityFilter(rawDiags, severity).length === 0) {
      confirmation = "unconfirmed";
    }
  } else if (canTrustTouchConfirmation(file, serverScope, confirmedByTouch)) {
    if (applySeverityFilter(rawDiags, severity).length === 0) {
      confirmation = "clean";
    }
  } else if (rawDiags.length === 0) {
    const resolved = await resolveEmptyResult(file, lspService);
    effectiveRawDiags = resolved.diagnostics;
    confirmation = resolved.confirmed ? resolved.diagnostics.length === 0 ? "clean" : void 0 : "unconfirmed";
  } else if (applySeverityFilter(rawDiags, severity).length === 0) {
    confirmation = await classifyEmptyResult(file, lspService);
  }
  const boundMismatch = binding?.boundToCurrentDisk === false;
  if (boundMismatch)
    confirmation = "unconfirmed";
  const rootFallback = confirmation === "clean" ? await primaryRootFallback(file, cwd) : void 0;
  if (rootFallback)
    confirmation = "unconfirmed";
  if (unconfirmedServerIds.length > 0 && confirmation === "clean") {
    confirmation = "unconfirmed";
  }
  effectiveRawDiags = await demoteInferredForFile(file, effectiveRawDiags, cwd, lspService);
  const policy = applyProbeFindingPolicy(file, effectiveRawDiags, cwd, collectedContent, severity);
  effectiveRawDiags = policy.kept;
  const filteredDiags = applySeverityFilter(effectiveRawDiags, severity);
  const verdict = reconcileWidgetFromLspResult(
    file,
    effectiveRawDiags,
    confirmation,
    writeIndex,
    cwd,
    collectedContent,
    boundMismatch,
    // #3505 r1 F1: the widget row is observed at the read, as the cache
    // entry recorded below is.
    scannedAt
  );
  if (verdict.confirmed && !verdict.blocking) {
    onConfirmedNoBlockers?.({
      filePath: file,
      writeIndex,
      coveredSources: coveredSourcesForCheck(file, serverScope, unconfirmedServerIds),
      cwd
    });
  }
  if (cacheCtx && scopeKey !== void 0 && confirmation !== "unconfirmed" && diagnosticsUnsupportedServerIds.length === 0 && unconfirmedServerIds.length === 0) {
    cacheCtx.record(file, scopeKey, policy.inlineKept, stat.mtimeMs, collectedContent !== void 0 ? hashDiagnosticContent(collectedContent) : void 0, stat.size, scannedAt);
  }
  return {
    file,
    diagnostics: diagnosticsToFileDiags(file, filteredDiags),
    unavailable: lspUnavailableMessage(file, health),
    confirmation,
    timedOut: confirmation === "unconfirmed" ? timedOut : void 0,
    ...rootFallback && { rootFallback },
    ...skipReason !== void 0 && { skipReason },
    primaryServerId: primaryServerId(file),
    diagnosticsUnsupported: diagnosticsUnsupportedServerIds.length > 0,
    ...policy.suppressed > 0 && {
      dispositionSuppressed: policy.suppressed
    }
  };
}
async function runFileDiagnostics(absPath, severity, lspService, waitMs, nextWriteIndex, serverScope = "all", cwd = process.cwd(), onConfirmedNoBlockers) {
  const writeIndex = nextWriteIndex?.();
  const observedAt = Date.now();
  const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, tooLargeReason, skipReason } = await collectDiagnosticsForFile(absPath, lspService, cwd, waitMs, serverScope);
  if (tooLargeReason !== void 0) {
    return {
      content: [{ type: "text", text: tooLargeReason }],
      details: {
        filePath: absPath,
        mode: "file",
        severity,
        serverScope,
        outcome: "too_large",
        tooLargeReason,
        diagnostics: [],
        totalDiagnostics: 0
      }
    };
  }
  const lspHealth = lspService.getDiagnosticsHealth?.(absPath);
  const unavailable = lspUnavailableMessage(absPath, lspHealth);
  let effectiveRawDiags = rawDiags;
  let confirmation;
  if (diagnosticsUnsupportedServerIds.length > 0) {
    confirmation = void 0;
  } else if (skipReason !== void 0) {
    confirmation = "unconfirmed";
  } else if (timedOut) {
    if (applySeverityFilter(rawDiags, severity).length === 0) {
      confirmation = "unconfirmed";
    }
  } else if (canTrustTouchConfirmation(absPath, serverScope, confirmedByTouch)) {
    if (applySeverityFilter(rawDiags, severity).length === 0) {
      confirmation = "clean";
    }
  } else if (rawDiags.length === 0) {
    const resolved = await resolveEmptyResult(absPath, lspService);
    effectiveRawDiags = resolved.diagnostics;
    confirmation = resolved.confirmed ? resolved.diagnostics.length === 0 ? "clean" : void 0 : "unconfirmed";
  } else if (applySeverityFilter(rawDiags, severity).length === 0) {
    confirmation = await classifyEmptyResult(absPath, lspService);
  }
  const boundMismatch = binding?.boundToCurrentDisk === false;
  if (boundMismatch)
    confirmation = "unconfirmed";
  const rootFallback = confirmation === "clean" ? await primaryRootFallback(absPath, cwd) : void 0;
  if (rootFallback)
    confirmation = "unconfirmed";
  const primaryCoverageGapOnly = unconfirmedServerIds.length > 0 && confirmation === "clean";
  if (primaryCoverageGapOnly)
    confirmation = "unconfirmed";
  effectiveRawDiags = await demoteInferredForFile(absPath, effectiveRawDiags, cwd, lspService);
  const policy = applyProbeFindingPolicy(absPath, effectiveRawDiags, cwd, collectedContent, severity);
  effectiveRawDiags = policy.kept;
  const filtered = applySeverityFilter(effectiveRawDiags, severity);
  const total = filtered.length;
  const truncated = total > MAX_DIAGNOSTICS;
  const limited = truncated ? filtered.slice(0, MAX_DIAGNOSTICS) : filtered;
  const unconfirmed = confirmation === "unconfirmed";
  const verdict = reconcileWidgetFromLspResult(absPath, effectiveRawDiags, confirmation, writeIndex, cwd, collectedContent, boundMismatch, observedAt);
  if (verdict.confirmed && !verdict.blocking) {
    onConfirmedNoBlockers?.({
      filePath: absPath,
      writeIndex,
      coveredSources: coveredSourcesForCheck(absPath, serverScope, unconfirmedServerIds),
      cwd
    });
  }
  const primaryId = primaryServerId(absPath);
  const primaryDiags = limited.filter((d) => d.serverId === primaryId);
  const auxiliaryDiags = limited.filter((d) => d.serverId !== primaryId);
  const primaryLine = (() => {
    if (diagnosticsUnsupportedServerIds.length > 0) {
      return `Primary LSP${primaryId ? ` (${primaryId})` : ""}: navigation-only \u2014 diagnostics unsupported (no pull provider or publish evidence).`;
    }
    if (timedOut) {
      return "Primary LSP: check timed out \u2014 NOT the same as 0 diagnostics; the file may still have errors that just hadn't been reported yet. Re-check after the server settles, or increase waitMs.";
    }
    if (rootFallback) {
      return `Primary LSP${primaryId ? ` (${primaryId})` : ""}: unconfirmed \u2014 ${describeRootFallback(rootFallback)}. NOT the same as 0 diagnostics; check the file from inside its project and re-run.`;
    }
    if (unconfirmed && !primaryCoverageGapOnly) {
      return `Primary LSP${primaryId ? ` (${primaryId})` : ""}: unconfirmed \u2014 cannot confirm clean (push-only, silent-on-clean, e.g. classic typescript-language-server never publishes on a clean re-check). NOT the same as 0 diagnostics; re-check after an edit, or use waitMs to wait longer.`;
    }
    if (primaryDiags.length === 0) {
      return `Primary LSP${primaryId ? ` (${primaryId})` : ""}: confirmed clean.`;
    }
    return `Primary LSP${primaryId ? ` (${primaryId})` : ""}: ${primaryDiags.length} diagnostic${primaryDiags.length === 1 ? "" : "s"}.`;
  })();
  const coverageLine = unconfirmedServerIds.length > 0 ? `Auxiliary coverage INCOMPLETE \u2014 ${[...unconfirmedServerIds].join(", ")} did not answer within the wait budget, so ${unconfirmedServerIds.length === 1 ? "its findings are" : "their findings are"} NOT included here. This is not a clean bill of health for ${unconfirmedServerIds.length === 1 ? "that scanner" : "those scanners"}; re-check after the next edit. waitMs can extend an ordinary wait, but it cannot override a session-demoted scanner.` : void 0;
  const suppressedLine = dispositionSuppressedLine(policy.suppressed);
  let text;
  if (total === 0) {
    text = [
      primaryLine,
      "",
      unavailable ?? "No auxiliary findings.",
      ...coverageLine ? [coverageLine] : [],
      ...suppressedLine ? [suppressedLine] : []
    ].join("\n");
  } else {
    const lines = [primaryLine, ""];
    if (primaryDiags.length > 0) {
      lines.push(...primaryDiags.map(formatDiag), "");
    }
    if (auxiliaryDiags.length > 0) {
      lines.push(`Auxiliary findings (${auxiliaryDiags.length}):`);
      lines.push(...auxiliaryDiags.map(formatDiag));
    }
    if (coverageLine)
      lines.push("", coverageLine);
    if (suppressedLine)
      lines.push("", suppressedLine);
    if (unavailable)
      lines.unshift(unavailable, "");
    if (truncated) {
      lines.unshift(`Found ${total} diagnostics (showing first ${MAX_DIAGNOSTICS}):`);
    }
    text = lines.join("\n");
  }
  return {
    content: [{ type: "text", text }],
    details: {
      filePath: absPath,
      mode: "file",
      severity,
      serverScope,
      ...unavailable ? { unavailable } : {},
      primaryServerId: primaryId,
      primaryDiagnosticsCount: primaryDiags.length,
      auxiliaryDiagnosticsCount: auxiliaryDiags.length,
      diagnostics: limited.map((d) => ({
        line: d.range?.start?.line,
        character: d.range?.start?.character,
        severity: d.severity,
        message: d.message,
        source: d.source,
        code: d.code
      })),
      totalDiagnostics: total,
      ...policy.suppressed > 0 && {
        dispositionSuppressed: policy.suppressed
      },
      truncated,
      unconfirmed,
      timedOut: unconfirmed ? timedOut : void 0,
      navigationOnlyFiles: diagnosticsUnsupportedServerIds.length > 0 ? 1 : void 0,
      ...skipReason !== void 0 && { skipReason },
      ...rootFallback && {
        rootFallbackReason: describeRootFallback(rootFallback)
      },
      // #1470: which servers this result does NOT speak for. Absent when it
      // speaks for all of them.
      ...unconfirmedServerIds.length > 0 && {
        unconfirmedServerIds: [...unconfirmedServerIds]
      },
      lspHealth,
      waitMs
    }
  };
}
function tallyConfirmation(results) {
  let clean = 0;
  let unconfirmed = 0;
  let timedOut = 0;
  const rootFallbackReasons = /* @__PURE__ */ new Set();
  let navigationOnly = 0;
  let tooLarge = 0;
  for (const result of results) {
    if (result.tooLargeReason) {
      tooLarge += 1;
      continue;
    }
    if (result.diagnosticsUnsupported) {
      navigationOnly += 1;
      continue;
    }
    if (result.diagnostics.length > 0)
      continue;
    if (result.confirmation === "unconfirmed") {
      unconfirmed += 1;
      if (result.timedOut)
        timedOut += 1;
      if (result.rootFallback) {
        rootFallbackReasons.add(describeRootFallback(result.rootFallback));
      }
    } else if (classifyBatchFileOutcome(result) === "clean") {
      clean += 1;
    }
  }
  return {
    clean,
    unconfirmed,
    timedOut,
    rootFallbackReasons: [...rootFallbackReasons],
    navigationOnly,
    tooLarge
  };
}
function classifyBatchFileOutcome(result) {
  if (result.error)
    return "failed";
  if (result.tooLargeReason)
    return "too_large";
  if (result.diagnosticsUnsupported)
    return "unsupported";
  if (result.timedOut || result.confirmation === "unconfirmed") {
    return "inconclusive";
  }
  if (result.diagnostics.length > 0)
    return "findings";
  if (result.unavailable) {
    return result.primaryServerId ? "unavailable" : "unsupported";
  }
  if (!result.primaryServerId)
    return "unsupported";
  return "clean";
}
function inconclusiveBatchResult(file, reason) {
  return {
    file,
    diagnostics: [],
    confirmation: "unconfirmed",
    timedOut: true,
    outcome: "inconclusive",
    inconclusiveReason: reason
  };
}
function unconfirmedReasonClause(unconfirmed, timedOut, rootFallbackReasons = []) {
  if (rootFallbackReasons.length > 0) {
    return `${rootFallbackReasons.join("; ")} (${unconfirmed} unconfirmed in all).`;
  }
  const silent = unconfirmed - timedOut;
  if (timedOut > 0 && silent > 0) {
    return `${timedOut} timed out (check didn't complete within budget) and ${silent} from a server that cannot confirm clean (push-only, silent-on-clean).`;
  }
  if (timedOut > 0) {
    return `${timedOut} timed out (check didn't complete within the wait budget).`;
  }
  return "from a server that cannot confirm clean (push-only, silent-on-clean; e.g. classic typescript-language-server does not publish on a clean re-check).";
}
async function collectBatchDiagnostics(files, severity, lspService, options) {
  const resolvedCwd = options.cwd ?? process.cwd();
  const cacheCtx = createWorkspaceDiagnosticsCacheContext(resolvedCwd);
  const scopeKey = buildScopeKey(options.serverScope ?? "all");
  const resultsByIndex = /* @__PURE__ */ new Map();
  let completed = 0;
  const pendingIndices = /* @__PURE__ */ new Map();
  files.forEach((file, index) => {
    const queue = pendingIndices.get(file);
    if (queue)
      queue.push(index);
    else
      pendingIndices.set(file, [index]);
  });
  const groups = groupFilesByPrimaryServer(files);
  await runPerServerGroups(groups, options.concurrency, async (group) => {
    if (options.signal?.aborted)
      return;
    const first = group.files[0];
    if (first && lspService && !isWarmAttached() && typeof lspService.ensureWarmForSweep === "function") {
      await lspService.ensureWarmForSweep(first, {
        signal: options.signal
      });
      if (options.signal?.aborted)
        return;
    }
    await mapWithConcurrency(group.files, 1, async (file) => {
      const index = pendingIndices.get(file).shift();
      const work = collectFileDiagnosticResult(file, severity, lspService, options.waitMs, options.nextWriteIndex, options.serverScope, cacheCtx, scopeKey, resolvedCwd, options.onConfirmedNoBlockers);
      const bounded2 = withDeadline(work, {
        ms: batchFileDeadlineMs(),
        onTimeout: "undefined",
        onReject: "undefined"
      });
      const result = options.signal ? await Promise.race([
        bounded2,
        new Promise((resolve30) => {
          if (options.signal?.aborted) {
            resolve30(void 0);
            return;
          }
          options.signal?.addEventListener("abort", () => resolve30(void 0), {
            once: true
          });
        })
      ]) : await bounded2;
      resultsByIndex.set(index, result ?? inconclusiveBatchResult(file, options.signal?.aborted ? "Batch aborted before this file completed." : `File check exceeded ${batchFileDeadlineMs()}ms.`));
      completed += 1;
      options.onProgress?.(completed, files.length);
    }, options.signal);
  }, options.signal);
  const results = [...resultsByIndex.entries()].sort(([left], [right]) => left - right).map(([, result]) => result);
  cacheCtx.persist();
  const fileErrors = results.flatMap((result) => result.error ? [result.error] : []);
  const lspHealthWarnings = results.flatMap((result) => result.unavailable ? [result.unavailable] : []);
  const allDiags = results.flatMap((result) => result.diagnostics);
  const total = allDiags.length;
  const truncated = total > MAX_DIAGNOSTICS;
  const display = truncated ? allDiags.slice(0, MAX_DIAGNOSTICS) : allDiags;
  const { clean, unconfirmed, timedOut, rootFallbackReasons } = tallyConfirmation(results);
  for (const result of results) {
    result.outcome = classifyBatchFileOutcome(result);
  }
  const outcomeCounts = Object.fromEntries([
    "clean",
    "findings",
    "unsupported",
    "unavailable",
    "failed",
    "too_large",
    "inconclusive"
  ].map((outcome) => [
    outcome,
    results.filter((result) => result.outcome === outcome).length
  ]));
  const primaryIdByFile = new Map(results.map((r) => [r.file, r.primaryServerId]));
  const primaryDisplay = display.filter((d) => d.serverId === primaryIdByFile.get(d.file));
  const auxiliaryDisplay = display.filter((d) => d.serverId !== primaryIdByFile.get(d.file));
  return {
    results,
    fileErrors,
    lspHealthWarnings,
    total,
    truncated,
    display,
    primaryDisplay,
    auxiliaryDisplay,
    clean,
    unconfirmed,
    timedOut,
    rootFallbackReasons,
    outcomeCounts,
    incompleteFiles: Math.max(0, files.length - results.length)
  };
}
async function expandPathsToFiles(cwd, rawPaths) {
  const explicitFiles = [];
  const directories = [];
  const seenFileKeys = /* @__PURE__ */ new Set();
  for (const entry of rawPaths) {
    const abs = normalizeMapKey(path35.resolve(cwd, entry));
    let stat;
    try {
      stat = fs22.statSync(abs);
    } catch {
    }
    if (stat?.isDirectory()) {
      directories.push(abs);
    } else {
      explicitFiles.push(abs);
      seenFileKeys.add(abs);
    }
  }
  const files = [...explicitFiles];
  const directoriesWithoutFiles = [];
  let capped = false;
  for (const directory of directories) {
    const collected = await collectDirectoryScanFiles(directory, MAX_FILES - files.length, seenFileKeys);
    capped ||= collected.capped;
    if (collected.eligible === 0)
      directoriesWithoutFiles.push(directory);
    for (const file of collected.files) {
      const key = normalizeMapKey(file);
      seenFileKeys.add(key);
      files.push(key);
    }
  }
  if (capped) {
    recordDegradationOnce({
      kind: "lsp-diagnostics-paths-cap",
      subject: cwd,
      reason: `paths directory expansion reached the ${MAX_FILES}-file cap; later entries were not checked`
    });
  }
  return { files, directoriesWithoutFiles, capped };
}
async function runBatchFileDiagnostics(absPaths, severity, lspService, options, scan = { capped: false, directoriesWithoutFiles: [] }) {
  const { capped, directoriesWithoutFiles } = scan;
  if (absPaths.length === 0) {
    return {
      content: [{ type: "text", text: "No file paths provided." }],
      isError: true,
      details: { mode: "batch", severity, filesChecked: 0 }
    };
  }
  const { results, fileErrors, lspHealthWarnings, total, truncated, display, primaryDisplay, auxiliaryDisplay, clean, unconfirmed, timedOut, rootFallbackReasons, outcomeCounts, incompleteFiles } = await collectBatchDiagnostics(absPaths, severity, lspService, options);
  const navigationOnly = results.filter((result) => result.diagnosticsUnsupported).length;
  const dispositionSuppressed = results.reduce((sum, result) => sum + (result.dispositionSuppressed ?? 0), 0);
  const lines = [
    `Files checked: ${results.length}${capped ? ` (capped at ${MAX_FILES})` : ""}`,
    `Total diagnostics: ${total}`,
    `Concurrency: ${options.concurrency}`
  ];
  if (options.waitMs !== void 0)
    lines.push(`Wait budget: ${options.waitMs}ms`);
  lines.push(`Outcomes: ${Object.entries(outcomeCounts).map(([outcome, count]) => `${outcome}=${count}`).join(", ")}`);
  const { listed: emptyListed, text: emptyText } = describeDirectoriesWithoutFiles(directoriesWithoutFiles);
  if (directoriesWithoutFiles.length > 0)
    lines.push("", emptyText);
  const notConfirmed = outcomeCounts.inconclusive + outcomeCounts.unavailable + outcomeCounts.unsupported + outcomeCounts.failed + outcomeCounts.too_large + incompleteFiles;
  if (notConfirmed > 0) {
    lines.push("", `Checks not confirmed: ${notConfirmed} (inconclusive/unavailable/unsupported/failed or not started).`);
  }
  if (incompleteFiles > 0) {
    lines.push("", `Batch incomplete: ${incompleteFiles} file${incompleteFiles === 1 ? "" : "s"} were not confirmed because the batch was aborted.`);
  }
  if (fileErrors.length > 0)
    lines.push("", "File errors:", ...fileErrors);
  const outcomeDetails = results.map((result) => ({
    file: result.file,
    outcome: result.outcome,
    reason: result.tooLargeReason ?? result.inconclusiveReason ?? result.error ?? result.unavailable
  }));
  const tooLargeLines = renderBatchOutcomeLines(outcomeDetails);
  if (tooLargeLines.length > 0) {
    lines.push("", "Files too large for LSP diagnostics:", ...tooLargeLines);
  }
  if (lspHealthWarnings.length > 0) {
    lines.push("", "LSP health warnings:", ...lspHealthWarnings.slice(0, 10));
  }
  if (unconfirmed > 0) {
    lines.push("", `${clean} file${clean === 1 ? "" : "s"} confirmed clean, ${unconfirmed} unconfirmed: ${unconfirmedReasonClause(unconfirmed, timedOut, rootFallbackReasons)} NOT the same as 0 diagnostics.`);
  }
  if (display.length === 0) {
    if (unconfirmed === 0 && outcomeCounts.clean === results.length) {
      lines.push("", "No diagnostics found.");
    }
  } else {
    if (primaryDisplay.length > 0) {
      lines.push("", `Primary findings (${primaryDisplay.length}):`);
      lines.push(...primaryDisplay.map(formatDisplayDiag));
    }
    if (auxiliaryDisplay.length > 0) {
      lines.push("", `Auxiliary findings (${auxiliaryDisplay.length}):`);
      lines.push(...auxiliaryDisplay.map(formatDisplayDiag));
    }
    if (truncated) {
      lines.push("", `... (${total - MAX_DIAGNOSTICS} more diagnostics not shown)`);
    }
  }
  const suppressedLine = dispositionSuppressedLine(dispositionSuppressed);
  if (suppressedLine)
    lines.push("", suppressedLine);
  return {
    content: [{ type: "text", text: lines.join("\n") }],
    details: {
      mode: "batch",
      severity,
      serverScope: options.serverScope ?? "all",
      filesChecked: results.length,
      ...capped ? { capped: true } : {},
      ...directoriesWithoutFiles.length > 0 ? { directoriesWithoutFiles: emptyListed } : {},
      concurrency: options.concurrency,
      waitMs: options.waitMs,
      diagnostics: display,
      primaryDiagnosticsCount: primaryDisplay.length,
      auxiliaryDiagnosticsCount: auxiliaryDisplay.length,
      totalDiagnostics: total,
      ...dispositionSuppressed > 0 && { dispositionSuppressed },
      truncated,
      cleanFiles: clean,
      unconfirmedFiles: unconfirmed,
      navigationOnlyFiles: navigationOnly > 0 ? navigationOnly : void 0,
      timedOutFiles: timedOut > 0 ? timedOut : void 0,
      outcomes: results.map((result) => ({
        file: result.file,
        outcome: result.outcome,
        reason: result.tooLargeReason ?? result.inconclusiveReason ?? result.error ?? result.unavailable,
        primaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.serverId === result.primaryServerId).length,
        auxiliaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.serverId !== result.primaryServerId).length
      })),
      outcomeCounts,
      incompleteFiles: incompleteFiles > 0 ? incompleteFiles : void 0,
      fileErrors: fileErrors.length > 0 ? fileErrors : void 0,
      lspHealthWarnings: lspHealthWarnings.length > 0 ? lspHealthWarnings : void 0
    }
  };
}
async function resolveDirectoryScanExtensions(hasMatch) {
  for (const family of SCAN_LANGUAGE_PRIORITY) {
    const exts = family.flatMap((languageId) => extensionsForLanguage(languageId));
    if (await hasMatch(exts)) {
      return exts;
    }
  }
  return void 0;
}
async function collectDirectoryScanFiles(absPath, maxFiles, seen = /* @__PURE__ */ new Set()) {
  const isIgnored = projectIgnorePredicate(absPath);
  let collectedFiles = [];
  await resolveDirectoryScanExtensions(async (exts) => {
    collectedFiles = await collectFiles(
      absPath,
      [...exts],
      // At most `seen.size` collected files can be dropped below, so this
      // walk still sees `maxFiles + 1` new files when the directory has them.
      maxFiles + seen.size + 1,
      isIgnored
    );
    return collectedFiles.length > 0;
  });
  const fresh = collectedFiles.flatMap((file) => seen.has(normalizeMapKey(file)) ? [] : [file]);
  return {
    files: fresh.slice(0, maxFiles),
    capped: fresh.length > maxFiles,
    // Raw count: a directory whose files are all `seen` is not empty.
    eligible: collectedFiles.length
  };
}
function describeDirectoriesWithoutFiles(directories) {
  const listed = directories.slice(0, 5);
  const more = directories.length - listed.length;
  return {
    listed,
    text: `No supported source files found in: ${listed.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`
  };
}
async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
  const { files: filesToProcess, capped: wasCapped } = await collectDirectoryScanFiles(absPath, MAX_FILES);
  if (filesToProcess.length === 0) {
    return {
      content: [
        {
          type: "text",
          text: describeDirectoriesWithoutFiles([absPath]).text
        }
      ],
      details: {
        filePath: absPath,
        mode: "directory",
        severity,
        filesScanned: 0
      }
    };
  }
  const { results, fileErrors, lspHealthWarnings, total, truncated, display, primaryDisplay, auxiliaryDisplay, clean, unconfirmed, timedOut, rootFallbackReasons, outcomeCounts } = await collectBatchDiagnostics(filesToProcess, severity, lspService, options);
  const navigationOnly = results.filter((result) => result.diagnosticsUnsupported).length;
  const dispositionSuppressed = results.reduce((sum, result) => sum + (result.dispositionSuppressed ?? 0), 0);
  const suppressedLine = dispositionSuppressedLine(dispositionSuppressed);
  const outcomeDetails = results.map((result) => ({
    file: result.file,
    outcome: result.outcome,
    reason: result.tooLargeReason ?? result.inconclusiveReason ?? result.error ?? result.unavailable
  }));
  const tooLargeLines = renderBatchOutcomeLines(outcomeDetails, (file) => path35.relative(absPath, file));
  let text;
  if (total === 0) {
    const cleanLine = unconfirmed > 0 ? `${clean} clean \xB7 ${unconfirmed} unconfirmed: ${unconfirmedReasonClause(unconfirmed, timedOut, rootFallbackReasons)} NOT the same as 0 diagnostics.` : "No diagnostics found.";
    text = [
      `Directory: ${absPath}`,
      `Files scanned: ${filesToProcess.length}${wasCapped ? ` (capped at ${MAX_FILES})` : ""}`,
      ...lspHealthWarnings.length > 0 ? [
        "LSP unavailable for one or more files:",
        ...lspHealthWarnings.slice(0, 10)
      ] : unconfirmed > 0 || outcomeCounts.clean === filesToProcess.length ? [cleanLine] : [],
      ...tooLargeLines.length > 0 ? ["", "Files too large for LSP diagnostics:", ...tooLargeLines] : [],
      ...suppressedLine ? ["", suppressedLine] : []
    ].join("\n");
  } else {
    const lines = [
      `Directory: ${absPath}`,
      `Files scanned: ${filesToProcess.length}${wasCapped ? ` (capped at ${MAX_FILES})` : ""}`,
      `Files with errors: ${new Set(display.map((d) => d.file)).size}`,
      `Total diagnostics: ${total}`,
      ...lspHealthWarnings.length > 0 ? ["", "LSP health warnings:", ...lspHealthWarnings.slice(0, 10)] : [],
      // #533/#570: the remaining clean-looking files in a mixed scan may
      // still be unconfirmed — say so even though the directory as a
      // whole found diagnostics elsewhere.
      ...unconfirmed > 0 ? [
        "",
        `${clean} other file${clean === 1 ? "" : "s"} confirmed clean, ${unconfirmed} unconfirmed: ` + unconfirmedReasonClause(unconfirmed, timedOut, rootFallbackReasons)
      ] : [],
      ""
    ];
    const toRelative = (d) => ({
      ...d,
      file: path35.relative(absPath, d.file)
    });
    if (primaryDisplay.length > 0) {
      lines.push(`Primary findings (${primaryDisplay.length}):`);
      lines.push(...primaryDisplay.map(toRelative).map(formatDisplayDiag));
      lines.push("");
    }
    if (auxiliaryDisplay.length > 0) {
      lines.push(`Auxiliary findings (${auxiliaryDisplay.length}):`);
      lines.push(...auxiliaryDisplay.map(toRelative).map(formatDisplayDiag));
    }
    if (truncated) {
      lines.push("", `... (${total - MAX_DIAGNOSTICS} more diagnostics not shown)`);
    }
    if (suppressedLine)
      lines.push("", suppressedLine);
    if (tooLargeLines.length > 0) {
      lines.push("", "Files too large for LSP diagnostics:", ...tooLargeLines);
    }
    text = lines.join("\n");
  }
  return {
    content: [{ type: "text", text }],
    details: {
      filePath: absPath,
      mode: "directory",
      severity,
      serverScope: options.serverScope ?? "all",
      filesScanned: filesToProcess.length,
      capped: wasCapped,
      diagnostics: display.map((d) => ({
        file: path35.relative(absPath, d.file),
        line: d.line,
        character: d.character,
        severity: d.severity,
        message: d.message,
        source: d.source,
        code: d.code
      })),
      primaryDiagnosticsCount: primaryDisplay.length,
      auxiliaryDiagnosticsCount: auxiliaryDisplay.length,
      totalDiagnostics: total,
      ...dispositionSuppressed > 0 && { dispositionSuppressed },
      truncated,
      cleanFiles: clean,
      unconfirmedFiles: unconfirmed,
      outcomes: outcomeDetails,
      outcomeCounts,
      navigationOnlyFiles: navigationOnly > 0 ? navigationOnly : void 0,
      timedOutFiles: timedOut > 0 ? timedOut : void 0,
      fileErrors: fileErrors.length > 0 ? fileErrors : void 0,
      lspHealthWarnings: lspHealthWarnings.length > 0 ? lspHealthWarnings : void 0,
      concurrency: options.concurrency,
      waitMs: options.waitMs
    }
  };
}
function dispositionSuppressedLine(suppressed) {
  if (suppressed <= 0)
    return void 0;
  return `suppressed by disposition: ${suppressed} finding(s) dropped from this result because they're marked false-positive/won't-fix, disabled by \`.pi-lens.json\` rule policy, or suppressed by an inline \`pi-lens-ignore\` comment. Not a clean verdict for those locations \u2014 they were found, then intentionally hidden.`;
}
function applySeverityFilter(diags, severity) {
  if (severity === "all")
    return diags;
  const maxLevel = {
    error: 1,
    warning: 2,
    information: 3,
    hint: 4
  };
  const max = maxLevel[severity] ?? 0;
  if (max === 0)
    return diags;
  return diags.filter((d) => (d.severity ?? 3) <= max);
}
function formatDisplayDiag(d) {
  const sevName = SEVERITY_NAMES[d.severity] ?? "unknown";
  const loc = d.line !== void 0 ? `${d.file}:${d.line + 1}:${(d.character ?? 0) + 1}` : d.file;
  const src = d.source ? `[${d.source}]` : "";
  const code = d.code ? ` (${d.code})` : "";
  return `${loc}: ${sevName}${src}${code}: ${d.message}`;
}
function formatDiag(diag) {
  const loc = diag.range?.start?.line !== void 0 ? `L${diag.range.start.line + 1}:${(diag.range.start.character ?? 0) + 1}` : "";
  const src = diag.source ? `[${diag.source}]` : "";
  const code = diag.code ? ` (${diag.code})` : "";
  const sevName = SEVERITY_NAMES[diag.severity] ?? "unknown";
  return `${loc}: ${sevName}${src}${code}: ${diag.message}`;
}

// dist/tools/lens-diagnostics.js
var MAX_DIAGNOSTICS_PER_FILE = 50;
var MAX_PATHS_ENTRIES = MAX_BATCH_FILES;
function notRequestedReason(host) {
  return `refreshRunners not requested this call (quick mode) \u2014 pass refreshRunners=cheap/all/cached to ${resolveLensToolName("lens_diagnostics", host) ?? "diagnostics"} mode=full to run it`;
}
var FULL_SCAN_WALL_CLOCK_MS = getFullScanWallClockMs();
var FULL_SCAN_BINDING_YIELD_EVERY = 16;
async function findFullScanBindingMismatches(results) {
  const mismatched = /* @__PURE__ */ new WeakSet();
  let sinceYield = 0;
  for (const result of results) {
    if (result.boundToCurrentDisk === false) {
      mismatched.add(result);
    } else if (
      // A `true` verdict came from the LSP/cache binding seam and has already
      // compared its fingerprint with disk. Only legacy/partial producers need
      // this fallback read; `unknown` remains unknown when no hash exists.
      result.boundToCurrentDisk !== true && result.contentHash !== void 0
    ) {
      try {
        const content = await fs23.readFile(result.filePath, "utf-8");
        if (hashDiagnosticContent(content) !== result.contentHash) {
          mismatched.add(result);
        }
      } catch {
      }
    }
    sinceYield += 1;
    if (sinceYield >= FULL_SCAN_BINDING_YIELD_EVERY) {
      sinceYield = 0;
      await new Promise((resolve30) => setImmediate(resolve30));
    }
  }
  return mismatched;
}
function createLensDiagnosticsTool(cacheManager2, getCwd, getLspService = getLSPService, flushPending = async () => {
}, nextWriteIndex, captureLspStatusRepaint, getRuntime, isLensGuardEnabled2 = () => true) {
  const lspProbe = createLspDiagnosticsTool(nextWriteIndex, (info) => {
    const runtime = getRuntime?.();
    if (!runtime)
      return;
    const retired = retireInlineBlockerAndResyncGuard({
      runtime,
      cacheManager: cacheManager2,
      cwd: info.cwd,
      filePath: info.filePath,
      ...info.writeIndex === void 0 ? {} : { writeIndex: info.writeIndex },
      coveredSources: info.coveredSources,
      lensGuardEnabled: isLensGuardEnabled2()
    });
    if (retired) {
      logExtension({
        subsystem: "git-guard",
        level: "debug",
        message: `inline_blocker: retired for ${info.filePath}`
      });
    }
  }, getLspService);
  return {
    name: "lens_diagnostics",
    label: "Project Diagnostics",
    description: 'Query pi-lens diagnostics from the session cache or an active LSP probe, at paths or workspace scope. Empty cache is not proof of clean; probe changed paths when findings are absent or stale. Example: `{source: "lsp", scope: "paths", paths: ["src/app.ts"]}`.',
    promptSnippet: "lens_diagnostics source=session reads the session cache and an empty cache is not proof of a clean file; use source=lsp scope=paths for changed files when cached findings are absent or stale",
    renderResult: compactRenderResult(({ details, args, isError, text }) => {
      if (details?.source === "lsp") {
        const count = details.totalDiagnostics ?? 0;
        const files2 = details.filesChecked ?? details.filesScanned ?? 0;
        const noun = count === 1 ? "diagnostic" : "diagnostics";
        const filePath = typeof details?.filePath === "string" ? details.filePath : Array.isArray(args?.paths) && args.paths.length === 1 && typeof args.paths[0] === "string" ? args.paths[0] : void 0;
        const singleFile = (details?.mode === "file" || files2 === 1) && typeof filePath === "string" ? ` ${path36.basename(filePath)}` : "";
        const scope = files2 > 1 ? ` across ${files2} files` : singleFile;
        if (isError)
          return `lens_diagnostics lsp \u2014 ${text.split("\n")[0] ?? "error"}`;
        if ((details.navigationOnlyFiles ?? 0) > 0)
          return `lens_diagnostics${scope} \u2014 ${count} ${noun} \xB7 ${details.cleanFiles ?? 0} clean \xB7 ${details.navigationOnlyFiles} navigation-only`;
        if ((details.unconfirmedFiles ?? 0) > 0)
          return `lens_diagnostics${scope} \u2014 ${count} ${noun} \xB7 ${details.cleanFiles ?? 0} clean \xB7 ${details.unconfirmedFiles} unconfirmed${details.timedOutFiles ? ` (${details.timedOutFiles} timed out)` : ""}`;
        const outcomeCounts = details.outcomeCounts;
        const tooLargeLines = renderBatchOutcomeLines(details.outcomes ?? []);
        if (tooLargeLines.length > 0)
          return `lens_diagnostics${scope} \u2014 too large: ${tooLargeLines.join("; ")}`;
        const notConfirmed = outcomeCounts ? (outcomeCounts.inconclusive ?? 0) + (outcomeCounts.unavailable ?? 0) + (outcomeCounts.unsupported ?? 0) + (outcomeCounts.failed ?? 0) + (outcomeCounts.too_large ?? 0) : 0;
        if (notConfirmed > 0)
          return `lens_diagnostics${scope} \u2014 ${count} ${noun} \xB7 ${notConfirmed} checks not confirmed`;
        if ((details.incompleteFiles ?? 0) > 0)
          return `lens_diagnostics${scope} \u2014 incomplete (${details.incompleteFiles} files not confirmed)`;
        if (count === 0 && details.unconfirmed)
          return details.timedOut ? `lens_diagnostics${scope} \u2014 timed out (result may be incomplete)` : `lens_diagnostics${scope} \u2014 unconfirmed (server cannot confirm clean)`;
        return `lens_diagnostics${scope} \u2014 ${count} ${noun}`;
      }
      const scanning = scanningSummaryLine(details, text);
      if (scanning)
        return scanning;
      const mode = details?.mode ?? (typeof args.mode === "string" ? args.mode : "delta");
      if (isError) {
        return `lens_diagnostics ${mode} \u2014 ${text.split("\n")[0] ?? "error"}`;
      }
      const coldSuffix = details?.coldRunners && details.coldRunners.length > 0 ? ` (${details.coldRunners.length} cold: ${details.coldRunners.join(", ")})` : "";
      const failedSuffix = details?.failedAnalyzers && details.failedAnalyzers.length > 0 ? ` (${details.failedAnalyzers.length} failed: ${details.failedAnalyzers.map((item) => item.id).join(", ")})` : "";
      if (mode === "delta") {
        const aw = details?.actionableWarnings ?? 0;
        const cq = details?.qualityIssues ?? 0;
        const pd = details?.projectDiagnostics ?? 0;
        if (aw + cq + pd === 0)
          return `lens_diagnostics delta \u2014 clean${coldSuffix}${failedSuffix}`;
        return `lens_diagnostics delta \u2014 ${aw} actionable \xB7 ${cq} quality \xB7 ${pd} project${coldSuffix}${failedSuffix}`;
      }
      const b = details?.totalBlocking ?? 0;
      const e = details?.totalErrors ?? 0;
      const w = details?.totalWarnings ?? 0;
      const adv = details?.totalAdvisories ?? 0;
      const files = details?.filesWithIssues ?? details?.filesChecked ?? 0;
      if (b + e + w === 0) {
        const advisorySuffix = adv > 0 ? `, ${adv} hint${adv === 1 ? "" : "s"}` : "";
        return `lens_diagnostics ${mode} \u2014 clean${advisorySuffix} (${files} files)${coldSuffix}${failedSuffix}`;
      }
      const parts = [`${b} blocking`];
      if (b === 0 && e > 0)
        parts.push(`${e} errors`);
      parts.push(`${w} warnings`);
      return `lens_diagnostics ${mode} \u2014 ${parts.join(" \xB7 ")} (${files} files)${coldSuffix}${failedSuffix}`;
    }),
    parameters: Type.Object({
      source: Type.Optional(Type.String({
        enum: ["session", "lsp"],
        description: "Evidence source: session cache (empty cache is not proof of a clean file \u2014 use source=lsp for changed files) or an active LSP probe."
      })),
      scope: Type.Optional(Type.String({
        enum: ["paths", "workspace"],
        description: (
          // #2860: no schema value maps to "current turn's delta" — the
          // default (omitted scope) already gives that for
          // source=session, and for source=lsp `delta` was byte-identical
          // to `paths` (round-2 verify N/F4), so exposing it would only
          // invite a caller to believe it does something distinct.
          "Coverage scope: explicit paths, or workspace (default: current turn's delta for source=session)."
        )
      })),
      mode: Type.Optional(Type.String({
        enum: ["delta", "all", "full"],
        description: "delta = current turn; all = cache-only; full = active LSP scan of paths. Empty cache is not proof of clean."
      })),
      path: Type.Optional(Type.String({
        description: "One file or directory for source=lsp checks."
      })),
      concurrency: Type.Optional(Type.Number({
        description: "Source=lsp batch concurrency (maximum 16)."
      })),
      waitMs: Type.Optional(Type.Number({
        description: "Source=lsp per-file wait budget in milliseconds."
      })),
      serverScope: Type.Optional(Type.String({
        enum: ["primary", "all"],
        description: "Source=lsp server coverage: primary or all."
      })),
      refreshRunners: Type.Optional(Type.Union([
        Type.Boolean(),
        Type.String({ enum: ["cached", "cheap", "all", "none"] })
      ], {
        description: "Analyzer refresh mode for full view."
      })),
      maxProjectFiles: Type.Optional(Type.Number({
        description: "Project-file limit for cheap runners."
      })),
      maxLspFiles: Type.Optional(Type.Number({
        description: "File limit for the LSP sweep."
      })),
      includeGenerated: Type.Optional(Type.Boolean({
        description: "Include generated-name paths in full scans."
      })),
      analysisRoot: Type.Optional(Type.String({
        description: "Explicit project directory for heavyweight analyzers; it must exist and resolve strictly below the home directory."
      })),
      severity: Type.Optional(Type.String({
        enum: [...LSP_SEVERITY_FILTERS],
        description: "Filter by severity threshold (default: all): error shows errors; warning includes errors and warnings; information includes errors, warnings, and information; hint and all show every known tier."
      })),
      paths: Type.Optional(Type.Array(Type.String(), {
        maxItems: MAX_PATHS_ENTRIES,
        description: "Files or directories to filter."
      }))
    }),
    async execute(_toolCallId, params, signal, onUpdate, ctx) {
      const requestedSource = params.source;
      const requestedScope = params.scope;
      const legacyMode = params.mode;
      const source = requestedSource ?? "session";
      const scope = requestedScope ?? (legacyMode === "delta" || legacyMode === void 0 ? source === "lsp" ? "paths" : "delta" : "workspace");
      const cwd = ctx.cwd ?? getCwd();
      if (source === "lsp") {
        const lspParams = { ...params };
        delete lspParams.source;
        delete lspParams.scope;
        const hasExplicitPath = typeof lspParams.path === "string" && lspParams.path.length > 0;
        const hasExplicitPaths = Array.isArray(lspParams.paths) && lspParams.paths.length > 0;
        if (scope === "workspace" && !hasExplicitPath && !hasExplicitPaths) {
          lspParams.path = cwd;
        }
        const lspStart = Date.now();
        const result = await lspProbe.execute(_toolCallId, lspParams, signal, onUpdate, ctx.signal ? { cwd, signal: ctx.signal } : { cwd });
        logLatency({
          type: "phase",
          toolName: "lens_diagnostics",
          filePath: cwd,
          phase: "lens_diagnostics_lsp_route",
          durationMs: Date.now() - lspStart,
          metadata: {
            scope,
            isError: result.isError === true,
            totalDiagnostics: result.details?.totalDiagnostics ?? 0,
            cleanFiles: result.details?.cleanFiles ?? 0,
            unconfirmedFiles: result.details?.unconfirmedFiles ?? 0,
            timedOutFiles: result.details?.timedOutFiles ?? 0
          }
        });
        return {
          ...result,
          isError: result.isError === true,
          details: { ...result.details, source, scope }
        };
      }
      if (requestedSource !== void 0) {
        const effectiveMode = legacyMode ?? (scope === "workspace" ? "all" : scope === "paths" ? "all" : "delta");
        params = { ...params, mode: effectiveMode };
      }
      const repaintLspStatus = captureLspStatusRepaint?.(ctx);
      const mode = params.mode ?? "delta";
      const severity = params.severity ?? "all";
      const refreshRunners = params.refreshRunners;
      const parsePositiveInt = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
      const maxProjectFiles = parsePositiveInt(params.maxProjectFiles);
      const maxLspFiles = parsePositiveInt(params.maxLspFiles);
      const includeGenerated = params.includeGenerated === true;
      let pathsScope;
      try {
        pathsScope = resolvePathsScope(cwd, params.paths);
      } catch (err) {
        return {
          content: [
            {
              type: "text",
              text: err instanceof Error ? err.message : String(err)
            }
          ],
          isError: true,
          details: { mode, pathsError: true }
        };
      }
      const abortSignal = combineAbortSignals(signal, ctx.signal);
      await flushPending();
      const staleDropped = await reconcileStaleWidgetFiles();
      if (mode === "all") {
        const dependencyGateStart = Date.now();
        const { demoted: dependencyDemoted, truncatedImports } = await reconcileStaleWidgetDependencyBlockers(cwd, getRuntime?.()?.turnIndex);
        logLatency({
          type: "phase",
          toolName: "lens_diagnostics",
          filePath: cwd,
          phase: "blocker_freshness_widget_gate",
          durationMs: Date.now() - dependencyGateStart,
          metadata: {
            demoted: dependencyDemoted,
            truncatedImports,
            surface: "mode=all"
          }
        });
        return formatAllMode(cwd, severity, void 0, void 0, staleDropped, pathsScope, dependencyDemoted);
      }
      if (mode === "full") {
        const ceiling = AbortSignal.timeout(FULL_SCAN_WALL_CLOCK_MS);
        const fullSignal = combineAbortSignals(abortSignal, ceiling);
        const onProgress = makeProgressReporter(onUpdate);
        return formatFullMode(cwd, severity, getLspService(), cacheManager2, {
          refreshRunners,
          maxProjectFiles,
          maxLspFiles,
          includeGenerated,
          signal: fullSignal,
          wallClockMs: FULL_SCAN_WALL_CLOCK_MS,
          onProgress,
          onServerReady: repaintLspStatus,
          pathsScope,
          nextWriteIndex,
          runtime: getRuntime?.(),
          analysisRoot: typeof params.analysisRoot === "string" ? params.analysisRoot : void 0,
          host: ctx.host ?? "pi"
        });
      }
      return formatDeltaMode(cacheManager2, cwd, severity, pathsScope);
    }
  };
}
function formatProjectDeltaDiagnostic(diagnostic, stale) {
  const marker = stale ? "\u25CB" : diagnostic.semantic === "blocking" || diagnostic.severity === "error" ? "\u{1F534}" : "\u2139";
  const rule = diagnostic.rule ?? diagnostic.code ?? diagnostic.runner;
  const where = stale ? STALE_LINE_MARKER : `L${diagnostic.line ?? "?"}`;
  return `  ${marker} ${where}  ${rule}  ${diagnostic.message}`;
}
function appendProjectDiagnosticsDeltaLines(groups, cwd, report, severity, includeFile) {
  const scoped = (report?.diagnostics ?? []).filter((diagnostic) => includeFile(diagnostic.filePath) && matchesSeverity(projectDiagnosticToWidget(diagnostic, diagnostic.filePath), severity));
  const { "lens-diagnostics-delta-project": gated } = gateFindingsByPathFreshness({
    cwd,
    sources: {
      "lens-diagnostics-delta-project": {
        findings: scoped,
        scannedAt: report?.generatedAt,
        citedPath: (d) => d.filePath,
        onMissing: "drop"
      }
    }
  });
  const staleSet = new Set(gated.stale);
  const diagnostics = [...gated.live, ...gated.stale];
  const byFile = /* @__PURE__ */ new Map();
  for (const diagnostic of diagnostics) {
    const filePath = path36.resolve(diagnostic.filePath);
    const bucket = byFile.get(filePath) ?? [];
    bucket.push(diagnostic);
    byFile.set(filePath, bucket);
  }
  for (const [filePath, fileDiagnostics] of byFile) {
    const group = getDeltaFileGroup(groups, cwd, filePath);
    for (const diagnostic of fileDiagnostics) {
      group.projectLines.push(formatProjectDeltaDiagnostic(diagnostic, staleSet.has(diagnostic)));
    }
  }
  return diagnostics.length;
}
var CACHED_CONTENT_MEMO_CAP = 64;
var cachedContentMemo = new BoundedLruCache(CACHED_CONTENT_MEMO_CAP);
function readContentForAnchors(filePath, stat) {
  const key = normalizeEphemeralMapKey(filePath);
  const hit = cachedContentMemo.get(key);
  if (hit && hit.mtimeMs === stat.mtimeMs && hit.size === stat.size) {
    return hit.content;
  }
  let content;
  try {
    content = fsSync.readFileSync(filePath, "utf-8");
  } catch {
    return void 0;
  }
  cachedContentMemo.set(key, {
    mtimeMs: stat.mtimeMs,
    size: stat.size,
    content
  });
  return content;
}
function applyCachedDispositions(cachedDiagnostics, cwd, filePath) {
  if (!hasAnyDispositionMarks(cwd))
    return cachedDiagnostics;
  if (!hasStrictDispositionMarks(cwd)) {
    return applyWeakDispositions(cachedDiagnostics, cwd, filePath);
  }
  let stat;
  try {
    stat = fsSync.statSync(filePath);
  } catch {
    return applyWeakDispositions(cachedDiagnostics, cwd, filePath);
  }
  const referenceMs = cachedDiagnostics.reduce((latest, d) => Math.max(latest, d.observedAt ?? 0), 0);
  if (referenceMs > 0 && freshnessFromMtime({ mtimeMs: stat.mtimeMs, referenceMs }).verdict === "stale") {
    return applyWeakDispositions(cachedDiagnostics, cwd, filePath);
  }
  const content = readContentForAnchors(filePath, stat);
  if (content === void 0) {
    return applyWeakDispositions(cachedDiagnostics, cwd, filePath);
  }
  return applyDispositions(cachedDiagnostics, cwd, filePath, content);
}
function filterDeltaReportDispositions(report, cwd, policyMap) {
  if (!report?.diagnostics.length)
    return report;
  const kept = report.diagnostics.filter((d) => applyCachedDispositions([projectDiagnosticToWidget(d, d.filePath)], cwd, d.filePath).length === 1);
  const policyKept = applyRulePolicy(kept, policyMap);
  if (policyKept.length === report.diagnostics.length)
    return report;
  return { ...report, diagnostics: policyKept };
}
function applyDeltaFreshnessGate(files, cwd, generatedAt) {
  const stamps = new Set(files.map((file) => file.generatedAt ?? generatedAt ?? ""));
  if (stamps.size > 1) {
    const gatedByPath = /* @__PURE__ */ new Map();
    for (const stamp of stamps) {
      const group = files.filter((file) => (file.generatedAt ?? generatedAt ?? "") === stamp);
      for (const gated2 of applyDeltaFreshnessGate(group.map((file) => ({
        filePath: file.filePath,
        warnings: file.warnings
      })), cwd, stamp === "" ? void 0 : stamp)) {
        gatedByPath.set(gated2.filePath, gated2);
      }
    }
    return files.map((file) => gatedByPath.get(file.filePath)).filter((entry) => entry !== void 0);
  }
  const stampedAt = [...stamps][0];
  const effectiveAt = stampedAt !== void 0 && stampedAt !== "" ? stampedAt : generatedAt;
  if (!effectiveAt)
    return files;
  const flat = [];
  for (const file of files) {
    for (const warning of file.warnings)
      flat.push({ filePath: file.filePath, warning });
  }
  if (flat.length === 0)
    return files;
  const { "lens-diagnostics-delta": gated } = gateFindingsByPathFreshness({
    cwd,
    sources: {
      "lens-diagnostics-delta": {
        findings: flat,
        scannedAt: effectiveAt,
        citedPath: (f) => f.filePath,
        onMissing: "drop"
      }
    }
  });
  const byFile = /* @__PURE__ */ new Map();
  for (const f of gated.live) {
    const arr = byFile.get(f.filePath) ?? [];
    arr.push(f.warning);
    byFile.set(f.filePath, arr);
  }
  for (const f of gated.stale) {
    const arr = byFile.get(f.filePath) ?? [];
    arr.push({
      ...f.warning,
      stale: true,
      line: void 0,
      staleAsOf: effectiveAt
    });
    byFile.set(f.filePath, arr);
  }
  return files.map((file) => ({
    filePath: file.filePath,
    warnings: byFile.get(file.filePath) ?? []
  })).filter((file) => file.warnings.length > 0);
}
function getDeltaFileGroup(groups, cwd, filePath) {
  const rel = path36.relative(cwd, filePath);
  let group = groups.get(rel);
  if (!group) {
    group = {
      rel,
      actionableLines: [],
      qualityLines: [],
      projectLines: [],
      incomplete: false
    };
    groups.set(rel, group);
  }
  return group;
}
function formatDeltaMode(cacheManager2, cwd, severity, pathsScope) {
  const actionableEntry = cacheManager2.readCache("actionable-warnings", cwd);
  const qualityEntry = cacheManager2.readCache("code-quality-warnings", cwd);
  const reverifyIncompletePaths = new Set((actionableEntry?.data?.files ?? []).filter((file) => file.reVerifyIncomplete).map((file) => normalizeMapKey(file.filePath)));
  const actionable = actionableEntry?.data;
  const quality = qualityEntry?.data;
  const policyMap = loadProjectRulePolicyMap(cwd);
  const projectDelta = filterDeltaReportDispositions(loadProjectDiagnosticsDeltaReport(cwd), cwd, policyMap);
  const includeFile = createScopedFileFilter(cwd, pathsScope);
  const visibleWarningFiles = (files) => (files ?? []).filter((file) => includeFile(file.filePath)).map((file) => ({
    filePath: file.filePath,
    // #2504 r4 (F1): carried through, so a merged report's older
    // entries keep their own age all the way to the freshness gate.
    generatedAt: file.generatedAt,
    warnings: applyRulePolicy(applyCachedDispositions(file.warnings ?? [], cwd, file.filePath), policyMap)
  })).filter((file) => file.warnings.length > 0);
  const actionableFiles = applyDeltaFreshnessGate(visibleWarningFiles(actionable?.files), cwd, actionable?.generatedAt);
  const qualityFiles = applyDeltaFreshnessGate(visibleWarningFiles(quality?.files), cwd, quality?.generatedAt);
  const matchingWarnings = (warnings) => warnings.filter((warning) => matchesRecordSeverity(warning.severity, severity));
  const filteredActionableFiles = actionableFiles.map((file) => ({
    ...file,
    warnings: matchingWarnings(file.warnings)
  })).filter((file) => file.warnings.length > 0);
  const filteredQualityFiles = qualityFiles.map((file) => ({
    ...file,
    warnings: matchingWarnings(file.warnings)
  })).filter((file) => file.warnings.length > 0);
  const groups = /* @__PURE__ */ new Map();
  if (filteredActionableFiles.length > 0) {
    for (const file of filteredActionableFiles) {
      const group = getDeltaFileGroup(groups, cwd, file.filePath);
      for (const w of file.warnings) {
        const where = w.stale ? STALE_LINE_MARKER : `L${w.line ?? "?"}`;
        group.actionableLines.push(`  \u26A0 ${where}  ${w.rule ?? w.code ?? w.tool}  ${w.message}`);
      }
      const key = normalizeMapKey(file.filePath);
      if (reverifyIncompletePaths.has(key))
        group.incomplete = true;
      if (!group.staleRow) {
        const staleWarning = file.warnings.find((w) => w.stale);
        if (staleWarning)
          group.staleRow = { staleAsOf: staleWarning.staleAsOf };
      }
    }
  }
  if (filteredQualityFiles.length > 0) {
    for (const file of filteredQualityFiles) {
      const group = getDeltaFileGroup(groups, cwd, file.filePath);
      for (const w of file.warnings) {
        const where = w.stale ? STALE_LINE_MARKER : `L${w.line ?? "?"}`;
        group.qualityLines.push(`  \u2139 ${where}  ${w.rule ?? w.code ?? w.tool}  ${w.message}`);
      }
      const key = normalizeMapKey(file.filePath);
      if (reverifyIncompletePaths.has(key))
        group.incomplete = true;
      if (!group.staleRow) {
        const staleWarning = file.warnings.find((w) => w.stale);
        if (staleWarning)
          group.staleRow = { staleAsOf: staleWarning.staleAsOf };
      }
    }
  }
  const projectDeltaCount = appendProjectDiagnosticsDeltaLines(groups, cwd, projectDelta, severity, includeFile);
  const lines = [];
  for (const group of groups.values()) {
    lines.push(group.rel);
    lines.push(...group.actionableLines);
    lines.push(...group.qualityLines);
    lines.push(...group.projectLines);
    if (group.staleRow) {
      lines.push(`  (${formatCacheAgeLabel(group.staleRow.staleAsOf)})`);
    }
    if (group.incomplete)
      lines.push("  (re-verify incomplete)");
  }
  const selectedActionableFiles = filteredActionableFiles;
  const selectedQualityFiles = filteredQualityFiles;
  const aw = selectedActionableFiles.reduce((count, file) => count + file.warnings.length, 0);
  const cq = selectedQualityFiles.reduce((count, file) => count + file.warnings.length, 0);
  if (lines.length === 0) {
    let text = `No ${severity === "all" ? "" : severity + " "}issues in the current turn delta.`;
    const carried = getFileDiagnosticSummaries().filter((f) => includeFile(f.filePath)).map((f) => ({
      ...f,
      diagnostics: applyRulePolicy(applyCachedDispositions(f.diagnostics, cwd, f.filePath), policyMap)
    })).filter((f) => f.diagnostics.length > 0);
    const carriedIssues = carried.reduce((n, f) => n + f.diagnostics.length, 0);
    if (carried.length > 0) {
      text += ` ${carriedIssues} finding${carriedIssues === 1 ? "" : "s"} across ${carried.length} file${carried.length === 1 ? "" : "s"} carried over from earlier this session \u2014 use mode=all to see them.`;
    }
    text += pathsScopeCacheOnlyNote(pathsScope);
    return {
      content: [{ type: "text", text }],
      details: { mode: "delta", warnings: 0, carriedOverFiles: carried.length }
    };
  }
  const summary = `
Summary (turn delta): ${aw} actionable warning${aw === 1 ? "" : "s"} \xB7 ${cq} quality issue${cq === 1 ? "" : "s"} \xB7 ${projectDeltaCount} project diagnostic${projectDeltaCount === 1 ? "" : "s"}`;
  return {
    content: [{ type: "text", text: lines.join("\n") + summary }],
    details: {
      mode: "delta",
      actionableWarnings: aw,
      qualityIssues: cq,
      projectDiagnostics: projectDeltaCount
    }
  };
}
function createCurrentIgnoreFilter(cwd) {
  try {
    const matcher = getProjectIgnoreMatcher(cwd);
    return (filePath) => !matcher.isIgnored(path36.resolve(filePath), false);
  } catch {
    return () => true;
  }
}
function resolvePathsScope(cwd, rawPaths) {
  if (!Array.isArray(rawPaths) || rawPaths.length === 0)
    return void 0;
  if (rawPaths.length > MAX_PATHS_ENTRIES) {
    throw new Error(`paths has ${rawPaths.length} entries, over the ${MAX_PATHS_ENTRIES} cap. Narrow the list or omit paths to scan without a restriction.`);
  }
  const entries = rawPaths.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => path36.resolve(path36.isAbsolute(entry) ? entry : path36.resolve(cwd, entry)));
  const missing = [];
  const fileEntries = [];
  const existingEntries = [];
  const dirEntries = [];
  for (const entry of entries) {
    let stat;
    try {
      stat = fsSync.statSync(entry);
    } catch {
      missing.push(entry);
      fileEntries.push(entry);
      continue;
    }
    if (stat.isDirectory()) {
      dirEntries.push(entry);
    } else {
      fileEntries.push(entry);
      existingEntries.push(entry);
    }
  }
  const fileKeys = new Set(fileEntries.map((f) => normalizeFilePath(f)));
  const dirPrefixes = dirEntries.map((d) => normalizeFilePath(d));
  const includeExplicitFile = (filePath) => fileKeys.has(normalizeFilePath(path36.resolve(filePath)));
  const includeFile = (filePath) => {
    const key = normalizeFilePath(path36.resolve(filePath));
    if (fileKeys.has(key))
      return true;
    return dirPrefixes.some((prefix) => key === prefix || key.startsWith(`${prefix}/`));
  };
  return {
    includeFile,
    includeExplicitFile,
    missing,
    existingEntries,
    hasDirectoryEntries: dirEntries.length > 0
  };
}
function pathsScopeCacheOnlyNote(scope) {
  if (!scope)
    return "";
  return "\n\nNote: paths restricts this to cached findings for files pi-lens has already dispatched this session \u2014 mode=delta/all can't see files it hasn't touched. Use mode=full to actively scan exactly these paths.";
}
function pathsScopeMissingNote(scope) {
  if (!scope || scope.missing.length === 0)
    return "";
  const shown2 = scope.missing.slice(0, 10).map((p) => path36.basename(p));
  const more = scope.missing.length > shown2.length ? ` (+${scope.missing.length - shown2.length} more)` : "";
  return `

\u26A0 Skipped ${scope.missing.length} path(s) not found on disk: ${shown2.join(", ")}${more}`;
}
function createScopedFileFilter(cwd, pathsScope) {
  const ignoreFile = createCurrentIgnoreFilter(cwd);
  return (filePath) => {
    if (!pathsScope)
      return ignoreFile(filePath);
    if (!pathsScope.includeFile(filePath))
      return false;
    return pathsScope.includeExplicitFile(filePath) || ignoreFile(filePath);
  };
}
function filterProjectDiagnosticsSnapshot(snapshot, includeFile) {
  if (!snapshot)
    return void 0;
  return {
    ...snapshot,
    diagnostics: snapshot.diagnostics.filter((d) => includeFile(d.filePath))
  };
}
function filterProjectDiagnosticsDeltaReport(report, includeFile) {
  if (!report)
    return void 0;
  return {
    ...report,
    diagnostics: report.diagnostics.filter((d) => includeFile(d.filePath))
  };
}
function applyProjectRulePolicy(value, policyMap) {
  if (!value)
    return void 0;
  return {
    ...value,
    diagnostics: applyRulePolicy(value.diagnostics, policyMap)
  };
}
function isErrorLike(d) {
  if (d.stale)
    return false;
  return d.semantic === "blocking" || d.severity === "error";
}
function matchesSeverity(d, severity) {
  return matchesRecordSeverity(isErrorLike(d) ? "error" : d.severity, severity);
}
function matchesRecordSeverity(recordSeverity, requested) {
  if (requested === "all")
    return true;
  if (requested === "error")
    return recordSeverity === "error";
  if (requested === "warning")
    return recordSeverity === "error" || recordSeverity === "warning";
  if (requested === "information")
    return recordSeverity === "error" || recordSeverity === "warning" || recordSeverity === "info" || recordSeverity === "note" || recordSeverity === "help";
  if (requested === "hint")
    return recordSeverity === "error" || recordSeverity === "warning" || recordSeverity === "info" || recordSeverity === "note" || recordSeverity === "help" || recordSeverity === "hint";
  return true;
}
function severityRank(d) {
  if (d.semantic === "blocking")
    return 0;
  if (d.severity === "error")
    return 1;
  if (d.severity === "warning")
    return 2;
  return 3;
}
function bySeverityThenLine(a, b) {
  return severityRank(a) - severityRank(b) || (a.line ?? 0) - (b.line ?? 0);
}
function lspSeverityName(severity) {
  if (severity === 1)
    return "error";
  if (severity === 2)
    return "warning";
  if (severity === 3)
    return "info";
  return "hint";
}
function lspRuleId(diagnostic) {
  const code = diagnostic.code === void 0 ? void 0 : String(diagnostic.code);
  if (diagnostic.source && code)
    return `${diagnostic.source}:${code}`;
  return diagnostic.source ?? code ?? "lsp";
}
function lspDiagnosticToWidget(diagnostic) {
  const severity = lspSeverityName(diagnostic.severity);
  const rule = lspRuleId(diagnostic);
  return {
    severity,
    semantic: diagnostic.severity === 1 ? "blocking" : "warning",
    message: diagnostic.message,
    line: diagnostic.range.start.line + 1,
    col: diagnostic.range.start.character + 1,
    rule,
    // #3041: keep auxiliary provenance. The footer-reconcile loop above already
    // gives a swept aux finding its real tool id via `retagAuxiliaryDiagnostics`
    // (#692); this second conversion of the SAME raw diagnostics did not, and
    // dispositions are anchored by `tool` — so a `false-positive`/`suppress`
    // mark recorded against the per-edit `ast-grep` finding never matched the
    // `lsp`-labelled copy mode=full renders.
    tool: findAuxiliaryProfileForSource(diagnostic.source)?.tool ?? "lsp"
  };
}
function projectDiagnosticToWidget(diagnostic, filePath) {
  return {
    severity: diagnostic.severity,
    semantic: diagnostic.semantic,
    message: diagnostic.message,
    line: diagnostic.line,
    col: diagnostic.column,
    rule: diagnostic.rule ?? diagnostic.code,
    tool: diagnostic.runner || diagnostic.tool,
    uri: widgetDiagnosticUri(filePath, diagnostic.line, diagnostic.column),
    // #3600: carry the analyzer's own read stamp through the #1888
    // correlated commit, which otherwise re-stamps the row with the project
    // scan's `scannedAt` or the fold's `Date.now()`. Absent for the cheap
    // scan's rows, whose freshness is settled by a content fingerprint.
    ...diagnostic.observedAt !== void 0 && {
      observedAt: diagnostic.observedAt
    }
  };
}
function diagnosticDedupKey(filePath, diagnostic) {
  const ruleId = normalizeRuleId(diagnostic.rule ?? diagnostic.tool ?? "");
  return [path36.resolve(filePath), diagnostic.line ?? "?", ruleId].join(":");
}
function runnerIdOf(diagnostic) {
  return diagnostic.tool ?? diagnostic.rule?.split(":", 1)[0] ?? "";
}
function runnerRetirementDecision(diagnostic, filePath, authoritativeRunnerIds, authoritativeCoverage) {
  const runnerId = runnerIdOf(diagnostic);
  const coverage = (authoritativeCoverage ?? []).filter((entry) => entry.runnerId === runnerId);
  if (coverage.length === 0) {
    return authoritativeRunnerIds?.has(runnerId) ? "retire" : "keep";
  }
  const realFilePath = realpathOrResolve(filePath);
  for (const entry of coverage) {
    const root = entry.root;
    const relative9 = path36.relative(root, realFilePath);
    const underRoot = relative9 === "" || !relative9.startsWith("..") && !path36.isAbsolute(relative9);
    if (!underRoot)
      continue;
    if (entry.files.has(realFilePath)) {
      return "retire";
    }
  }
  return "keep";
}
function summarizeDiagnostics(filePath, diagnostics, hasFinalSnapshot) {
  let blocking = 0;
  let errors = 0;
  let warnings = 0;
  let advisories = 0;
  for (const diagnostic of diagnostics) {
    if (diagnostic.stale)
      continue;
    if (diagnostic.semantic === "blocking")
      blocking++;
    const tier = classifyDiagnosticTier(diagnostic);
    if (tier === "error")
      errors++;
    else if (tier === "warning")
      warnings++;
    else if (tier === "advisory")
      advisories++;
  }
  return {
    filePath,
    blocking,
    errors,
    warnings,
    advisories,
    hasFinalSnapshot,
    diagnostics
  };
}
var UNCONFIRMED_REASON_SENTENCE = {
  budget: "check didn't complete within budget",
  inconclusive: "check was inconclusive (notify-write or diagnostics wait timed out)",
  coverage_gap: "an auxiliary scanner coverage gap",
  service_destroyed: "the LSP service was reset mid-sweep",
  error: "check errored",
  // #1618 review round 2: the LSP layer considered this touch CONFIRMED —
  // the file's content changed under it (stale binding), which is a
  // completely different failure from a timeout. Must never collapse into
  // "within budget", the exact string this PR exists to stop misrendering.
  binding_mismatch: "the file changed on disk since this result was computed",
  // #2052: no server was asked. Say WHY and say it is permanent for this
  // path, so the reader does not read an empty result as "clean" or retry it.
  outside_project_root: "the file is outside every initialized session project root, so no language server was asked"
};
var UNCONFIRMED_REASON_COUNT_LABEL = {
  budget: "timed out",
  inconclusive: "inconclusive",
  coverage_gap: "coverage gap",
  service_destroyed: "service reset mid-sweep",
  error: "errored",
  binding_mismatch: "stale binding",
  outside_project_root: "outside project root"
};
function classifyUnconfirmedReason(result, mismatched) {
  if (mismatched.has(result))
    return "binding_mismatch";
  return result.unconfirmedReason ?? (result.error ? "error" : "budget");
}
function tallyUnconfirmedReasons(results, mismatched) {
  const tally = /* @__PURE__ */ new Map();
  for (const result of results) {
    const reason = classifyUnconfirmedReason(result, mismatched);
    tally.set(reason, (tally.get(reason) ?? 0) + 1);
  }
  return tally;
}
function formatUnconfirmedReasonClause(tally) {
  const entries = [...tally.entries()].filter(([, count]) => count > 0);
  if (entries.length === 0)
    return "";
  if (entries.length === 1) {
    return UNCONFIRMED_REASON_SENTENCE[entries[0][0]];
  }
  return entries.map(([reason, count]) => `${count} ${UNCONFIRMED_REASON_COUNT_LABEL[reason]}`).join(", ");
}
function tallyLspByPrimaryServer(confirmed, unconfirmed) {
  const byServer = /* @__PURE__ */ new Map();
  const bump = (filePath, wasConfirmed) => {
    const id = primaryServerId(filePath) ?? "unknown";
    const entry = byServer.get(id) ?? { confirmed: 0, total: 0 };
    entry.total += 1;
    if (wasConfirmed)
      entry.confirmed += 1;
    byServer.set(id, entry);
  };
  for (const result of confirmed)
    bump(result.filePath, true);
  for (const result of unconfirmed)
    bump(result.filePath, false);
  return byServer;
}
function formatLspServerBreakdown(byServer) {
  return [...byServer.entries()].sort((a, b) => {
    const rateA = a[1].confirmed / a[1].total;
    const rateB = b[1].confirmed / b[1].total;
    return rateA - rateB;
  }).map(([id, { confirmed, total }]) => `${id}: ${confirmed}/${total}`).join(", ");
}
function tallyLspPrimaryVsAuxiliary(results) {
  let primary = 0;
  let auxiliary = 0;
  for (const result of results) {
    const primaryId = primaryServerId(result.filePath);
    for (const diagnostic of result.diagnostics ?? []) {
      if (diagnostic.serverId === primaryId)
        primary += 1;
      else
        auxiliary += 1;
    }
  }
  return { primary, auxiliary };
}
function mergeDiagnosticsWithWidgetSummaries(widgetSummaries, lspResults, projectSnapshot, projectDelta, authoritativeLspFiles, authoritativeRunnerIds, authoritativeRunnerCoverage) {
  const byFile = /* @__PURE__ */ new Map();
  const seen = /* @__PURE__ */ new Set();
  for (const summary of widgetSummaries) {
    const filePath = path36.resolve(summary.filePath);
    if (authoritativeLspFiles?.has(filePath))
      continue;
    const retained = summary.diagnostics ?? [];
    const diagnostics = retained.filter((diagnostic) => runnerRetirementDecision(diagnostic, filePath, authoritativeRunnerIds, authoritativeRunnerCoverage) !== "retire").map((d) => ({ ...d }));
    byFile.set(
      filePath,
      // #2154 v4: when the filter retires rows, the stored tallies describe
      // diagnostics that are no longer delivered — full mode rendered
      // `main.go 1W … 1 warning` with no row under it. Recompute from what
      // survived, and only then (an untouched file keeps its own counts).
      diagnostics.length === retained.length ? { ...summary, filePath, diagnostics } : summarizeDiagnostics(filePath, diagnostics, summary.hasFinalSnapshot ?? true)
    );
    for (const diagnostic of diagnostics) {
      seen.add(diagnosticDedupKey(filePath, diagnostic));
    }
  }
  const addDiagnostic = (filePath, widgetDiagnostic) => {
    const existing = byFile.get(filePath);
    const diagnostics = existing ? [...existing.diagnostics] : [];
    const key = diagnosticDedupKey(filePath, widgetDiagnostic);
    if (seen.has(key))
      return;
    seen.add(key);
    diagnostics.push(widgetDiagnostic);
    byFile.set(filePath, summarizeDiagnostics(filePath, diagnostics, existing?.hasFinalSnapshot ?? true));
  };
  for (const result of lspResults) {
    const filePath = path36.resolve(result.filePath);
    for (const diagnostic of result.diagnostics ?? []) {
      addDiagnostic(filePath, {
        ...lspDiagnosticToWidget(diagnostic),
        ...result.observedAt !== void 0 && {
          observedAt: result.observedAt
        }
      });
    }
  }
  const deltaDiagnostics = projectDelta?.diagnostics ?? [];
  const deltaObservedAt = parseScannedAtMs(projectDelta?.generatedAt);
  if (deltaDiagnostics.length > 0 && deltaObservedAt === void 0) {
    recordDegradationOnce({
      kind: "project-delta-generatedat-unparseable",
      subject: String(projectDelta?.generatedAt),
      reason: "projectDelta rows fell back to the fold time because generatedAt could not be parsed"
    });
  }
  for (const diagnostic of projectSnapshot?.diagnostics ?? []) {
    addDiagnostic(path36.resolve(diagnostic.filePath), projectDiagnosticToWidget(diagnostic, diagnostic.filePath));
  }
  for (const diagnostic of deltaDiagnostics) {
    const widget = projectDiagnosticToWidget(diagnostic, diagnostic.filePath);
    const observedAt = widget.observedAt ?? deltaObservedAt;
    addDiagnostic(path36.resolve(diagnostic.filePath), {
      ...widget,
      ...observedAt !== void 0 && { observedAt }
    });
  }
  return [...byFile.values()];
}
async function applyInlineSuppressionsToSummaries(summaries, cwd, policyMap) {
  return Promise.all(summaries.map(async (summary) => {
    if (!summary.diagnostics.length)
      return summary;
    let content;
    try {
      content = await fs23.readFile(summary.filePath, "utf8");
    } catch {
      const policyKept2 = applyRulePolicy(summary.diagnostics, policyMap);
      return policyKept2.length === summary.diagnostics.length ? summary : summarizeDiagnostics(summary.filePath, policyKept2, summary.hasFinalSnapshot);
    }
    const { kept: policyKept } = applyFindingPolicy(summary.diagnostics, {
      cwd,
      filePath: summary.filePath,
      content,
      policyMap
    });
    for (const d of policyKept) {
      const { weak } = anchorsForDiagnostic(cwd, summary.filePath, d, content);
      if (getDisposition(cwd, weak)?.disposition === "flagged") {
        d.flagged = true;
      }
    }
    if (policyKept.length === summary.diagnostics.length)
      return summary;
    return summarizeDiagnostics(summary.filePath, policyKept, summary.hasFinalSnapshot);
  }));
}
function shouldUseCachedProjectDiagnostics(value) {
  return value === "cached";
}
function shouldRefreshProjectDiagnostics(value) {
  return value === "cheap" || value === "all";
}
function shouldIncludeProjectRunners(value) {
  return shouldRefreshProjectDiagnostics(value) || value === "cached";
}
function foldExtraDiagnosticsIntoSnapshot(snapshot, extra, runners, cwd) {
  if (extra.length === 0)
    return snapshot;
  if (snapshot) {
    const merged = /* @__PURE__ */ new Set([...snapshot.runners, ...runners]);
    return {
      ...snapshot,
      diagnostics: [...snapshot.diagnostics, ...extra],
      runners: [...merged]
    };
  }
  return {
    version: PROJECT_DIAGNOSTICS_CACHE_VERSION,
    cwd,
    tier: "all",
    scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
    diagnostics: extra,
    filesScanned: 0,
    runners
  };
}
async function getProjectDiagnosticsSnapshotForFullMode(cwd, options) {
  let loaded;
  if (shouldRefreshProjectDiagnostics(options.refreshRunners)) {
    loaded = await scanProjectDiagnostics({
      cwd,
      tier: "cheap",
      maxFiles: options.maxProjectFiles,
      signal: options.signal,
      files: options.files,
      includeGenerated: options.includeGenerated
    });
  } else if (shouldUseCachedProjectDiagnostics(options.refreshRunners)) {
    loaded = loadProjectDiagnosticsSnapshot(cwd);
  }
  if (!loaded)
    return void 0;
  const reconciled = reconcileProjectDiagnosticsSnapshot(loaded);
  if (reconciled.staleDropped > 0) {
    logLatency({
      type: "phase",
      toolName: "lens_diagnostics",
      filePath: cwd,
      phase: "project_snapshot_rows_retired",
      durationMs: 0,
      metadata: {
        files: reconciled.staleDropped,
        rows: loaded.diagnostics.length - reconciled.snapshot.diagnostics.length,
        scannedAt: loaded.scannedAt,
        // #3573: which arm retired them, now that both are reconciled.
        arm: shouldRefreshProjectDiagnostics(options.refreshRunners) ? "fresh" : "cached"
      }
    });
  }
  return reconciled.snapshot;
}
async function formatFullMode(cwd, severity, lspService, cacheManager2, options = {}) {
  const runWorkspaceDiagnostics = lspService.runWorkspaceDiagnostics;
  if (typeof runWorkspaceDiagnostics !== "function") {
    return {
      content: [
        {
          type: "text",
          text: "LSP service does not support project-wide workspace diagnostics."
        }
      ],
      details: { mode: "full", filesChecked: 0, lspUnavailable: true }
    };
  }
  const { signal, pathsScope, nextWriteIndex } = options;
  const host = options.host ?? "pi";
  const includeFile = createScopedFileFilter(cwd, pathsScope);
  const explicitFiles = pathsScope && !pathsScope.hasDirectoryEntries ? pathsScope.existingEntries : void 0;
  const projectRunnersRequested = shouldIncludeProjectRunners(options.refreshRunners);
  const analyzersPromise = projectRunnersRequested ? loadBootstrapClients().then((clients) => fetchFreshProjectDiagnostics(cacheManager2, cwd, clients, signal, {
    runtime: options.runtime,
    analysisRoot: options.analysisRoot
  })) : Promise.resolve({
    diagnostics: [],
    runners: [],
    analyzed: [],
    authoritativeCoverage: [],
    // #1623: every heavyweight analyzer is ELIGIBLE for this project but
    // this call never asked for it (refreshRunners wasn't cheap/all/
    // cached) — the expensive fetch below deliberately never runs in
    // that case (see the comment above), but rendering total silence
    // here reads exactly as "ran clean", the false-empty #1623 exists to
    // close. `ANALYZER_IDS` (fresh-fetch.ts) is the single source of
    // truth for which lanes this covers — reuse it rather than
    // hand-listing ids here.
    cold: [...ANALYZER_IDS],
    coldReasons: Object.fromEntries(ANALYZER_IDS.map((id) => [id, notRequestedReason(host)])),
    failed: [],
    timings: {}
  });
  const [rawLspResults, rawProjectSnapshot, extracted] = await Promise.all([
    runWorkspaceDiagnostics.call(lspService, cwd, {
      maxFiles: options.maxLspFiles,
      signal,
      onProgress: options.onProgress,
      onServerReady: options.onServerReady,
      nextWriteIndex: options.nextWriteIndex,
      files: explicitFiles
    }),
    getProjectDiagnosticsSnapshotForFullMode(cwd, {
      ...options,
      files: explicitFiles
    }),
    analyzersPromise
  ]);
  if (extracted.analysisRootError) {
    return {
      content: [{ type: "text", text: extracted.analysisRootError }],
      isError: true,
      details: {
        mode: "full",
        analysisRootError: extracted.analysisRootError,
        analysisRootValidation: extracted.analysisRootValidation
      }
    };
  }
  const aborted = signal?.aborted ?? false;
  const lspResults = await demoteInferredProjectSweepResults(
    rawLspResults.filter((result2) => includeFile(result2.filePath)),
    cwd,
    lspService,
    // #1645 review F1: the sweep that produced these results is signal-bounded,
    // so the probe loop that post-processes them must be too.
    signal
  );
  const mismatchedLspResults = await findFullScanBindingMismatches(lspResults);
  const confirmedLspResults = lspResults.filter((result2) => !result2.timedOut && !result2.error && !mismatchedLspResults.has(result2));
  const fullyCoveredLspResults = confirmedLspResults.filter((result2) => (result2.unconfirmedServerIds?.length ?? 0) === 0);
  const partiallyCoveredLspResults = confirmedLspResults.filter((result2) => (result2.unconfirmedServerIds?.length ?? 0) > 0);
  const unconfirmedLspResults = lspResults.filter((result2) => result2.timedOut || result2.error || mismatchedLspResults.has(result2));
  const authoritativeLspFiles = /* @__PURE__ */ new Set();
  const unreconciledFiles = /* @__PURE__ */ new Set();
  for (const result2 of fullyCoveredLspResults) {
    try {
      const diagnostics = convertLspDiagnostics(result2.diagnostics, result2.filePath, { scanOrigin: "lens_diagnostics_full" });
      const retagged = retagAuxiliaryDiagnostics(diagnostics, result2.diagnostics, "", { cwd, fileRole: detectFileRole(result2.filePath) });
      const retired = reconcileScanDiagnostics(
        result2.filePath,
        retagged,
        true,
        // The service reserves this token when the file enters the scan. The
        // fallback preserves compatibility with older test doubles/services.
        result2.writeIndex ?? nextWriteIndex?.(),
        // #1093: for a result served from the workspace-diagnostics cache,
        // `observedAt` is when it was scanned, not now(), so a mode=full
        // that only re-serves the cache can't keep a resolved finding on
        // screen by re-arming the mtime gate. #3573: for a fresh result it
        // is the sweep's read of the file.
        result2.observedAt
      );
      if (retired === true) {
        authoritativeLspFiles.add(path36.resolve(result2.filePath));
      } else {
        unreconciledFiles.add(path36.resolve(result2.filePath));
        recordDegradationOnce({
          kind: "diagnostic-retained-unreconciled",
          subject: `${path36.resolve(result2.filePath)}:lsp`,
          reason: "confirmed diagnostic result could not replace retained widget state"
        });
      }
    } catch {
      unreconciledFiles.add(path36.resolve(result2.filePath));
      recordDegradationOnce({
        kind: "diagnostic-retained-unreconciled",
        subject: `${path36.resolve(result2.filePath)}:lsp`,
        reason: "confirmed diagnostic result failed during widget reconciliation"
      });
    }
  }
  const policyMap = loadProjectRulePolicyMap(cwd);
  const scannedSnapshot = filterProjectDiagnosticsSnapshot(rawProjectSnapshot, includeFile);
  const authoritativeRunnerIds = new Set(extracted.analyzed ?? []);
  const authoritativeRunnerCoverage = extracted.authoritativeCoverage ?? [];
  const foldedProjectSnapshot = foldExtraDiagnosticsIntoSnapshot(scannedSnapshot, extracted.diagnostics.filter((d) => includeFile(d.filePath)), extracted.runners, cwd);
  const projectSnapshot = applyProjectRulePolicy(foldedProjectSnapshot, policyMap);
  const projectDelta = applyProjectRulePolicy(filterProjectDiagnosticsDeltaReport(loadProjectDiagnosticsDeltaReport(cwd), includeFile), policyMap);
  const authoritativeRetiredCount = getFileDiagnosticSummaries().filter((summary) => includeFile(summary.filePath) && authoritativeLspFiles.has(path36.resolve(summary.filePath)) && (summary.diagnostics?.length ?? 0) > 0).length;
  if (authoritativeRetiredCount > 0) {
    logLatency({
      type: "phase",
      phase: "lsp_authoritative_widget_retire",
      filePath: "",
      durationMs: 0,
      metadata: { files: authoritativeRetiredCount }
    });
  }
  const retiredRunnerCounts = /* @__PURE__ */ new Map();
  const runnerRetiredRows = getFileDiagnosticSummaries().filter((summary) => includeFile(summary.filePath)).reduce((total, summary) => total + (summary.diagnostics ?? []).filter((diagnostic) => {
    const retired = runnerRetirementDecision(diagnostic, summary.filePath, authoritativeRunnerIds, authoritativeRunnerCoverage) === "retire";
    if (retired) {
      const runnerId = runnerIdOf(diagnostic);
      retiredRunnerCounts.set(runnerId, (retiredRunnerCounts.get(runnerId) ?? 0) + 1);
    }
    return retired;
  }).length, 0);
  if (runnerRetiredRows > 0) {
    logLatency({
      type: "phase",
      phase: "runner_authoritative_widget_retire",
      filePath: "",
      durationMs: 0,
      metadata: {
        rows: runnerRetiredRows,
        runners: [...retiredRunnerCounts.keys()].join(","),
        coverage: authoritativeRunnerCoverage.map((entry) => ({
          runnerId: entry.runnerId,
          root: entry.root,
          files: entry.files.size
        }))
      }
    });
  }
  for (const entry of authoritativeRunnerCoverage) {
    if (entry.files.size === 0)
      continue;
    if (!claimPhaseOncePerSession("runner_coverage_retired", entry.runnerId)) {
      continue;
    }
    logLatency({
      type: "phase",
      phase: "runner_coverage_retired",
      filePath: "",
      durationMs: 0,
      metadata: {
        runnerId: entry.runnerId,
        count: retiredRunnerCounts.get(entry.runnerId) ?? 0
      }
    });
  }
  let summaries = await applyInlineSuppressionsToSummaries(mergeDiagnosticsWithWidgetSummaries(getFileDiagnosticSummaries().filter((summary) => includeFile(summary.filePath)), confirmedLspResults, projectSnapshot, projectDelta, authoritativeLspFiles, authoritativeRunnerIds, authoritativeRunnerCoverage), cwd, policyMap);
  if (unreconciledFiles.size > 0) {
    summaries = summaries.map((summary) => unreconciledFiles.has(path36.resolve(summary.filePath)) ? summarizeDiagnostics(summary.filePath, markUnreconciledFindings(summary.diagnostics), summary.hasFinalSnapshot) : summary);
  }
  const parsedProjectScanObservedAt = projectSnapshot?.scannedAt ? Date.parse(projectSnapshot.scannedAt) : void 0;
  const projectScanObservedAt = parsedProjectScanObservedAt !== void 0 && Number.isFinite(parsedProjectScanObservedAt) ? parsedProjectScanObservedAt : void 0;
  for (const summary of summaries) {
    reconcileCorrelatedScanDiagnostics(summary.filePath, summary.diagnostics, nextWriteIndex?.(), projectScanObservedAt);
  }
  const result = await formatAllMode(cwd, severity, summaries, {
    mode: "full",
    lspFilesChecked: rawLspResults.length,
    partial: aborted,
    projectDiagnostics: projectSnapshot === void 0 ? void 0 : {
      tier: projectSnapshot.tier,
      filesScanned: projectSnapshot.filesScanned,
      diagnostics: projectSnapshot.diagnostics.length,
      runners: projectSnapshot.runners
    },
    projectDiagnosticsDelta: projectDelta === void 0 ? void 0 : {
      diagnostics: projectDelta.diagnostics.length,
      sources: projectDelta.sources,
      turnIndex: projectDelta.turnIndex
    }
  }, 0, pathsScope);
  const missingNote = pathsScopeMissingNote(pathsScope);
  const unconfirmedByReason = tallyUnconfirmedReasons(unconfirmedLspResults, mismatchedLspResults);
  const lspServerBreakdown = tallyLspByPrimaryServer(confirmedLspResults, unconfirmedLspResults);
  const serverBreakdownClause = lspServerBreakdown.size > 1 ? ` \u2014 by server: ${formatLspServerBreakdown(lspServerBreakdown)}.` : "";
  const unconfirmedLspNote = unconfirmedLspResults.length > 0 ? `

\u26A0 LSP sweep: ${confirmedLspResults.length} file(s) confirmed via LSP, ${unconfirmedLspResults.length} unconfirmed (${formatUnconfirmedReasonClause(unconfirmedByReason)})${serverBreakdownClause} \u2014 NOT the same as 0 diagnostics for: ${unconfirmedLspResults.slice(0, 20).map((result2) => result2.filePath).join(", ")}${unconfirmedLspResults.length > 20 ? ", \u2026" : ""}. These files' LSP contribution is excluded from this result; re-run mode=full to retry them (they may still show findings above from cached/project-runner state).` : "";
  const uncoveredScannerIds = [
    ...new Set(partiallyCoveredLspResults.flatMap((result2) => result2.unconfirmedServerIds ?? []))
  ].sort((a, b) => Number(a > b) - Number(a < b));
  const auxiliaryCoverageNote = uncoveredScannerIds.length > 0 ? `

\u26A0 Auxiliary coverage incomplete: ${uncoveredScannerIds.join(", ")} did not answer for ${partiallyCoveredLspResults.length} file(s). Findings from answering servers are included; this is not a clean verdict for the named scanner lane(s).` : "";
  const lspPrimaryVsAuxiliary = tallyLspPrimaryVsAuxiliary(confirmedLspResults);
  logExtension({
    subsystem: "lsp-diagnostics",
    message: "lens_diagnostics verdict",
    metadata: {
      server: [
        ...new Set(confirmedLspResults.map((result2) => primaryServerId(result2.filePath) ?? "unknown"))
      ].join(","),
      primary: lspPrimaryVsAuxiliary.primary,
      auxiliary: lspPrimaryVsAuxiliary.auxiliary,
      total: lspPrimaryVsAuxiliary.primary + lspPrimaryVsAuxiliary.auxiliary,
      sources: [
        ...new Set(confirmedLspResults.flatMap((result2) => (result2.diagnostics ?? []).map((diagnostic) => diagnostic.source ?? "unknown")))
      ].slice(0, 5)
    }
  });
  const lspPrimaryVsAuxiliaryNote = lspPrimaryVsAuxiliary.primary + lspPrimaryVsAuxiliary.auxiliary > 0 ? `

LSP sweep findings: ${lspPrimaryVsAuxiliary.primary} primary (language server), ${lspPrimaryVsAuxiliary.auxiliary} auxiliary (ast-grep/opengrep/zizmor/typos/marksman/...).` : "";
  const abortedIds = new Set(extracted.abortedIds ?? []);
  const genuinelyColdIds = extracted.cold.filter((id) => !abortedIds.has(id));
  const partialIds = extracted.partial ?? [];
  const coldNote = extracted.unsafeRoot ? `

heavyweight analyzers skipped: the analysis root resolves at or above the home directory, so a fresh knip/jscpd/madge/gitleaks/govulncheck/trivy/dead-code scan would walk every unrelated tree under it. Supply a project directory below the home ceiling. Absence of their findings is NOT a clean verdict.` : extracted.analysisRootError ? `

heavyweight analyzers skipped: ${extracted.analysisRootError}. Supply an existing project directory below the home ceiling. Absence of their findings is NOT a clean verdict.` : !projectRunnersRequested && genuinelyColdIds.length > 0 ? `

not run this call (quick mode): ${genuinelyColdIds.join(", ")}. ${notRequestedReason(host)}. Absence of their findings is NOT a clean verdict.` : genuinelyColdIds.length > 0 ? `

cold (not applicable / unavailable this run): ${genuinelyColdIds.map((id) => formatNotRunEntry(id, extracted.coldReasons)).join(", ")}. These analyzers have not contributed to this result \u2014 absence of their findings is NOT a clean verdict.` : "";
  const partialNote = partialIds.length > 0 ? `

\u26A0 partial coverage (findings included): ${partialIds.map((id) => `${id} \u2014 ${extracted.partialReasons?.[id] ?? "coverage is incomplete"}`).join(", ")}. Coverage is incomplete, so absence of findings is NOT a clean verdict.` : "";
  const testRunnerEditScopedNote = extracted.runners.includes("test-runner") ? `

test-runner coverage is edit-scoped, NOT a full-project run: its findings only reflect the test file(s) touched by the most recent edit's turn_end fire, not a fresh whole-project test suite run (mode=full never re-runs the suite \u2014 see fresh-fetch.ts). Absence of test-runner findings elsewhere in the project is NOT a clean verdict, and presence here does not mean "just checked" \u2014 it may be from several turns ago.` : "";
  const failedAnalyzers = extracted.failed ?? [];
  const failedNote = failedAnalyzers.length > 0 ? `

failed (ran, but no trustworthy result): ${failedAnalyzers.map(({ id, summary }) => `${id} \u2014 ${summary}`).join(", ")}. These analyzers were not cached and will be retried; absence of their findings is NOT a clean verdict.` : "";
  const dispositionSuppressedByLaneText = Object.entries(extracted.dispositionSuppressedByLane ?? {}).map(([id, count]) => `${id} ${count}`).join(", ");
  const dispositionSuppressedNote = (extracted.dispositionSuppressed ?? 0) > 0 ? `

suppressed by disposition: ${extracted.dispositionSuppressed} finding(s) dropped from this result because they're marked false-positive or won't-fix (${dispositionSuppressedByLaneText}). Not a clean verdict for those locations \u2014 they were found, then intentionally hidden.` : "";
  const walkUnsafeRoot = explicitFiles === void 0 && isAtOrAboveHomeDir(cwd);
  const walkUnsafeRootNote = walkUnsafeRoot ? `

project file walk skipped: the working directory resolves at or above the home directory, so the cheap project scan and the LSP workspace sweep would each enumerate every unrelated tree under it. Re-run from inside a project directory. Absence of their findings is NOT a clean verdict.` : "";
  const scanTruncatedNoticeText = projectSnapshot ? scanTruncationNotice(projectSnapshot) : void 0;
  const scanTruncatedNote = scanTruncatedNoticeText ? `

${scanTruncatedNoticeText}` : "";
  const generatedSkipNoticeText = projectSnapshot ? generatedSkipNotice(projectSnapshot, host) : void 0;
  const generatedSkipNote = generatedSkipNoticeText ? `

${generatedSkipNoticeText}` : "";
  const cheapScanScannedAtMs = rawProjectSnapshot?.scannedAt ? Date.parse(rawProjectSnapshot.scannedAt) : void 0;
  const cheapScanStatusNote = !projectRunnersRequested ? `

cheap project scan (tree-sitter/fact-rules/ast-grep): not run this call (${notRequestedReason(host)}).` : options.refreshRunners === "cached" ? cheapScanScannedAtMs !== void 0 && Number.isFinite(cheapScanScannedAtMs) ? `

cheap project scan (tree-sitter/fact-rules/ast-grep): served from cache, ${formatCacheAgeOld(Date.now() - cheapScanScannedAtMs)} \u2014 not re-run this call.` : `

cheap project scan (tree-sitter/fact-rules/ast-grep): not run (no cached scan; refresh with refreshRunners=cheap/all to populate).` : "";
  const abortedNote = abortedIds.size > 0 ? `

stopped mid-scan (still running in the background, not reflected in this result): ${[
    ...abortedIds
  ].join(", ")}. Not a clean verdict for these \u2014 re-run mode=full to pick up their result once it's cached.` : "";
  const cachedAgeMs = extracted.cachedAgeMs ?? {};
  const timingEntries = Object.entries(extracted.timings ?? {}).filter(([id]) => !(id in cachedAgeMs));
  const freshNote = timingEntries.length > 0 ? `

fetched fresh this call: ${timingEntries.map(([id, ms]) => `${id} (${Math.round(ms)}ms)`).join(", ")}.` : "";
  const cachedAgeEntries = Object.entries(cachedAgeMs);
  const cachedAgeNote = cachedAgeEntries.length > 0 ? `

served from cache this call (not re-run): ${cachedAgeEntries.map(([id, ms]) => `${id} (${formatCacheAgeOld(ms)})`).join(", ")}.` : "";
  const resultWithCold = {
    ...result,
    details: {
      ...result.details,
      coldRunners: extracted.cold,
      partialRunners: partialIds,
      // #1623: the specific reason each cold id was skipped (single source
      // of truth: extractors.ts's `formatNotRunEntry` renders the same map
      // into the text note above) — lets a caller check WHY without parsing
      // text.
      coldReasons: extracted.coldReasons ?? {},
      failedAnalyzers,
      analyzerTimingsMs: extracted.timings,
      dispositionSuppressed: extracted.dispositionSuppressed ?? 0,
      dispositionSuppressedByLane: extracted.dispositionSuppressedByLane ?? {},
      // #1623: ms-old each cache-read-by-design lane's data was (today, only
      // test-runner) — lets a caller distinguish "just ran" from "served
      // from cache" without parsing the text note.
      analyzersCachedAgeMs: cachedAgeMs,
      analyzersAborted: extracted.aborted ?? false,
      analyzersAbortedIds: extracted.abortedIds ?? [],
      // #747: true when the fresh fetch refused an at-or-above-$HOME root —
      // lets a caller distinguish "skipped for safety" from per-analyzer
      // cold reasons without parsing the text note.
      // This is the fetch seam's explicit-root policy. The cwd walk below is
      // intentionally separate: it answers a different root question.
      analyzersUnsafeRoot: extracted.analysisRootValidation?.state === "unsafe" || extracted.unsafeRoot === true,
      analysisRootValidation: extracted.analysisRootValidation,
      // #747: true when the cwd resolved at/above $HOME so the cheap project
      // scan and the LSP workspace sweep both refused to walk — lets a caller
      // distinguish "walked nothing for safety" from a genuinely clean sweep.
      projectWalkUnsafeRoot: walkUnsafeRoot,
      // #630: confirmed/unconfirmed LSP-sweep tally, mirroring
      // `lsp_diagnostics`' confirmation state — lets a caller check "were
      // any files unconfirmed" without re-deriving it from the text.
      lspFilesConfirmed: confirmedLspResults.length,
      lspFilesUnconfirmed: unconfirmedLspResults.length,
      lspFilesPartiallyCovered: partiallyCoveredLspResults.length,
      unconfirmedLspServerIds: uncoveredScannerIds,
      unconfirmedLspFiles: unconfirmedLspResults.map((result2) => result2.filePath),
      // #646: per-primary-server breakdown of the same tally, plus the
      // primary/auxiliary findings split — mirrors `lsp_diagnostics`'
      // per-server reporting granularity (always present, even when every
      // server confirmed cleanly, so a caller can check it unconditionally).
      lspServerBreakdown: Object.fromEntries(lspServerBreakdown),
      lspPrimaryDiagnosticsCount: lspPrimaryVsAuxiliary.primary,
      lspAuxiliaryDiagnosticsCount: lspPrimaryVsAuxiliary.auxiliary,
      // #784: lets a caller check "was the cheap project scan truncated"
      // without parsing the text note.
      projectScanTruncated: projectSnapshot?.scanTruncated ?? false,
      // #1107 phase 2: same machine-readable convention, for the
      // generated-name/dir skip counters.
      ...projectSnapshot?.generatedFileSkips ? { projectGeneratedFileSkips: projectSnapshot.generatedFileSkips } : {},
      ...projectSnapshot?.generatedNameOnlySkips ? {
        projectGeneratedNameOnlySkips: projectSnapshot.generatedNameOnlySkips
      } : {},
      ...projectSnapshot?.generatedDirSkips ? { projectGeneratedDirSkips: projectSnapshot.generatedDirSkips } : {}
    }
  };
  if (aborted) {
    const timedOut = signal?.reason?.name === "TimeoutError";
    const note = timedOut ? `

\u26A0 Scan exceeded its ${Math.round((options.wallClockMs ?? 0) / 1e3)}s time budget and was stopped \u2014 results are partial. Narrow it with maxLspFiles, or raise PI_LENS_LENS_DIAGNOSTICS_FULL_TIMEOUT_MS.` : "\n\n\u26A0 Scan cancelled before completion \u2014 results are partial. Re-run with a smaller maxLspFiles to finish within budget.";
    return {
      content: [
        {
          type: "text",
          text: result.content[0].text + note + unconfirmedLspNote + auxiliaryCoverageNote + lspPrimaryVsAuxiliaryNote + partialNote + coldNote + testRunnerEditScopedNote + failedNote + dispositionSuppressedNote + walkUnsafeRootNote + scanTruncatedNote + generatedSkipNote + abortedNote + freshNote + cachedAgeNote + cheapScanStatusNote + missingNote
        }
      ],
      details: { ...resultWithCold.details, timedOut }
    };
  }
  if (missingNote || coldNote || partialNote || testRunnerEditScopedNote || failedNote || dispositionSuppressedNote || walkUnsafeRootNote || scanTruncatedNote || generatedSkipNote || abortedNote || freshNote || cachedAgeNote || cheapScanStatusNote || unconfirmedLspNote || auxiliaryCoverageNote || lspPrimaryVsAuxiliaryNote) {
    return {
      content: [
        {
          type: "text",
          text: result.content[0].text + unconfirmedLspNote + auxiliaryCoverageNote + lspPrimaryVsAuxiliaryNote + partialNote + coldNote + testRunnerEditScopedNote + failedNote + dispositionSuppressedNote + walkUnsafeRootNote + scanTruncatedNote + generatedSkipNote + abortedNote + freshNote + cachedAgeNote + cheapScanStatusNote + missingNote
        }
      ],
      details: resultWithCold.details
    };
  }
  return resultWithCold;
}
async function projectResolvedCwd(summary, cwd) {
  const resolvedCwd = await resolveLspCwdForFile(summary.filePath, cwd);
  return {
    ...summary,
    ...resolvedCwd === void 0 ? {} : { resolvedCwd },
    diagnostics: summary.diagnostics ?? []
  };
}
async function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(), detailOverrides = { mode: "all" }, staleDropped = 0, pathsScope, dependencyDemoted = 0) {
  const footerCapped = summaries.reduce((n, s) => n + (s.diagnostics ?? []).filter((d) => d.footerRetired === true).length, 0);
  const staleNote = (staleDropped > 0 ? ` (${staleDropped} changed file${staleDropped === 1 ? "" : "s"} omitted as stale \u2014 use mode=full to rescan)` : "") + (dependencyDemoted > 0 ? ` (${dependencyDemoted} blocking finding${dependencyDemoted === 1 ? "" : "s"} demoted to [stale] \u2014 an imported file changed since; re-run to confirm)` : "") + (footerCapped > 0 ? ` (${footerCapped} demoted finding${footerCapped === 1 ? "" : "s"} capped after ${DEPENDENCY_DRIFT_MAX_DELIVERIES} unconfirmed deliveries \u2014 no longer shown in the pi-lens footer, still listed here; re-run to confirm)` : "");
  const isFullMode = detailOverrides.mode === "full";
  const includeFile = createScopedFileFilter(cwd, pathsScope);
  const policyMap = loadProjectRulePolicyMap(cwd);
  const dispositioned = isFullMode ? summaries : summaries.map((s) => {
    const diagnostics = s.diagnostics ?? [];
    const kept = applyCachedDispositions(diagnostics, cwd, s.filePath);
    const policyKept = applyRulePolicy(kept, policyMap);
    if (policyKept.length === kept.length && kept.length === diagnostics.length)
      return s;
    return summarizeDiagnostics(s.filePath, policyKept, s.hasFinalSnapshot);
  });
  const eofGated = dispositioned.map((s) => {
    const { diagnostics, demotedCount } = demotePastEofDiagnostics({
      store: "lens_diagnostics",
      cwd,
      filePath: s.filePath,
      diagnostics: s.diagnostics ?? [],
      resync: resyncDocumentOnPastEof
    });
    if (demotedCount === 0)
      return s;
    return summarizeDiagnostics(s.filePath, diagnostics, s.hasFinalSnapshot);
  });
  const visibleSummaries = eofGated.filter((s) => includeFile(s.filePath));
  const withIssues = visibleSummaries.filter((s) => {
    if (severity === "error")
      return s.blocking > 0 || s.errors > 0;
    if (severity === "warning")
      return s.blocking > 0 || s.errors > 0 || s.warnings > 0;
    return s.blocking > 0 || s.errors > 0 || s.warnings > 0 || s.advisories > 0 || (s.diagnostics ?? []).some((d) => d.stale);
  });
  const pathsNote = isFullMode ? "" : pathsScopeCacheOnlyNote(pathsScope);
  if (withIssues.length === 0) {
    const text = (visibleSummaries.length === 0 ? "No files diagnosed yet this session." : `No ${severity === "all" ? "" : severity + " "}issues across ${visibleSummaries.length} file${visibleSummaries.length === 1 ? "" : "s"} diagnosed this session. \u2713`) + staleNote + pathsNote;
    return {
      content: [{ type: "text", text }],
      details: {
        ...detailOverrides,
        filesChecked: visibleSummaries.length,
        staleDropped
      }
    };
  }
  const sorted = withIssues.sort((a, b) => b.blocking - a.blocking || b.errors - a.errors || b.warnings - a.warnings);
  const rendered = await Promise.all(sorted.map((summary2) => projectResolvedCwd(summary2, cwd)));
  const lines = [];
  let totalBlocking = 0;
  let totalErrors = 0;
  let totalWarnings = 0;
  let totalAdvisories = 0;
  for (const s of rendered) {
    const rel = path36.relative(cwd, s.filePath);
    const parts = [];
    if (s.blocking > 0)
      parts.push(`\u{1F534} ${s.blocking} blocking`);
    if (s.errors > 0 && s.blocking === 0)
      parts.push(`${s.errors}E`);
    if (s.warnings > 0)
      parts.push(`${s.warnings}W`);
    if (s.advisories > 0)
      parts.push(`${s.advisories} hint${s.advisories === 1 ? "" : "s"}`);
    if (!s.hasFinalSnapshot)
      parts.push(`(pending)`);
    const staleCount = (s.diagnostics ?? []).filter((d) => d.stale).length;
    if (staleCount > 0) {
      parts.push(`${staleCount} stale \u2014 re-run to confirm`);
    }
    if (s.resolvedCwd !== void 0)
      parts.push(`cwd=${s.resolvedCwd}`);
    lines.push(`${rel}  ${parts.join("  ")}`);
    const matching = (s.diagnostics ?? []).filter((d) => matchesSeverity(d, severity)).sort(bySeverityThenLine);
    const shown2 = matching.slice(0, MAX_DIAGNOSTICS_PER_FILE);
    for (const d of shown2) {
      const marker = d.stale ? "" : isErrorLike(d) ? d.semantic === "blocking" ? "\u{1F534} " : "" : "";
      const label = d.rule ?? d.tool;
      const tag = label ? ` [${label}]` : "";
      const flaggedTag = d.flagged ? " \u{1F4CC} flagged-to-fix" : "";
      const msg = d.message.replace(/\s+/g, " ").trim();
      const pastEof = d.stale && (d.staleReason ?? "past-eof") === "past-eof";
      const loc = pastEof ? PAST_EOF_STALE_MARKER : `L${d.line ?? "?"}`;
      const staleTag = d.stale && !pastEof ? ` ${STALE_LINE_MARKER}` : "";
      lines.push(`  ${marker}${loc}: ${msg}${tag}${flaggedTag}${staleTag}`);
    }
    if (matching.length > shown2.length) {
      lines.push(`  \u2026 ${matching.length - shown2.length} more in this file (showing ${shown2.length} of ${matching.length})`);
    }
    totalBlocking += s.blocking;
    totalErrors += s.errors;
    totalWarnings += s.warnings;
    totalAdvisories += s.advisories;
  }
  const summary = [
    `
Summary (${visibleSummaries.length} files diagnosed this session):`,
    totalBlocking > 0 ? `  \u{1F534} ${totalBlocking} blocking error${totalBlocking === 1 ? "" : "s"}` : null,
    totalBlocking === 0 && totalErrors > 0 ? `  ${totalErrors} error${totalErrors === 1 ? "" : "s"}` : null,
    totalWarnings > 0 ? `  ${totalWarnings} warning${totalWarnings === 1 ? "" : "s"}` : null,
    // #2414: reported separately from `warnings` — hint/info are style
    // opinions, not defects, and must not read as unresolved code issues.
    totalAdvisories > 0 ? `  ${totalAdvisories} hint/info (style \u2014 not counted as warnings)` : null
  ].filter(Boolean).join("\n");
  return {
    content: [
      { type: "text", text: lines.join("\n") + summary + staleNote }
    ],
    details: {
      ...detailOverrides,
      filesWithIssues: withIssues.length,
      totalBlocking,
      totalErrors,
      totalWarnings,
      totalAdvisories,
      staleDropped
    }
  };
}

// dist/tools/lsp-navigation.js
import * as nodeFs2 from "node:fs";
import * as path37 from "node:path";
import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "node:url";

// dist/tools/lsp-structured-output.js
import { fileURLToPath, pathToFileURL } from "node:url";
function asRecord(value) {
  return typeof value === "object" && value !== null ? value : void 0;
}
function finiteNumber(value) {
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
}
function lspStatusFromFailureKind(failureKind) {
  if (failureKind === "success" || failureKind === "fallback_success") {
    return "success";
  }
  if (failureKind === "empty_result")
    return "empty";
  if (failureKind === "unsupported")
    return "unsupported";
  if (failureKind === "bad_input" || failureKind === "invalid_operation" || failureKind === "missing_file_path") {
    return "bad_input";
  }
  if (failureKind === "no_server")
    return "no_server";
  if (failureKind === "lsp_disabled")
    return "lsp_disabled";
  if (failureKind === "filepath_is_directory")
    return "filepath_is_directory";
  if (failureKind === "tracked_snapshot")
    return "tracked_snapshot";
  return "error";
}
function lspToolLocationFromUriRange(uri, range) {
  if (typeof uri !== "string" || !uri.startsWith("file:"))
    return void 0;
  const rangeLike = asRecord(range);
  const startLine = finiteNumber(rangeLike?.start?.line);
  const startCharacter = finiteNumber(rangeLike?.start?.character);
  const endLine = finiteNumber(rangeLike?.end?.line);
  const endCharacter = finiteNumber(rangeLike?.end?.character);
  if (startLine === void 0 || startCharacter === void 0 || endLine === void 0 || endCharacter === void 0) {
    return void 0;
  }
  try {
    return {
      uri,
      filePath: fileURLToPath(uri),
      // LLM-facing coordinates: 1-based, matching editor/tool params.
      range: {
        start: {
          line: Math.floor(startLine) + 1,
          character: Math.floor(startCharacter) + 1
        },
        end: {
          line: Math.floor(endLine) + 1,
          character: Math.floor(endCharacter) + 1
        }
      }
    };
  } catch {
    return void 0;
  }
}
function lspToolLocationFromFileRange(filePath, range) {
  if (!filePath || filePath === "(workspace)")
    return void 0;
  const rangeLike = asRecord(range);
  const startLine = finiteNumber(rangeLike?.start?.line);
  const startCharacter = finiteNumber(rangeLike?.start?.character);
  const endLine = finiteNumber(rangeLike?.end?.line);
  const endCharacter = finiteNumber(rangeLike?.end?.character);
  if (startLine === void 0 || startCharacter === void 0 || endLine === void 0 || endCharacter === void 0) {
    return void 0;
  }
  return {
    uri: pathToFileURL(filePath).href,
    filePath,
    range: {
      start: {
        line: Math.floor(startLine) + 1,
        character: Math.floor(startCharacter) + 1
      },
      end: {
        line: Math.floor(endLine) + 1,
        character: Math.floor(endCharacter) + 1
      }
    }
  };
}
function pushToolLocation(out, uri, range) {
  const loc = lspToolLocationFromUriRange(uri, range);
  if (loc)
    out.push(loc);
}
function collectLocationSummaries(result, filePath) {
  const out = [];
  for (const entry of Array.isArray(result) ? result : [result]) {
    const record = asRecord(entry);
    if (!record)
      continue;
    pushToolLocation(out, record.uri, record.range);
    pushToolLocation(out, record.targetUri, record.targetSelectionRange ?? record.targetRange);
    if (!record.uri && !record.targetUri) {
      const local = lspToolLocationFromFileRange(filePath, record.range);
      let alreadyIncluded = false;
      for (const loc of out) {
        if (loc.uri === local?.uri) {
          alreadyIncluded = true;
          break;
        }
      }
      if (local && !alreadyIncluded)
        out.push(local);
    }
  }
  return out;
}
function collectWorkspaceSymbolLocationSummaries(result) {
  const out = [];
  for (const entry of Array.isArray(result) ? result : [result]) {
    const symbol = asRecord(entry);
    const location = asRecord(symbol?.location);
    if (!location)
      continue;
    pushToolLocation(out, location.uri, location.range);
    pushToolLocation(out, location.targetUri, location.targetSelectionRange ?? location.targetRange);
  }
  return out;
}
function collectCallHierarchyLocationSummaries(result, operation) {
  const out = [];
  for (const entry of Array.isArray(result) ? result : [result]) {
    const record = asRecord(entry);
    const item = asRecord(operation === "incomingCalls" ? record?.from : record?.to);
    pushToolLocation(out, item?.uri, item?.selectionRange ?? item?.range);
  }
  return out;
}
function collectLspToolLocationsForOperation(operation, result, filePath) {
  if ([
    "definition",
    "typeDefinition",
    "declaration",
    "references",
    "implementation",
    "prepareCallHierarchy"
  ].includes(operation)) {
    return collectLocationSummaries(result, filePath);
  }
  if (operation === "workspaceSymbol") {
    return collectWorkspaceSymbolLocationSummaries(result);
  }
  if (operation === "documentSymbol" || operation === "findSymbol") {
    return collectLocationSummaries(result, filePath);
  }
  if (operation === "incomingCalls" || operation === "outgoingCalls") {
    return collectCallHierarchyLocationSummaries(result, operation);
  }
  return [];
}
function parseBalancedJsonPrefix(text) {
  const trimmed = text.trim();
  const start = trimmed.search(/[[{]/);
  if (start < 0)
    return void 0;
  const open = trimmed[start];
  const close = open === "[" ? "]" : "}";
  let depth = 0;
  let inString = false;
  let escaped = false;
  for (let i = start; i < trimmed.length; i += 1) {
    const ch = trimmed[i];
    if (inString) {
      if (escaped)
        escaped = false;
      else if (ch === "\\")
        escaped = true;
      else if (ch === '"')
        inString = false;
      continue;
    }
    if (ch === '"') {
      inString = true;
      continue;
    }
    if (ch === open)
      depth += 1;
    if (ch === close)
      depth -= 1;
    if (depth === 0) {
      try {
        return {
          value: JSON.parse(trimmed.slice(start, i + 1)),
          rest: trimmed.slice(i + 1).trim()
        };
      } catch {
        return void 0;
      }
    }
  }
  return void 0;
}
function parseLspNavigationTextPayload(text) {
  const notes = [];
  const hints = [];
  const errors = [];
  const trimmed = text.trim();
  if (!trimmed)
    return { notes, hints, errors };
  try {
    return { result: JSON.parse(trimmed), notes, hints, errors };
  } catch {
  }
  const lines = trimmed.split(/\r?\n/);
  let jsonCandidate = trimmed;
  if (/^Note:/i.test(lines[0] ?? "") && lines.length > 1) {
    notes.push((lines.shift() ?? "").replace(/^Note:\s*/i, ""));
    jsonCandidate = lines.join("\n").trim();
  }
  const parsed = parseBalancedJsonPrefix(jsonCandidate);
  if (parsed) {
    for (const entry of parsed.rest.split(/\r?\n/)) {
      const line = entry.trim();
      if (!line)
        continue;
      if (/^Hint:/i.test(line))
        hints.push(line.replace(/^Hint:\s*/i, ""));
      else if (/^Note:/i.test(line))
        notes.push(line.replace(/^Note:\s*/i, ""));
      else
        notes.push(line);
    }
    return { result: parsed.value, notes, hints, errors };
  }
  for (const entry of lines) {
    const line = entry.trim();
    if (!line)
      continue;
    if (/^Hint:/i.test(line))
      hints.push(line.replace(/^Hint:\s*/i, ""));
    else if (/^Note:/i.test(line))
      notes.push(line.replace(/^Note:\s*/i, ""));
    else if (/^LSP error:/i.test(line))
      errors.push(line);
    else
      notes.push(line);
  }
  return { notes, hints, errors };
}
function withoutUndefined(value) {
  for (const key of Object.keys(value)) {
    if (value[key] === void 0)
      delete value[key];
  }
  return value;
}
function buildLspNavigationEnvelope(options) {
  const parsed = parseLspNavigationTextPayload(options.text ?? "");
  const status = lspStatusFromFailureKind(options.failureKind);
  const result = parsed.result;
  const notes = [...parsed.notes];
  const hints = [...parsed.hints];
  const errors = [...parsed.errors];
  if (options.isError && errors.length === 0 && (options.text ?? "").trim()) {
    errors.push((options.text ?? "").trim());
  }
  const locations = collectLspToolLocationsForOperation(options.operation, result, options.filePath ?? "");
  return withoutUndefined({
    tool: "lsp_navigation",
    operation: options.operation,
    ok: !options.isError,
    status,
    filePath: options.filePath,
    resultCount: options.resultCount,
    result,
    locations: locations.length > 0 ? locations : void 0,
    notes: notes.length > 0 ? notes : void 0,
    hints: hints.length > 0 ? hints : void 0,
    errors: errors.length > 0 ? errors : void 0,
    metadata: options.details
  });
}

// dist/tools/lsp-navigation.js
var VALID_OPERATIONS = [
  "definition",
  "typeDefinition",
  "declaration",
  "references",
  "hover",
  "signatureHelp",
  "documentSymbol",
  "findSymbol",
  "workspaceSymbol",
  "codeAction",
  "rename",
  "rename_file",
  "implementation",
  "prepareCallHierarchy",
  "incomingCalls",
  "outgoingCalls",
  "executeCommand",
  "workspaceDiagnostics",
  "capabilities"
];
var NAVIGABLE_SYMBOL_KINDS = /* @__PURE__ */ new Set([
  5,
  // Class
  6,
  // Method
  8,
  // Field
  11,
  // Interface
  12,
  // Function
  13,
  // Variable
  22,
  // EnumMember
  23
  // Struct
]);
function normalizeOperation(value) {
  if (typeof value !== "string")
    return "";
  return value.trim().replace(/^["']+|["']+$/g, "");
}
function isValidOperation(value) {
  return VALID_OPERATIONS.includes(value);
}
function operationSupportStatus(operation, support) {
  if (!support)
    return null;
  if (operation === "definition")
    return support.definition;
  if (operation === "typeDefinition")
    return support.typeDefinition;
  if (operation === "declaration")
    return support.declaration;
  if (operation === "references")
    return support.references;
  if (operation === "hover")
    return support.hover;
  if (operation === "signatureHelp")
    return support.signatureHelp;
  if (operation === "documentSymbol" || operation === "findSymbol")
    return support.documentSymbol;
  if (operation === "workspaceSymbol")
    return support.workspaceSymbol;
  if (operation === "codeAction")
    return support.codeAction;
  if (operation === "rename")
    return support.rename;
  if (operation === "implementation")
    return support.implementation;
  if (operation === "prepareCallHierarchy" || operation === "incomingCalls" || operation === "outgoingCalls")
    return support.callHierarchy;
  return null;
}
function emptyReasonForOperation(operation) {
  if (operation === "signatureHelp")
    return "position-sensitive-or-no-signature";
  if (operation === "codeAction")
    return "no-applicable-actions";
  if (operation === "rename")
    return "no-rename-edits-or-symbol-not-renamable";
  if (operation === "rename_file")
    return "no-file-rename-result";
  if (operation === "findSymbol")
    return "no-matching-symbols";
  if (operation === "workspaceSymbol")
    return "no-matching-symbols-or-server-index-unavailable";
  if (operation === "capabilities")
    return "no-active-lsp-servers";
  if (operation === "executeCommand")
    return "command-returned-no-result";
  if (operation === "incomingCalls" || operation === "outgoingCalls")
    return "no-call-hierarchy-results";
  return "no-results";
}
function parseSymbolSelector(symbol) {
  const trimmed = symbol.trim();
  const match = /^([^#]*)(?:#(-?\d+))?$/.exec(trimmed);
  const baseSymbol = (match?.[1] ?? trimmed).trim();
  const rawOccurrence = match?.[2];
  if (!rawOccurrence)
    return { baseSymbol, occurrence: 1 };
  const occurrence = Number.parseInt(rawOccurrence, 10);
  if (!Number.isFinite(occurrence) || occurrence < 1) {
    return {
      baseSymbol,
      occurrence: 1,
      debug: `invalid occurrence selector #${rawOccurrence}; using #1`
    };
  }
  return { baseSymbol, occurrence };
}
function findNthMatch(lineText, regex, occurrence) {
  let seen = 0;
  regex.lastIndex = 0;
  let match;
  while ((match = regex.exec(lineText)) !== null) {
    seen += 1;
    if (seen === occurrence)
      return match;
    if (match[0].length === 0)
      regex.lastIndex += 1;
  }
  return null;
}
function firstNonWhitespaceCharacter(lineText) {
  const match = /\S/.exec(lineText);
  return (match?.index ?? 0) + 1;
}
function resolveSymbolColumn(content, line1, character, symbol) {
  if (typeof character === "number" && character > 0) {
    return { character, strategy: "explicit" };
  }
  const lineText = content.split(/\r?\n/)[line1 - 1] ?? "";
  if (!symbol || symbol.trim().length === 0) {
    return {
      character: 1,
      strategy: "fallback",
      debug: "character omitted and no symbol supplied; using column 1"
    };
  }
  const { baseSymbol, occurrence, debug } = parseSymbolSelector(symbol);
  if (!baseSymbol) {
    return {
      character: firstNonWhitespaceCharacter(lineText),
      requestedSymbol: symbol,
      baseSymbol,
      requestedOccurrence: occurrence,
      usedOccurrence: 1,
      strategy: "fallback",
      debug: debug ?? "empty symbol selector; using first non-whitespace column"
    };
  }
  const pattern = `\\b${escapeRegExp(baseSymbol)}\\b`;
  const exactRegex = new RegExp(pattern, "g");
  const exact = findNthMatch(lineText, exactRegex, occurrence);
  if (exact) {
    return {
      character: exact.index + 1,
      requestedSymbol: symbol,
      baseSymbol,
      requestedOccurrence: occurrence,
      usedOccurrence: occurrence,
      strategy: "word-boundary",
      debug
    };
  }
  const firstExact = findNthMatch(lineText, exactRegex, 1);
  if (firstExact && occurrence !== 1) {
    return {
      character: firstExact.index + 1,
      requestedSymbol: symbol,
      baseSymbol,
      requestedOccurrence: occurrence,
      usedOccurrence: 1,
      strategy: "word-boundary",
      debug: `${debug ? `${debug}; ` : ""}occurrence #${occurrence} not found; using #1`
    };
  }
  const insensitiveRegex = new RegExp(pattern, "gi");
  const insensitive = findNthMatch(lineText, insensitiveRegex, occurrence);
  if (insensitive) {
    return {
      character: insensitive.index + 1,
      requestedSymbol: symbol,
      baseSymbol,
      requestedOccurrence: occurrence,
      usedOccurrence: occurrence,
      strategy: "case-insensitive",
      debug: debug ?? "exact-case symbol not found; used case-insensitive match"
    };
  }
  const firstInsensitive = findNthMatch(lineText, insensitiveRegex, 1);
  if (firstInsensitive && occurrence !== 1) {
    return {
      character: firstInsensitive.index + 1,
      requestedSymbol: symbol,
      baseSymbol,
      requestedOccurrence: occurrence,
      usedOccurrence: 1,
      strategy: "case-insensitive",
      debug: `${debug ? `${debug}; ` : ""}occurrence #${occurrence} not found case-insensitively; using #1`
    };
  }
  return {
    character: firstNonWhitespaceCharacter(lineText),
    requestedSymbol: symbol,
    baseSymbol,
    requestedOccurrence: occurrence,
    usedOccurrence: 1,
    strategy: "fallback",
    debug: `${debug ? `${debug}; ` : ""}symbol not found on line; using first non-whitespace column`
  };
}
function tokenAtPosition(content, line1, char1) {
  const lines = content.split(/\r?\n/);
  const line = lines[line1 - 1];
  if (!line)
    return void 0;
  const chars = [...line];
  const idx = Math.max(0, Math.min(chars.length - 1, char1 - 1));
  const isWord = (ch) => !!ch && /[A-Za-z0-9_?!]/.test(ch);
  let left = idx;
  let right = idx;
  if (!isWord(chars[idx]) && isWord(chars[idx + 1])) {
    left = idx + 1;
    right = idx + 1;
  }
  while (left > 0 && isWord(chars[left - 1]))
    left -= 1;
  while (right < chars.length - 1 && isWord(chars[right + 1]))
    right += 1;
  const token = chars.slice(left, right + 1).join("").trim();
  return token.length > 0 ? token : void 0;
}
function symbolKindLabel(kind) {
  return kind == null ? "symbol" : SYMBOL_KIND_NAMES[kind] ?? "symbol";
}
function rangeStart(range) {
  const start = range?.start;
  return {
    line: typeof start?.line === "number" ? start.line + 1 : void 0,
    character: typeof start?.character === "number" ? start.character + 1 : void 0
  };
}
function findSymbolMatches(symbols, query, options) {
  const normalizedQuery = query.trim().toLowerCase();
  if (!normalizedQuery)
    return [];
  const matches = [];
  const matchesText = (symbol) => {
    const values = [symbol.name, symbol.detail].filter((value) => Boolean(value)).map((value) => value.trim().toLowerCase());
    return options.exactMatch ? values.some((value) => value === normalizedQuery) : values.some((value) => value.includes(normalizedQuery));
  };
  const matchesKind = (symbol) => {
    if (options.kinds.size === 0)
      return true;
    return options.kinds.has(symbolKindLabel(symbol.kind).toLowerCase());
  };
  const visit = (entries, depth) => {
    for (const symbol of entries) {
      if (symbol.name && matchesText(symbol) && matchesKind(symbol)) {
        const preferredRange = symbol.selectionRange ?? symbol.range;
        const start = rangeStart(preferredRange);
        matches.push({
          name: symbol.name,
          kind: symbolKindLabel(symbol.kind),
          kindCode: symbol.kind,
          detail: symbol.detail,
          line: start.line,
          character: start.character,
          depth,
          location: symbol.location,
          range: preferredRange
        });
        if (matches.length >= options.maxResults)
          return;
      }
      if (!options.topLevelOnly && symbol.children?.length) {
        visit(symbol.children, depth + 1);
        if (matches.length >= options.maxResults)
          return;
      }
    }
  };
  visit(symbols, 1);
  return matches;
}
function flattenSymbols(symbols) {
  const all = [];
  for (const symbol of symbols) {
    all.push(symbol);
    if (symbol.children && symbol.children.length > 0) {
      all.push(...flattenSymbols(symbol.children));
    }
  }
  return all;
}
function pickLocalSymbolLocation(symbols, token, filePath) {
  const flat = flattenSymbols(symbols).filter((symbol) => symbol.name === token);
  if (flat.length === 0)
    return [];
  const uri = pathToFileURL2(filePath).href;
  return flat.map((symbol) => {
    if (symbol.location?.uri && symbol.location.range) {
      return { uri: symbol.location.uri, range: symbol.location.range };
    }
    if (symbol.range) {
      return { uri, range: symbol.range };
    }
    return void 0;
  }).filter((entry) => Boolean(entry));
}
function workspaceSymbolDedupeKey(symbol) {
  const location = symbol.location;
  const start = rangeStart(location?.range ?? symbol.range ?? symbol.selectionRange);
  return [
    symbol.name ?? "",
    symbol.detail ?? "",
    symbol.kind ?? "",
    location?.uri ?? "",
    start.line ?? "",
    start.character ?? ""
  ].join(":");
}
function dedupeWorkspaceSymbols(symbols) {
  const out = [];
  const seen = /* @__PURE__ */ new Set();
  for (const symbol of symbols) {
    const key = workspaceSymbolDedupeKey(symbol);
    if (seen.has(key))
      continue;
    seen.add(key);
    out.push(symbol);
  }
  return out;
}
function asRecord2(value) {
  return typeof value === "object" && value !== null ? value : void 0;
}
function searchReadFromUriRange(uri, range) {
  if (typeof uri !== "string" || !uri.startsWith("file:"))
    return void 0;
  const rangeLike = asRecord2(range);
  const startLine = rangeLike?.start?.line;
  if (typeof startLine !== "number" || !Number.isFinite(startLine)) {
    return void 0;
  }
  const endLine = rangeLike?.end?.line;
  try {
    return {
      file: fileURLToPath2(uri),
      startLine: Math.max(1, Math.floor(startLine) + 1),
      endLine: typeof endLine === "number" && Number.isFinite(endLine) ? Math.max(1, Math.floor(endLine) + 1) : void 0
    };
  } catch {
    return void 0;
  }
}
function pushSearchRead(out, uri, range) {
  const loc = searchReadFromUriRange(uri, range);
  if (loc)
    out.push(loc);
}
function collectLocationSearchReads(result) {
  const out = [];
  for (const entry of Array.isArray(result) ? result : [result]) {
    const record = asRecord2(entry);
    if (!record)
      continue;
    pushSearchRead(out, record.uri, record.range);
    pushSearchRead(out, record.targetUri, record.targetSelectionRange ?? record.targetRange);
  }
  return out;
}
function collectWorkspaceSymbolSearchReads(result) {
  const out = [];
  for (const entry of Array.isArray(result) ? result : [result]) {
    const symbol = asRecord2(entry);
    const location = asRecord2(symbol?.location);
    if (!location)
      continue;
    pushSearchRead(out, location.uri, location.range);
    pushSearchRead(out, location.targetUri, location.targetSelectionRange ?? location.targetRange);
  }
  return out;
}
function collectCallHierarchySearchReads(result, operation, callHierarchyItem) {
  const out = [];
  for (const entry of Array.isArray(result) ? result : [result]) {
    const record = asRecord2(entry);
    if (!record)
      continue;
    const item = asRecord2(operation === "incomingCalls" ? record.from : record.to);
    pushSearchRead(out, item?.uri, item?.selectionRange ?? item?.range);
    const rangeUri = operation === "incomingCalls" ? item?.uri : callHierarchyItem?.uri;
    const fromRanges = record.fromRanges;
    if (Array.isArray(fromRanges)) {
      for (const range of fromRanges)
        pushSearchRead(out, rangeUri, range);
    }
  }
  return out;
}
function collectSearchReadsForOperation(operation, result, callHierarchyItem) {
  if ([
    "definition",
    "typeDefinition",
    "declaration",
    "references",
    "implementation"
  ].includes(operation)) {
    return collectLocationSearchReads(result);
  }
  if (operation === "workspaceSymbol") {
    return collectWorkspaceSymbolSearchReads(result);
  }
  if (operation === "incomingCalls" || operation === "outgoingCalls") {
    return collectCallHierarchySearchReads(result, operation, callHierarchyItem);
  }
  return [];
}
function formatCapabilities(snapshots, filePath) {
  if (snapshots.length === 0) {
    return filePath ? `No active LSP server for ${path37.basename(filePath)}. Open/touch the file first or run another LSP operation to start the server.` : "No active LSP servers in this session.";
  }
  const rows = [
    ["definition", (s) => !!s.operationSupport.definition],
    ["typeDefinition", (s) => !!s.operationSupport.typeDefinition],
    ["declaration", (s) => !!s.operationSupport.declaration],
    ["references", (s) => !!s.operationSupport.references],
    ["hover", (s) => !!s.operationSupport.hover],
    ["rename", (s) => !!s.operationSupport.rename],
    ["codeAction", (s) => !!s.operationSupport.codeAction],
    ["codeAction/resolve", (s) => !!s.operationSupport.codeActionResolve],
    ["workspace/willRenameFiles", (s) => !!s.operationSupport.willRenameFiles],
    ["workspaceSymbol", (s) => !!s.operationSupport.workspaceSymbol],
    ["implementation", (s) => !!s.operationSupport.implementation],
    ["signatureHelp", (s) => !!s.operationSupport.signatureHelp],
    ["incomingCalls", (s) => !!s.operationSupport.callHierarchy],
    ["outgoingCalls", (s) => !!s.operationSupport.callHierarchy],
    [
      "workspaceDiagnostics",
      (s) => s.workspaceDiagnosticsSupport.mode === "pull",
      "pull diagnostics"
    ],
    [
      "rename_file",
      () => true,
      "filesystem rename; preflight support shown above"
    ]
  ];
  const lines = [];
  for (const snapshot of snapshots) {
    const label = filePath ? `${snapshot.serverId} (${path37.basename(filePath)})` : `${snapshot.serverId} (${snapshot.root})`;
    lines.push(label);
    for (const [name, supported, note] of rows) {
      const suffix = note ? ` (${note})` : "";
      lines.push(`  ${name} ${supported(snapshot) ? "\u2713" : "\u2717"}${suffix}`);
    }
    const commands = snapshot.advertisedCommands ?? [];
    lines.push(`  executeCommand ${commands.length > 0 ? "\u2713" : "\u2717"} (${commands.length} advertised command(s)` + (commands.length > 0 ? `: ${commands.slice(0, 20).join(", ")}` : "") + ")");
  }
  return lines.join("\n");
}
function classifyCodeActions(actions) {
  if (!actions || actions.length === 0)
    return { quickfix: 0, refactor: 0, other: 0 };
  let quickfix = 0;
  let refactor = 0;
  let other = 0;
  for (const action of actions) {
    const kind = action.kind ?? "";
    if (kind.startsWith("quickfix"))
      quickfix += 1;
    else if (kind.startsWith("refactor"))
      refactor += 1;
    else
      other += 1;
  }
  return { quickfix, refactor, other };
}
async function openFileBestEffort(lspService, filePath, waitForDiagnostics = false) {
  let fileContent;
  try {
    fileContent = nodeFs2.readFileSync(filePath, "utf-8");
  } catch {
    return void 0;
  }
  if (fileContent === "")
    return "";
  try {
    await lspService.touchFile(filePath, fileContent, {
      diagnostics: waitForDiagnostics ? "document" : "none",
      source: "lsp_navigation",
      clientScope: waitForDiagnostics ? "all" : "primary"
    });
  } catch {
  }
  return fileContent;
}
var RENAME_MTIME_MARGIN_MS = 2e3;
function captureRenameExpectedContent(lspService, edit, cwd, targetFilePath, targetContent, requestedAtMs) {
  const expected = /* @__PURE__ */ new Map();
  let targetRealPath;
  if (targetContent !== void 0) {
    try {
      targetRealPath = nodeFs2.realpathSync.native(targetFilePath);
    } catch {
      targetRealPath = void 0;
    }
  }
  for (const diskPath of workspaceEditTextPaths(edit)) {
    let realPath;
    let content;
    let mtimeMs;
    try {
      realPath = nodeFs2.realpathSync.native(diskPath);
      content = nodeFs2.readFileSync(realPath, "utf-8");
      mtimeMs = nodeFs2.statSync(realPath).mtimeMs;
    } catch {
      continue;
    }
    if (targetContent !== void 0 && realPath === targetRealPath) {
      expected.set(realPath, targetContent);
      continue;
    }
    const tracked = lspService.getTrackedContent(diskPath, cwd);
    const firstOpenedAfterRequest = tracked !== void 0 && (tracked.openedAtMs ?? 0) >= requestedAtMs;
    const quietSinceClientStart = tracked?.clientStartedAtMs !== void 0 && mtimeMs < tracked.clientStartedAtMs - RENAME_MTIME_MARGIN_MS;
    const sent = firstOpenedAfterRequest && quietSinceClientStart ? void 0 : tracked;
    if (sent !== void 0) {
      if (sent.hash !== hashDiagnosticContent(content) || (sent.changedAtMs ?? requestedAtMs) >= requestedAtMs) {
        refuseStaleWorkspaceEdit(diskPath, "it changed after the language server computed the rename from it");
      }
    } else if (tracked !== void 0 && (tracked.hash !== hashDiagnosticContent(content) || tracked.openedHash !== tracked.hash)) {
      refuseStaleWorkspaceEdit(diskPath, "it changed after the language client first opened it, after the rename was requested");
    } else if (mtimeMs >= requestedAtMs) {
      refuseStaleWorkspaceEdit(diskPath, "it was written after the rename was requested and the language server has no open copy, so it may not have read these bytes");
    } else if (mtimeMs >= requestedAtMs - RENAME_MTIME_MARGIN_MS) {
      refuseStaleWorkspaceEdit(diskPath, `it was modified within ${RENAME_MTIME_MARGIN_MS / 1e3} s of the rename request; retry`);
    }
    expected.set(realPath, content);
  }
  return expected;
}
function createLspNavigationTool(getFlag, mutationDeps) {
  return {
    name: "lsp_navigation",
    label: "LSP Navigate",
    description: 'Navigate source with language-server operations such as definition, references, hover, and rename. Example: use `{operation: "references", path: "src/app.ts", line: 12}`.',
    promptSnippet: "Navigate definitions and references with LSP",
    renderResult: compactRenderResult(({ details, args, isError, text }) => {
      const op = details?.operation ?? (typeof args.operation === "string" ? args.operation : "lsp");
      if (isError || details?.supported === false) {
        return `lsp_navigation ${op} \u2014 ${details?.emptyReason ?? text.split("\n")[0] ?? "unavailable"}`;
      }
      const n = details?.resultCount ?? 0;
      if (n === 0) {
        return `lsp_navigation ${op} \u2014 ${details?.emptyReason ?? "no results"}`;
      }
      return `lsp_navigation ${op} \u2014 ${n} result${n === 1 ? "" : "s"}`;
    }),
    parameters: Type.Object({
      operation: Type.String({
        description: "LSP operation to perform. Valid values: definition, typeDefinition, declaration, references, hover, signatureHelp, documentSymbol, findSymbol, workspaceSymbol, codeAction, rename, rename_file, implementation, prepareCallHierarchy, incomingCalls, outgoingCalls, executeCommand, workspaceDiagnostics, capabilities."
      }),
      path: Type.Optional(Type.String({
        description: "Target file path."
      })),
      line: Type.Optional(Type.Number({
        description: "1-based line number."
      })),
      character: Type.Optional(Type.Number({
        description: "1-based character offset."
      })),
      symbol: Type.Optional(Type.String({
        description: "Symbol for automatic character resolution."
      })),
      endLine: Type.Optional(Type.Number({
        description: "1-based range end line."
      })),
      endCharacter: Type.Optional(Type.Number({
        description: "1-based range end character."
      })),
      newName: Type.Optional(Type.String({
        description: "New symbol name."
      })),
      newFilePath: Type.Optional(Type.String({
        description: "New file path."
      })),
      apply: Type.Optional(Type.Boolean({
        description: "Apply a mutation instead of previewing it."
      })),
      command: Type.Optional(Type.String({
        description: "Server command identifier."
      })),
      commandArguments: Type.Optional(Type.Array(Type.Unknown(), {
        description: "Arguments for the server command."
      })),
      query: Type.Optional(Type.String({
        description: "Symbol search query."
      })),
      kinds: Type.Optional(Type.Array(Type.String(), {
        description: "Symbol-kind filters."
      })),
      exactMatch: Type.Optional(Type.Boolean({
        description: "Require an exact symbol match."
      })),
      topLevelOnly: Type.Optional(Type.Boolean({
        description: "Exclude nested symbols."
      })),
      maxResults: Type.Optional(Type.Number({
        description: "Maximum symbol matches."
      })),
      callHierarchyItem: Type.Optional(Type.Object({
        name: Type.String(),
        kind: Type.Number(),
        uri: Type.String(),
        range: Type.Object({
          start: Type.Object({
            line: Type.Number(),
            character: Type.Number()
          }),
          end: Type.Object({
            line: Type.Number(),
            character: Type.Number()
          })
        }),
        selectionRange: Type.Object({
          start: Type.Object({
            line: Type.Number(),
            character: Type.Number()
          }),
          end: Type.Object({
            line: Type.Number(),
            character: Type.Number()
          })
        })
      }, {}))
    }),
    async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
      const session = mutationDeps?.runtime?.captureSessionGeneration?.();
      const startedAt = Date.now();
      let supported = null;
      let diagnosticsMode = "unknown";
      let columnResolution;
      let openedFileContent;
      let renameRequestedAtMs = 0;
      let mutationContext;
      let requestedApply = false;
      const workspaceScopeAttribution = {};
      const finalize = (payload, meta) => {
        if (payload.isError && requestedApply && mutationContext && ["rename", "rename_file", "executeCommand"].includes(meta.operation)) {
          recordLspMutationOutcome(mutationContext, "failed");
        }
        const normalizedFilePath = meta.filePath.replace(/\\/g, "/");
        logLatency({
          type: "phase",
          phase: "lsp_navigation_result",
          filePath: normalizedFilePath,
          durationMs: Date.now() - startedAt,
          metadata: {
            operation: meta.operation,
            failureKind: meta.failureKind,
            resultCount: meta.resultCount,
            supported,
            diagnosticsMode,
            columnResolution,
            ...Object.keys(workspaceScopeAttribution).length > 0 ? { workspaceScopeAttribution } : {}
          }
        });
        const text = payload.content[0]?.text ?? "";
        const envelope = buildLspNavigationEnvelope({
          operation: meta.operation,
          filePath: meta.filePath,
          failureKind: meta.failureKind,
          resultCount: meta.resultCount,
          text,
          isError: payload.isError,
          details: payload.details
        });
        return {
          ...payload,
          content: [
            {
              type: "text",
              // Compact JSON: omit indentation. Saves ~50% on rename / workspace-edits payloads
              // while keeping the envelope machine-parseable. Tests use JSON.parse so
              // they are agnostic to whitespace.
              text: JSON.stringify(envelope)
            }
          ],
          details: {
            ...payload.details,
            failureKind: meta.failureKind
          }
        };
      };
      if (getFlag("no-lsp", ctx.cwd)) {
        return finalize({
          content: [
            {
              type: "text",
              text: "lsp_navigation requires LSP to be enabled. Remove --no-lsp to use LSP navigation."
            }
          ],
          isError: true
        }, {
          operation: "precheck",
          filePath: "(workspace)",
          failureKind: "lsp_disabled",
          resultCount: 0
        });
      }
      const { operation: rawOperation, path: rawPath, line, character, symbol, endLine, endCharacter, newName, newFilePath, apply, command, commandArguments, query, kinds, exactMatch, topLevelOnly, maxResults, callHierarchyItem } = params;
      const normalizedOperation = normalizeOperation(rawOperation);
      if (!isValidOperation(normalizedOperation)) {
        return finalize({
          content: [
            {
              type: "text",
              text: `Unknown lsp_navigation operation "${normalizedOperation || String(rawOperation ?? "") || ""}". Valid operations: ${VALID_OPERATIONS.join(", ")}`
            }
          ],
          isError: true,
          details: {
            rawOperation,
            normalizedOperation,
            validOperations: VALID_OPERATIONS
          }
        }, {
          operation: normalizedOperation || "invalid",
          filePath: "(workspace)",
          failureKind: "invalid_operation",
          resultCount: 0
        });
      }
      const operation = normalizedOperation;
      requestedApply = apply === true;
      if (requestedApply && ["rename", "rename_file", "executeCommand"].includes(operation)) {
        const cwd = ctx.cwd || ".";
        const mutationSource = operation === "executeCommand" ? "lsp-execute-command" : "lsp-rename";
        mutationContext = {
          cwd,
          correlationId: newLspMutationCorrelationId(_toolCallId),
          tool: `lsp_navigation:${operation}`,
          source: mutationSource,
          ...mutationDeps,
          session,
          readGuard: getFlag("no-read-guard", cwd) ? void 0 : mutationDeps?.readGuard,
          // #2450 review round 2 (F4)/round 3 (F2, F4): the SAME gate
          // `registerMutationBridge` applies internally in `index.ts`, so
          // this directly-threaded path and the bridge fallback
          // (`clients/lsp-mutation.ts`, reached when `mutationDeps` is
          // absent/partial — e.g. the MCP server) agree on which files
          // count as project source, rather than the direct path
          // recording an ignored/vendor write the fallback would drop.
          // `no-read-guard` is intentionally NOT checked here (round 3
          // F2): it gates only the read-guard stamp above, the same
          // canonical split `clients/runtime-tool-result.ts` applies
          // (`:1120`, `:1485`) — bookkeeping (turn-state / receipts)
          // still runs under `--no-read-guard`. Judge against the
          // project root, not the request `cwd` (round 3 F4): a
          // sub-package `cwd` must not read a sibling-package rename as
          // external.
          isRecordable: (filePath2) => {
            return isRecordableProjectPath(filePath2, mutationDeps?.runtime?.projectRoot ?? cwd);
          }
        };
      }
      const isCallHierarchyTraversal = operation === "incomingCalls" || operation === "outgoingCalls";
      const needsFilePath = operation !== "workspaceDiagnostics" && operation !== "workspaceSymbol" && operation !== "executeCommand" && operation !== "capabilities" && !isCallHierarchyTraversal;
      if (needsFilePath && (!rawPath || rawPath.trim().length === 0)) {
        return finalize({
          content: [
            {
              type: "text",
              text: `path is required for ${operation}`
            }
          ],
          isError: true
        }, {
          operation,
          filePath: "(workspace)",
          failureKind: "missing_file_path",
          resultCount: 0
        });
      }
      const filePath = rawPath ? path37.isAbsolute(rawPath) ? rawPath : path37.resolve(ctx.cwd || ".", rawPath) : "";
      let filePathIsDirectory = false;
      if (filePath) {
        try {
          filePathIsDirectory = nodeFs2.statSync(filePath).isDirectory();
        } catch {
        }
      }
      const lspService = getLSPService();
      if (operation === "capabilities") {
        const snapshots = rawPath ? await lspService.getCapabilitySnapshots(filePath) : await lspService.getCapabilitySnapshots(void 0, workspaceScopeAttribution);
        const output2 = formatCapabilities(snapshots, rawPath ? filePath : void 0);
        return finalize({
          content: [{ type: "text", text: output2 }],
          details: {
            operation,
            resultCount: snapshots.length,
            servers: snapshots.map((snapshot) => snapshot.serverId)
          }
        }, {
          operation,
          filePath: rawPath ? filePath : "(workspace)",
          failureKind: snapshots.length === 0 ? "empty_result" : "success",
          resultCount: snapshots.length
        });
      }
      if (operation === "workspaceDiagnostics") {
        const wsDiagSupport = rawPath ? await lspService.getWorkspaceDiagnosticsSupport(filePath) : await lspService.getWorkspaceDiagnosticsSupport(void 0, workspaceScopeAttribution);
        diagnosticsMode = wsDiagSupport?.mode ?? "unknown";
        if (rawPath && !filePathIsDirectory) {
          const hasLSP2 = lspService.supportsLSP(filePath);
          if (!hasLSP2) {
            return finalize({
              content: [
                {
                  type: "text",
                  text: `No LSP server available for ${path37.basename(filePath)}. Check that the language server is installed.`
                }
              ],
              isError: true
            }, {
              operation,
              filePath,
              failureKind: "no_server",
              resultCount: 0
            });
          }
          await openFileBestEffort(lspService, filePath, true);
          const diagnostics = await lspService.getDiagnostics(filePath);
          const result3 = [
            {
              filePath,
              diagnostics,
              count: diagnostics.length
            }
          ];
          const noteMap = {
            pull: "Note: path mode requests pull diagnostics for this file and returns the aggregated result",
            "push-only": "Note: server is push-only; result depends on published diagnostics for this file"
          };
          const note2 = noteMap[diagnosticsMode] ?? "Note: workspace diagnostics mode unknown (no active capability snapshot)";
          const resultCount2 = diagnostics.length;
          return finalize({
            content: [
              {
                type: "text",
                text: `${note2}
${JSON.stringify(result3)}`
              }
            ],
            details: {
              operation,
              resultCount: resultCount2,
              diagnosticsMode,
              coverage: "requested-file"
            }
          }, {
            operation,
            filePath,
            failureKind: resultCount2 === 0 ? "empty_result" : "success",
            resultCount: resultCount2
          });
        }
        const allDiagnostics = await lspService.getAllDiagnostics();
        const result2 = Array.from(allDiagnostics.entries()).map(([trackedFile, { diags }]) => ({
          filePath: trackedFile,
          diagnostics: diags,
          count: diags.length
        }));
        const noteMap2 = {
          "push-only": "Note: push-only tracked diagnostics snapshot (not full workspace pull diagnostics).",
          pull: "Note: tracked diagnostics snapshot from active clients. Provide path to force file-level diagnostics collection"
        };
        const note = noteMap2[diagnosticsMode] ?? "Note: workspace diagnostics mode unknown (no active capability snapshot)";
        return finalize({
          content: [
            {
              type: "text",
              text: `${note}
${JSON.stringify(result2)}`
            }
          ],
          details: {
            operation,
            resultCount: result2.length,
            diagnosticsMode,
            coverage: "tracked-open-files"
          }
        }, {
          operation,
          filePath: rawPath ? filePath : "(workspace)",
          failureKind: diagnosticsMode === "push-only" ? "tracked_snapshot" : "success",
          resultCount: result2.length
        });
      }
      if (needsFilePath && filePathIsDirectory) {
        return finalize({
          content: [
            {
              type: "text",
              text: `path must be a source file, got directory: ${filePath}. Pass a source file path, or omit path for workspace-level operations.`
            }
          ],
          isError: true
        }, {
          operation,
          filePath,
          failureKind: "filepath_is_directory",
          resultCount: 0
        });
      }
      const hasLSP = filePath ? lspService.supportsLSP(filePath) : false;
      if (needsFilePath && !hasLSP) {
        return finalize({
          content: [
            {
              type: "text",
              text: `No LSP server available for ${path37.basename(filePath)}. Check that the language server is installed.`
            }
          ],
          isError: true
        }, {
          operation,
          filePath,
          failureKind: "no_server",
          resultCount: 0
        });
      }
      if (needsFilePath) {
        const support = await lspService.getOperationSupport(filePath);
        supported = operationSupportStatus(operation, support);
        if (supported === false) {
          return finalize({
            content: [
              {
                type: "text",
                text: `LSP server for ${path37.basename(filePath)} does not advertise support for ${operation}`
              }
            ],
            isError: true,
            details: {
              operation,
              supported: false,
              emptyReason: "unsupported"
            }
          }, { operation, filePath, failureKind: "unsupported", resultCount: 0 });
        }
        openedFileContent = await openFileBestEffort(lspService, filePath);
      }
      const lspLine = (line ?? 1) - 1;
      const needsPosition = [
        "definition",
        "typeDefinition",
        "declaration",
        "references",
        "hover",
        "signatureHelp",
        "codeAction",
        "rename",
        "implementation",
        "prepareCallHierarchy"
      ].includes(operation);
      const resolvedCharacter = needsPosition && filePath ? resolveSymbolColumn(nodeFs2.existsSync(filePath) ? nodeFs2.readFileSync(filePath, "utf-8") : "", line ?? 1, character, symbol) : {
        character: character ?? 1,
        strategy: "explicit"
      };
      columnResolution = resolvedCharacter;
      const lspChar = resolvedCharacter.character - 1;
      const lspEndLine = (endLine ?? line ?? 1) - 1;
      const lspEndChar = (endCharacter ?? resolvedCharacter.character) - 1;
      const runWorkspaceSymbolOperation = async () => {
        const support = rawPath ? await lspService.getOperationSupport(filePath) : await lspService.getOperationSupport(void 0, workspaceScopeAttribution);
        supported = operationSupportStatus(operation, support);
        if (supported === false) {
          throw new Error("__UNSUPPORTED__ Active LSP server does not advertise support for workspaceSymbol");
        }
        if (!query || query.trim().length === 0) {
          throw new Error("__BADINPUT__ query parameter required for workspaceSymbol");
        }
        if (rawPath) {
          await openFileBestEffort(lspService, filePath);
        }
        try {
          const raw = rawPath ? await lspService.workspaceSymbol(query ?? "", filePath) : await lspService.workspaceSymbol(query ?? "", void 0, workspaceScopeAttribution);
          const filtered = (Array.isArray(raw) ? raw : [raw]).filter((s) => typeof s === "object" && s !== null && (!s.kind || NAVIGABLE_SYMBOL_KINDS.has(s.kind)));
          return dedupeWorkspaceSymbols(filtered).slice(0, 15);
        } catch (err) {
          const msg = err instanceof Error ? err.message : String(err);
          if (rawPath && /No Project/i.test(msg)) {
            await openFileBestEffort(lspService, filePath);
            await new Promise((resolve30) => setTimeout(resolve30, 120));
            const retryRaw = await lspService.workspaceSymbol(query ?? "", filePath);
            const retrySymbols = (Array.isArray(retryRaw) ? retryRaw : [retryRaw]).filter((s) => typeof s === "object" && s !== null);
            return dedupeWorkspaceSymbols(retrySymbols);
          }
          throw err;
        }
      };
      const runOperation = async () => {
        switch (operation) {
          case "definition":
            return lspService.definition(filePath, lspLine, lspChar);
          case "typeDefinition":
            return lspService.typeDefinition(filePath, lspLine, lspChar);
          case "declaration":
            return lspService.declaration(filePath, lspLine, lspChar);
          case "references":
            return lspService.references(filePath, lspLine, lspChar);
          case "hover":
            return lspService.hover(filePath, lspLine, lspChar);
          case "signatureHelp":
            return lspService.signatureHelp(filePath, lspLine, lspChar);
          case "documentSymbol":
            return lspService.documentSymbol(filePath);
          case "findSymbol": {
            if (!query || query.trim().length === 0) {
              throw new Error("__BADINPUT__ query parameter required for findSymbol");
            }
            const symbols = await lspService.documentSymbol(filePath);
            return findSymbolMatches(symbols, query, {
              maxResults: Math.max(1, Math.min(100, maxResults ?? 20)),
              topLevelOnly: topLevelOnly ?? false,
              exactMatch: exactMatch ?? false,
              kinds: new Set((kinds ?? []).map((kind) => kind.trim().toLowerCase()).filter(Boolean))
            });
          }
          case "workspaceSymbol":
            return runWorkspaceSymbolOperation();
          case "codeAction":
            return lspService.codeAction(filePath, lspLine, lspChar, lspEndLine, lspEndChar);
          case "rename": {
            if (!newName || newName.trim().length === 0) {
              throw new Error("__BADINPUT__ newName parameter required for rename");
            }
            renameRequestedAtMs = Date.now();
            const edit = await lspService.rename(filePath, lspLine, lspChar, newName);
            if (!edit)
              return null;
            if (!apply) {
              return {
                applied: false,
                summary: summarizeWorkspaceEdit(edit, ctx.cwd || "."),
                edit
              };
            }
            const applied = await applyWorkspaceEdit(edit, ctx.cwd || ".", {
              mutationContext,
              // #3601: the server computed this edit from the bytes each touched
              // file held when it answered. A file that changed in between is
              // refused inside pi's queue, before any write, so the edit never
              // lands on text it was not computed for.
              expectedContent: captureRenameExpectedContent(lspService, edit, ctx.cwd || ".", filePath, openedFileContent, renameRequestedAtMs)
            });
            for (const touchedFile of applied.files) {
              try {
                await openFileBestEffort(lspService, touchedFile, false);
              } catch {
              }
            }
            return { applied: true, ...applied };
          }
          case "rename_file": {
            if (!newFilePath || newFilePath.trim().length === 0) {
              throw new Error("__BADINPUT__ newFilePath parameter required for rename_file");
            }
            const resolvedNewFilePath = path37.isAbsolute(newFilePath) ? newFilePath : path37.resolve(ctx.cwd || ".", newFilePath);
            const result2 = await lspService.renameFile(filePath, resolvedNewFilePath, {
              cwd: ctx.cwd || ".",
              apply: apply ?? false,
              ...mutationContext ? { mutationContext } : {}
            });
            if (result2.applied) {
              for (const touchedFile of result2.files ?? []) {
                try {
                  await openFileBestEffort(lspService, touchedFile, false);
                } catch {
                }
              }
            }
            return result2;
          }
          case "implementation":
            return lspService.implementation(filePath, lspLine, lspChar);
          case "prepareCallHierarchy":
            return lspService.prepareCallHierarchy(filePath, lspLine, lspChar);
          case "executeCommand": {
            if (!command || command.trim().length === 0) {
              throw new Error("__BADINPUT__ command parameter required for executeCommand");
            }
            const advertised = rawPath ? await lspService.getAdvertisedCommands(filePath) : await lspService.getAdvertisedCommands(void 0, workspaceScopeAttribution);
            const isAdvertised = advertised.includes(command);
            if (apply !== true) {
              return {
                executed: false,
                dryRun: true,
                command,
                advertised: isAdvertised,
                advertisedCommands: advertised,
                note: isAdvertised ? "Command is advertised. Re-run with apply:true to execute." : "Command is NOT advertised by the active server; execution would be refused."
              };
            }
            if (!isAdvertised) {
              throw new Error(`__UNSUPPORTED__ command "${command}" is not advertised by the active LSP server (advertised: ${advertised.join(", ") || "none"})`);
            }
            return rawPath ? lspService.executeCommand(filePath, command, commandArguments, mutationContext) : lspService.executeCommand(void 0, command, commandArguments, mutationContext, workspaceScopeAttribution);
          }
          case "incomingCalls": {
            if (!callHierarchyItem) {
              throw new Error("__BADINPUT__ callHierarchyItem parameter required for incomingCalls");
            }
            supported = operationSupportStatus(operation, await lspService.getOperationSupport(uriToPath(callHierarchyItem.uri)));
            if (supported === false) {
              throw new Error("__UNSUPPORTED__ Active LSP server does not advertise support for incomingCalls");
            }
            return lspService.incomingCalls(callHierarchyItem);
          }
          case "outgoingCalls": {
            if (!callHierarchyItem) {
              throw new Error("__BADINPUT__ callHierarchyItem parameter required for outgoingCalls");
            }
            supported = operationSupportStatus(operation, await lspService.getOperationSupport(uriToPath(callHierarchyItem.uri)));
            if (supported === false) {
              throw new Error("__UNSUPPORTED__ Active LSP server does not advertise support for outgoingCalls");
            }
            return lspService.outgoingCalls(callHierarchyItem);
          }
          default:
            return [];
        }
      };
      let result;
      let usedDocumentSymbolFallback = false;
      try {
        result = await runOperation();
        const isEmptyInitial = !result || Array.isArray(result) && result.length === 0;
        const shouldRetryOnEmpty = isEmptyInitial && needsFilePath && [
          "definition",
          "typeDefinition",
          "declaration",
          "references",
          "hover",
          "signatureHelp",
          "codeAction",
          "rename",
          "implementation"
        ].includes(operation);
        if (shouldRetryOnEmpty) {
          openedFileContent = await openFileBestEffort(lspService, filePath, true);
          result = await runOperation();
        }
        const stillEmpty = !result || Array.isArray(result) && result.length === 0;
        if (stillEmpty && needsFilePath && operation === "definition") {
          const content = nodeFs2.readFileSync(filePath, "utf-8");
          const token = line && character ? tokenAtPosition(content, line, character) : void 0;
          if (token) {
            const docSymbols = await lspService.documentSymbol(filePath);
            const locations = pickLocalSymbolLocation(docSymbols, token, filePath);
            if (locations.length > 0) {
              result = locations;
              usedDocumentSymbolFallback = true;
            }
          }
        }
        if (mutationContext && requestedApply && !mutationContext.summaryEmitted) {
          const outcome = operation === "rename_file" && result && typeof result === "object" && "applied" in result && result.applied === false ? "failed" : operation === "executeCommand" && result && typeof result === "object" && "executed" in result && result.executed === false ? "failed" : "skipped";
          recordLspMutationOutcome(mutationContext, outcome);
        }
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        if (msg.startsWith("__UNSUPPORTED__ ")) {
          return finalize({
            content: [
              {
                type: "text",
                text: msg.replace("__UNSUPPORTED__ ", "")
              }
            ],
            isError: true,
            details: {
              operation,
              supported: false,
              emptyReason: "unsupported"
            }
          }, { operation, filePath, failureKind: "unsupported", resultCount: 0 });
        }
        if (msg.startsWith("__BADINPUT__ ")) {
          return finalize({
            content: [
              {
                type: "text",
                text: msg.replace("__BADINPUT__ ", "")
              }
            ],
            isError: true,
            details: {}
          }, { operation, filePath, failureKind: "bad_input", resultCount: 0 });
        }
        return finalize({
          content: [
            {
              type: "text",
              text: `LSP error: ${err instanceof Error ? err.message : String(err)}`
            }
          ],
          isError: true,
          details: {}
        }, { operation, filePath, failureKind: "lsp_error", resultCount: 0 });
      }
      const isEmpty = !result || Array.isArray(result) && result.length === 0;
      const fileCtx = filePath ? " at " + path37.basename(filePath) : "";
      const lineCtx = line ? ":" + line + ":" + character : "";
      let output = isEmpty ? "No results for " + operation + fileCtx + lineCtx : JSON.stringify(result);
      if (isEmpty && operation === "workspaceSymbol" && !rawPath) {
        output += "\nHint: provide path to scope workspaceSymbol to the active language server/root.";
      }
      if (usedDocumentSymbolFallback) {
        output += "\nNote: served from documentSymbol fallback due to empty primary result.";
      }
      if (operation === "references" && Array.isArray(result) && result.length <= 2) {
        output += "\nHint: references from usage sites can be partial; retry from the symbol definition for broader cross-file results.";
      }
      const actionStats = operation === "codeAction" && Array.isArray(result) ? classifyCodeActions(result) : null;
      if (operation === "codeAction" && actionStats) {
        if (actionStats.quickfix === 0 && actionStats.refactor > 0) {
          output += "\nNote: no diagnostic quick fixes returned; refactor-only actions available.";
        }
      }
      const resultCount = Array.isArray(result) ? result.length : result ? 1 : 0;
      const searchReads = collectSearchReadsForOperation(operation, result, callHierarchyItem);
      return finalize({
        content: [{ type: "text", text: output }],
        details: {
          operation,
          supported,
          searchReads: searchReads.length > 0 ? searchReads : void 0,
          emptyReason: isEmpty ? emptyReasonForOperation(operation) : void 0,
          codeActionKinds: actionStats ?? void 0,
          columnResolution: columnResolution?.strategy === "explicit" ? void 0 : columnResolution,
          resultCount
        }
      }, {
        operation,
        filePath: rawPath ? filePath : "(workspace)",
        failureKind: isEmpty ? "empty_result" : usedDocumentSymbolFallback ? "fallback_success" : "success",
        resultCount
      });
    }
  };
}

// dist/mcp/tool-arguments.js
var MAX_REPORTED_KEYS = 8;
var MAX_REPORTED_KEY_CHARS = 64;
function withinOneEdit(a, b) {
  let i = 0;
  while (i < a.length && i < b.length && a[i] === b[i])
    i++;
  if (a.length === b.length)
    return a.slice(i + 1) === b.slice(i + 1);
  return a.length < b.length ? a.slice(i) === b.slice(i + 1) : b.slice(i) === a.slice(i + 1);
}
function nearestDeclaredKey(key, declared) {
  const fold = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "");
  const folded = fold(key);
  let best;
  for (const candidate of declared) {
    const other = fold(candidate);
    const contained = Math.min(folded.length, other.length) >= 3 && (folded.includes(other) || other.includes(folded));
    if (!contained && !withinOneEdit(folded, other))
      continue;
    const score = Math.abs(folded.length - other.length);
    if (!best || score < best.score)
      best = { key: candidate, score };
  }
  return best?.key;
}
function tokensOf(key) {
  return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).map((token) => token.replace(/s$/, "")).filter(Boolean);
}
var NON_LOCATOR_QUALIFIERS = /* @__PURE__ */ new Set([
  "max",
  "min",
  "num",
  "count",
  "total",
  "include",
  "exclude",
  "out",
  "output"
]);
function classifyKey(key, declared) {
  const tokens = tokensOf(key);
  const folded = tokens.join("");
  const declaredFolds = new Set(declared.map((name) => tokensOf(name).join("")));
  const equal = [];
  const suffix = [];
  const retargeted = [];
  for (const candidate of declared) {
    const wanted = tokensOf(candidate);
    if (wanted.length === 0)
      continue;
    if (folded === wanted.join("")) {
      equal.push(candidate);
      continue;
    }
    if (tokens.slice(-wanted.length).join(" ") !== wanted.join(" "))
      continue;
    const qualifier = tokens.slice(0, -wanted.length);
    const retargets = declaredFolds.has(qualifier.join("")) || qualifier.some((token) => NON_LOCATOR_QUALIFIERS.has(token));
    (retargets ? retargeted : suffix).push(candidate);
  }
  return equal.length > 0 ? { matches: equal, retargeted: [] } : { matches: suffix, retargeted };
}
function findIgnoredArguments(schema, args) {
  const properties = schema.properties ?? {};
  const declared = Object.keys(properties);
  const unsentSuggestions = [];
  const ignored = Object.keys(args).filter((key) => !Object.hasOwn(properties, key)).map((key) => {
    const { matches, retargeted } = classifyKey(key, declared);
    const unsent = matches.some((match) => Object.hasOwn(args, match)) ? void 0 : matches[0];
    if (unsent !== void 0 && !unsentSuggestions.some((entry) => entry.suggestion === unsent))
      unsentSuggestions.push({ key, suggestion: unsent });
    const suggestion = matches[0] ?? nearestDeclaredKey(key, declared.filter((name) => !retargeted.includes(name)));
    return suggestion === void 0 ? { key } : { key, suggestion };
  });
  if (ignored.length === 0)
    return void 0;
  const missingRequired = (schema.required ?? []).filter((key) => !Object.hasOwn(args, key));
  return { ignored, missingRequired, unsentSuggestions };
}
function shown(key) {
  return key.length > MAX_REPORTED_KEY_CHARS ? `${key.slice(0, MAX_REPORTED_KEY_CHARS)}\u2026` : key;
}
function ignoredArgumentsLine(tool, report) {
  const listed = report.ignored.slice(0, MAX_REPORTED_KEYS).map((entry) => {
    const hint = entry.suggestion ? ` (did you mean \`${entry.suggestion}\`?)` : "";
    return `\`${shown(entry.key)}\`${hint}`;
  });
  const more = report.ignored.length - listed.length;
  const tail = more > 0 ? ` and ${more} more` : "";
  return `Ignored unknown argument(s) for ${tool}: ${listed.join(", ")}${tail}. They had no effect on this call.`;
}
function ignoredArgumentsStructured(report) {
  return {
    ignoredArguments: report.ignored.slice(0, MAX_REPORTED_KEYS).map((entry) => shown(entry.key)),
    ignoredArgumentCount: report.ignored.length
  };
}
function withIgnoredArguments(result, tool, report) {
  const line = ignoredArgumentsLine(tool, report);
  const [first, ...rest] = result.content;
  return {
    ...result,
    content: first ? [{ ...first, text: `${line}

${first.text}` }, ...rest] : [{ type: "text", text: line }],
    structuredContent: ignoredArgumentsStructured(report)
  };
}
function refusalResult(tool, report) {
  const reasons = [];
  if (report.missingRequired.length > 0)
    reasons.push(`required argument(s) ${report.missingRequired.map((key) => `\`${key}\``).join(", ")} missing`);
  for (const { key, suggestion } of report.unsentSuggestions) {
    if (report.missingRequired.includes(suggestion))
      continue;
    reasons.push(`\`${shown(key)}\` looks like a mistyped \`${suggestion}\`, which was not sent`);
  }
  if (reasons.length === 0)
    return void 0;
  return {
    content: [
      {
        type: "text",
        text: `${ignoredArgumentsLine(tool, report)}
Not run: ${reasons.join("; ")}.`
      }
    ],
    isError: true,
    structuredContent: ignoredArgumentsStructured(report)
  };
}

// dist/mcp/build-staleness.js
import * as fs24 from "node:fs";
var realStat = (targetPath) => {
  try {
    return fs24.statSync(targetPath);
  } catch {
    return void 0;
  }
};
function computeBuildStamp(entryPath, stat = realStat) {
  const info = stat(entryPath);
  if (!info)
    return void 0;
  return { entryPath, mtimeMs: info.mtimeMs, size: info.size };
}
function checkStaleness(stamp, stat = realStat) {
  const info = stat(stamp.entryPath);
  if (!info)
    return { stale: false, stamp, currentMtimeMs: void 0 };
  return {
    stale: info.mtimeMs !== stamp.mtimeMs || info.size !== stamp.size,
    stamp,
    currentMtimeMs: info.mtimeMs
  };
}
var StalenessGate = class {
  stamp;
  checkIntervalMs;
  stat;
  now;
  lastCheckedAt;
  lastVerdict = false;
  constructor(stamp, options = {}) {
    this.stamp = stamp;
    this.checkIntervalMs = options.checkIntervalMs ?? 1e3;
    this.stat = options.stat ?? realStat;
    this.now = options.now ?? Date.now;
  }
  /** True when the warm server's loaded code is known to be stale. */
  isStale() {
    if (!this.stamp)
      return false;
    const nowMs = this.now();
    if (this.lastCheckedAt !== void 0 && nowMs - this.lastCheckedAt < this.checkIntervalMs) {
      return this.lastVerdict;
    }
    this.lastCheckedAt = nowMs;
    this.lastVerdict = checkStaleness(this.stamp, this.stat).stale;
    return this.lastVerdict;
  }
};
function stalenessCheckEnabled(env = process.env) {
  return env.PI_LENS_WARM_STALENESS_CHECK !== "0";
}
var STALE_SERVED_BY_FRESH = "fresh (warm code stale \u2014 restart the Claude session to re-warm)";
var STALE_WARN_ONLY = "warmCodeStale: this result was served by the warm server's loaded code, which is older than the on-disk build (a rebuild or merge landed after this session started). Restart the Claude session to re-warm, or use pilens_rebuild + a tool that supports mode=fresh if available.";

// dist/mcp/server.js
console.log = (...args) => {
  console.error(...args);
};
var SERVER_NAME = "pi-lens-mcp";
var SERVER_VERSION = "0.1.0";
var FALLBACK_PROTOCOL_VERSION = "2025-06-18";
function resolveDefaultCwd() {
  const fromArg = process.argv.find((arg) => arg.startsWith("--cwd="))?.slice("--cwd=".length);
  return fromArg ?? process.env.PI_LENS_MCP_CWD ?? process.cwd();
}
var DEFAULT_CWD = path38.resolve(resolveDefaultCwd());
var lspReadyCwds = /* @__PURE__ */ new Set();
var SERVER_FILE = fileURLToPath3(import.meta.url);
var SERVER_DIR = path38.dirname(SERVER_FILE);
var WORKER_PATH = path38.join(SERVER_DIR, "worker.js");
var REBUILD_SCRIPT = resolveRebuildScript(SERVER_FILE);
var STALENESS_STAT_PATH = process.env.PI_LENS_MCP_STALENESS_STAT_PATH ?? SERVER_FILE;
var BUILD_STAMP = computeBuildStamp(STALENESS_STAT_PATH);
var stalenessIntervalOverride = (() => {
  const raw = process.env.PI_LENS_MCP_STALENESS_INTERVAL_MS;
  if (raw === void 0)
    return void 0;
  const parsed = Number(raw);
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
})();
var STALENESS_GATE = new StalenessGate(BUILD_STAMP, {
  checkIntervalMs: stalenessIntervalOverride
});
function isWarmBuildStale() {
  if (!stalenessCheckEnabled())
    return false;
  return STALENESS_GATE.isStale();
}
function findRepoRoot(start) {
  let dir = start;
  for (let depth = 0; depth < 6; depth++) {
    const pkgPath = path38.join(dir, "package.json");
    if (fs25.existsSync(pkgPath)) {
      try {
        const pkg = JSON.parse(fs25.readFileSync(pkgPath, "utf8"));
        if (pkg.name === "pi-lens")
          return dir;
      } catch {
      }
    }
    const parent = path38.dirname(dir);
    if (parent === dir)
      break;
    dir = parent;
  }
  return path38.resolve(start, "..", "..");
}
var REPO_ROOT = process.env.PI_LENS_MCP_REPO_ROOT ? path38.resolve(process.env.PI_LENS_MCP_REPO_ROOT) : findRepoRoot(SERVER_DIR);
async function ensureReady(cwd) {
  const normalized = path38.resolve(cwd);
  if (!shouldInitializeSessionRoot(normalized, lspReadyCwds)) {
    return;
  }
  try {
    await ensureLspConfig(normalized);
  } catch (err) {
    console.error(`[pi-lens-mcp] initLSPConfig failed for ${normalized}: ${err}`);
  }
  lspReadyCwds.add(normalized);
}
var autoSessionState = {
  attempted: false,
  succeeded: false,
  firedAt: null,
  error: null
};
var autoSessionInFlight = null;
function maybeAutoSessionStart() {
  if (process.env.PI_LENS_MCP_AUTO_SESSION !== "1")
    return;
  if (autoSessionInFlight || autoSessionState.succeeded)
    return;
  autoSessionState = {
    attempted: true,
    succeeded: false,
    firedAt: (/* @__PURE__ */ new Date()).toISOString(),
    error: null
  };
  autoSessionInFlight = ensureReady(DEFAULT_CWD).then(() => runSessionStart(DEFAULT_CWD)).then(() => {
    autoSessionState = { ...autoSessionState, succeeded: true };
    console.error("[pi-lens-mcp] auto session_start complete");
  }).catch((err) => {
    autoSessionState = { ...autoSessionState, error: String(err) };
    console.error(`[pi-lens-mcp] auto session_start failed: ${err}`);
  }).finally(() => {
    autoSessionInFlight = null;
  });
}
function getAutoSessionStatus() {
  if (process.env.PI_LENS_MCP_AUTO_SESSION !== "1")
    return null;
  return { ...autoSessionState };
}
var IPC_PATH = ipcPathForCwd(DEFAULT_CWD);
function rejectForeignTurnCwd(route, cwd) {
  const message = `${route} cwd outside this server's workspace: ${path38.resolve(cwd)} not within ${DEFAULT_CWD}`;
  console.error(`[pi-lens-mcp] rejected ${message}`);
  return message;
}
function isWithinServerWorkspace(cwd) {
  const target = path38.resolve(cwd);
  const rel = path38.relative(DEFAULT_CWD, target);
  return !rel.startsWith("..") && !path38.isAbsolute(rel);
}
function startIpcServer() {
  if (process.platform !== "win32") {
    try {
      fs25.unlinkSync(IPC_PATH);
    } catch {
    }
  }
  const requestQueue = createWarmIpcRequestQueue();
  const ipc = net.createServer((socket) => {
    socket.setEncoding("utf8");
    socket.on("data", createWarmIpcLineReader((line) => {
      void requestQueue.enqueue(async () => {
        try {
          if (isWarmBuildStale()) {
            console.error("[pi-lens-mcp] warm request: build stale, replying with an error");
            socket.end(`${JSON.stringify({ error: "warm build stale" })}
`);
            return;
          }
          const parsed = JSON.parse(line);
          if (parsed.route === "turn-end-ack") {
            if (parsed.version !== WARM_TURN_END_SCHEMA_VERSION || typeof parsed.deliveryId !== "string") {
              socket.end(`${JSON.stringify({ error: `turn-end ack schema ${parsed.version} != ${WARM_TURN_END_SCHEMA_VERSION}` })}
`);
              return;
            }
            const turnCwd = parsed.cwd ?? DEFAULT_CWD;
            if (!isWithinServerWorkspace(turnCwd)) {
              socket.end(`${JSON.stringify({ error: rejectForeignTurnCwd("turn-end-ack", turnCwd) })}
`);
              return;
            }
            const acknowledged = acknowledgeTurnEnd(turnCwd, parsed.deliveryId);
            socket.end(`${JSON.stringify({
              result: {
                route: "turn-end-ack",
                version: WARM_TURN_END_SCHEMA_VERSION,
                acknowledged
              }
            })}
`);
            return;
          }
          if (parsed.route === "turn-end") {
            if (parsed.version !== WARM_TURN_END_SCHEMA_VERSION) {
              socket.end(`${JSON.stringify({ error: `turn-end schema ${parsed.version} != ${WARM_TURN_END_SCHEMA_VERSION}` })}
`);
              return;
            }
            const turnCwd = parsed.cwd ?? DEFAULT_CWD;
            if (!isWithinServerWorkspace(turnCwd)) {
              socket.end(`${JSON.stringify({ error: rejectForeignTurnCwd("turn-end", turnCwd) })}
`);
              return;
            }
            if (socket.destroyed) {
              console.error(`[pi-lens-mcp] warm turn-end: client gone before the pass started (${turnCwd})`);
              return;
            }
            console.error(`[pi-lens-mcp] warm turn-end: ${turnCwd}`);
            await ensureReady(turnCwd);
            const delivery = await runTurnEndForIpc(turnCwd);
            const result2 = {
              route: "turn-end",
              version: WARM_TURN_END_SCHEMA_VERSION,
              turnEnd: delivery.outcome.turnEnd,
              tests: delivery.outcome.tests,
              deliveryId: delivery.deliveryId
            };
            socket.end(`${JSON.stringify({ result: result2 })}
`);
            return;
          }
          const req = parsed;
          console.error(`[pi-lens-mcp] warm analyze: ${req.file}`);
          const result = await analyzeFile(req.file, req.cwd, {
            registerTurnState: true,
            updateGraph: true
          });
          socket.end(`${JSON.stringify({ result })}
`);
        } catch (err) {
          socket.end(`${JSON.stringify({ error: String(err) })}
`);
        }
      });
    }, {
      label: "mcp-warm-server",
      // #3383: a peer that never terminates its request line gets the same
      // error reply every other unusable request gets, instead of this
      // process holding its bytes until the client's timeout.
      onOverflow: () => socket.end(`${JSON.stringify({ error: "warm request line exceeded the framing limit" })}
`)
    }));
    socket.on("error", () => {
    });
  });
  ipc.on("error", (err) => {
    console.error(`[pi-lens-mcp] IPC listener unavailable: ${err}`);
  });
  ipc.listen(IPC_PATH, () => {
    console.error(`[pi-lens-mcp] warm side-channel listening at ${IPC_PATH}`);
  });
  const cleanup = () => {
    try {
      ipc.close();
    } catch {
    }
    if (process.platform !== "win32") {
      try {
        fs25.unlinkSync(IPC_PATH);
      } catch {
      }
    }
  };
  process.on("exit", cleanup);
}
function send(message) {
  process.stdout.write(`${JSON.stringify(message)}
`);
}
function sendResult(id, result) {
  send({ jsonrpc: "2.0", id, result });
}
function sendError(id, code, message) {
  send({ jsonrpc: "2.0", id, error: { code, message } });
}
var GRAPH_STALENESS_THRESHOLD_MS = 10 * 6e4;
function formatRelativeAge(ms) {
  if (ms < 6e4)
    return `${Math.round(ms / 1e3)}s ago`;
  if (ms < 60 * 6e4)
    return `${Math.round(ms / 6e4)}m ago`;
  if (ms < 24 * 60 * 6e4)
    return `${Math.round(ms / (60 * 6e4))}h ago`;
  return `${Math.round(ms / (24 * 60 * 6e4))}d ago`;
}
function graphStalenessNote(builtAtIso, label) {
  if (!builtAtIso)
    return void 0;
  const builtAtMs = Date.parse(builtAtIso);
  if (!Number.isFinite(builtAtMs))
    return void 0;
  const ageMs = Date.now() - builtAtMs;
  if (ageMs < GRAPH_STALENESS_THRESHOLD_MS)
    return void 0;
  return `${label} last updated ${formatRelativeAge(ageMs)}; run pilens_analyze on recently-changed files, pilens_session_start, or pilens_rebuild to refresh it.`;
}
var cacheManager = new CacheManager();
var isLensGuardEnabled = () => Boolean(createMcpHost(void 0, DEFAULT_CWD).getFlag("lens-guard"));
var lensDiagnosticsTool = createLensDiagnosticsTool(
  cacheManager,
  () => DEFAULT_CWD,
  void 0,
  void 0,
  void 0,
  void 0,
  // #1413 surface parity: validate cached test-runner findings against the
  // same session identity the in-process path uses. Resolved lazily — until
  // an MCP session context exists there is no session to compare against and
  // validation skips the check, which is the honest classification.
  () => peekMcpSessionRuntime(),
  // #2860: without this, the default `() => true` applies on the MCP
  // surface and every confirmed-clean LSP probe unconditionally resyncs the
  // commit-gate `turn-end-findings` record even when the project has
  // `lens-guard` off — a config bypass on a durable cross-surface store.
  // Matches index.ts:1656 and the two sibling readers
  // (clients/runtime-tool-call.ts, clients/runtime-tool-result.ts).
  () => isLensGuardEnabled()
);
var astGrepClient = new AstGrepClient();
var astGrepSearchTool = createAstGrepSearchTool(astGrepClient);
var astGrepReplaceTool = createAstGrepReplaceTool(astGrepClient);
var lspNavigationTool = createLspNavigationTool((name, cwd) => createMcpHost(void 0, cwd ?? DEFAULT_CWD).getFlag(name));
function schemaWithCwd(parameters) {
  const p = parameters;
  return {
    type: "object",
    properties: {
      ...p.properties,
      cwd: {
        type: "string",
        description: "Project root (defaults to the server workspace)."
      }
    },
    ...p.required ? { required: p.required } : {}
  };
}
var ALL_TOOLS = [
  {
    name: "pilens_analyze",
    description: "Run pi-lens's per-edit dispatch pipeline on one file. Example: analyze `src/app.ts` after an edit.",
    inputSchema: {
      type: "object",
      properties: {
        file: {
          type: "string",
          description: "Path to the file to analyze (absolute, or relative to cwd)."
        },
        cwd: {
          type: "string",
          description: "Project root. Defaults to the server's workspace."
        },
        mode: {
          type: "string",
          enum: ["warm", "fresh"],
          description: "warm (default): run in this server process \u2014 fast, warm LSP, but reflects the code the server was started with. fresh: fork a worker that loads the freshly-built code from disk \u2014 slower, but reflects the latest commit (the honest review loop; pair with pilens_rebuild)."
        },
        flags: {
          type: "object",
          description: 'Optional pi-lens flag overrides for this run, e.g. {"no-lsp": true} to bench the non-LSP path.'
        }
      },
      required: ["file"]
    }
  },
  {
    name: "pilens_diagnostics",
    description: lensDiagnosticsTool.description,
    inputSchema: schemaWithCwd(lensDiagnosticsTool.parameters)
  },
  {
    name: "pilens_latency",
    description: "Return recent dispatch latency reports. Example: limit results to 5.",
    inputSchema: {
      type: "object",
      properties: {
        limit: {
          type: "number",
          description: "Max reports to return (default 5)."
        },
        file: {
          type: "string",
          description: "Only reports whose path ends with this."
        }
      }
    }
  },
  {
    name: "pilens_rebuild",
    description: "Rebuild pi-lens so later fresh analyses use the latest commit. Example: rebuild after changing a tool.",
    inputSchema: { type: "object", properties: {} }
  },
  {
    name: "pilens_project_scan",
    description: "Scan project files for structural and quality diagnostics. Example: cap the scan with `maxFiles: 20`.",
    inputSchema: {
      type: "object",
      properties: {
        cwd: { type: "string" },
        maxFiles: { type: "number", description: "Cap files scanned." },
        includeGenerated: {
          type: "boolean",
          description: "Scan WITHOUT the generated/artifact NAME-heuristic filter (lockfiles, gen.ts-style names, generated/ dirs, \u2026). Default false. Use when a scan's 'excluded by generated-name heuristics' notice suggests a real file was skipped."
        }
      }
    }
  },
  {
    name: "pilens_symbol_search",
    description: "Find relevant files by ranked identifier search. On a cold cache, project_report and symbol_search return available: false with a retry hint and start a non-blocking background build; module_report degrades to outline-only with cache freshness explicit. Example: search `authenticate user` before pilens_module_report.",
    inputSchema: {
      type: "object",
      properties: {
        query: {
          type: "string",
          description: "Identifier-ish query, e.g. 'authenticate user'. Mix in composable prefix filters: lang:<kind> (e.g. lang:jsts, lang:python \u2014 kinds from file-kinds.ts), file:<substr> (path substring), ext:<ext> (e.g. ext:ts or ext:.ts), each optionally negated with a leading '-' (-file:test). Filters apply before ranking; e.g. 'lang:jsts file:clients/ -file:test rank'. Unknown prefixes/kinds error with the supported list."
        },
        cwd: { type: "string" },
        limit: {
          type: "number",
          description: "Max files to return (default 20)."
        },
        paths: {
          type: "array",
          items: { type: "string" },
          description: "Glob array scoping hits to matching files \u2014 same shape/semantics as pilens_ast_grep_search's `paths` (a bare directory/file entry scopes its whole subtree). Filters before ranking, so scores within the scoped set are unaffected."
        },
        lang: {
          type: "string",
          description: "Restrict hits to one language, using the same identifiers as pilens_ast_grep_search's `lang` param (e.g. 'typescript', 'python', 'go')."
        }
      },
      required: ["query"]
    }
  },
  {
    name: "pilens_module_report",
    description: "Return a navigable source-module outline with references and read handles. An outline shows shape, not bodies, and does not satisfy read-before-edit; `read_symbol` and `read_enclosing` return body text and record read coverage. On a cold cache, project_report and symbol_search return available: false with a retry hint and start a non-blocking background build; module_report degrades to outline-only with cache freshness explicit. Example: use pilens_module_report on `src/app.ts` before pilens_read_symbol.",
    inputSchema: {
      type: "object",
      properties: {
        file: {
          type: "string",
          description: "File to report on (absolute or relative to cwd)."
        },
        cwd: { type: "string" },
        maxRefsPerSymbol: {
          type: "number",
          description: "Cap who-uses-this entries per symbol (default 10)."
        },
        focus: {
          type: "string",
          description: "Optional task hint used only to rank recommendedReads (does not expand scope or trigger scans)."
        },
        view: {
          type: "string",
          enum: ["summary", "default", "compact"],
          description: "Payload tier. summary returns top-level entries/recommendedReads with heavy callback/usedBy/blast-radius payloads omitted. compact (cheapest) returns a line-oriented TEXT rendering of the full report instead of JSON."
        },
        blastRadius: {
          type: "boolean",
          description: "Include the cross-file blast radius: transitive dependents aggregated to ranked file reads. Read-only over the cached graph (omitted when cold)."
        },
        blastRadiusDepth: {
          type: "number",
          description: "Max hops for the blast-radius walk (default 3). Only used with blastRadius."
        },
        callGraph: {
          type: "boolean",
          description: "Include bounded derived callers/callees from the cached FunctionCallGraph; cold or stale cache state is explicit."
        },
        maxCallGraphEntries: {
          type: "number",
          description: "Per-direction cap for call-graph relations (default 20)."
        }
      },
      required: ["file"]
    }
  },
  {
    name: "pilens_project_report",
    description: "Orient in a project from its review graph. On a cold cache, project_report and symbol_search return available: false with a retry hint and start a non-blocking background build; module_report degrades to outline-only with cache freshness explicit. Example: use pilens_project_report before choosing a file.",
    inputSchema: {
      type: "object",
      properties: {
        cwd: { type: "string" },
        limit: {
          type: "number",
          description: "Scales every ranked section's cap (default 10) \u2014 a single knob for all sections."
        },
        focus: {
          type: "string",
          description: "Optional task hint used only to re-rank sections toward relevant subsystems (does not expand scope or trigger scans)."
        },
        view: {
          type: "string",
          enum: ["default", "compact"],
          description: "Payload tier. compact (cheapest) returns a line-oriented TEXT rendering instead of JSON. Default returns JSON."
        }
      }
    }
  },
  {
    name: "pilens_read_symbol",
    description: "Return one symbol's verbatim source. An outline shows shape, not bodies, and does not satisfy read-before-edit; `read_symbol` and `read_enclosing` return body text and record read coverage. Example: use pilens_read_symbol after pilens_module_report identifies `parseConfig`.",
    inputSchema: {
      type: "object",
      properties: {
        file: { type: "string", description: "File containing the symbol." },
        symbol: {
          type: "string",
          description: "Exact symbol name to read, or a dotted `Class.method` to disambiguate a member."
        },
        kind: {
          type: "string",
          description: "Optional kind filter (e.g. function, interface, class) to disambiguate when multiple same-file symbols share the requested name."
        },
        cwd: { type: "string" }
      },
      required: ["file", "symbol"]
    }
  },
  {
    name: "pilens_read_enclosing",
    description: "Return the smallest symbol or callback enclosing a line. An outline shows shape, not bodies, and does not satisfy read-before-edit; `read_symbol` and `read_enclosing` return body text and record read coverage. Example: use pilens_read_enclosing after a diagnostic points to line 42.",
    inputSchema: {
      type: "object",
      properties: {
        file: {
          type: "string",
          description: "Absolute or workspace-relative path to the source file."
        },
        line: {
          type: "number",
          description: "1-based line number inside the desired symbol/callback."
        },
        cwd: { type: "string" },
        kinds: {
          type: "array",
          items: { type: "string" },
          description: "Optional kind filter, e.g. function, method, callback, class."
        },
        maxLines: {
          type: "number",
          description: "Optional maximum body size to return. Oversized matches obey onOversize."
        },
        onOversize: {
          type: "string",
          enum: ["error", "slice", "outline"],
          description: "Behavior when the enclosing body exceeds maxLines. error (default) returns metadata only; slice returns a bounded partial read around line; outline returns nested symbols/callbacks with read handles."
        },
        aroundLine: {
          type: "number",
          description: "Maximum lines for onOversize=slice; defaults to maxLines, then 80."
        }
      },
      required: ["file", "line"]
    }
  },
  {
    name: "pilens_health",
    description: "Return pi-lens runtime health for this server. Example: call it after a slow analysis.",
    inputSchema: { type: "object", properties: {} }
  },
  {
    name: "pilens_effective_config",
    description: "Explain resolved configuration and provenance. Response is redacted by construction: it contains no environment values, no command arguments beyond the binary, and home-relative paths; a tier-denied LSP decision cannot be lifted by a nearer config. Example: pass `file` to explain one selection.",
    inputSchema: {
      type: "object",
      properties: {
        cwd: { type: "string" },
        file: {
          type: "string",
          description: "Path to explain: adds the resolved language plus the per-server and per-runner selection decisions for it. Must resolve inside `cwd` (a sibling package in the same monorepo does not count) \u2014 a file outside it is rejected with the `cwd` it was measured against; re-query with `cwd` set to that file's own workspace instead."
        }
      }
    }
  },
  {
    name: "pilens_session_start",
    description: "Initialize pi-lens for a workspace. Example: run once before reviewing a project.",
    inputSchema: {
      type: "object",
      properties: { cwd: { type: "string" } }
    }
  },
  {
    name: "pilens_turn_end",
    description: "Summarize checks for files changed this turn. Example: pass `files` for an unanalysed file.",
    inputSchema: {
      type: "object",
      properties: {
        cwd: { type: "string" },
        files: {
          type: "array",
          items: { type: "string" },
          description: "Optional extra files to include (auto-registered ones are already picked up)."
        }
      }
    }
  },
  {
    name: "pilens_session_end",
    description: "End the MCP connection's telemetry session and write its bounded situational-tool telemetry line. This is terminal for the connection.",
    inputSchema: {
      type: "object",
      properties: {}
    }
  },
  {
    name: "pilens_ast_grep_search",
    description: "Search code by AST structure rather than text. Example: find calls with `console.log($MSG)`.",
    inputSchema: schemaWithCwd(astGrepSearchTool.parameters)
  },
  {
    name: "pilens_ast_grep_replace",
    description: "Find and rewrite code by AST structure; preview by default. Example: set `apply: false` to inspect a diff.",
    inputSchema: schemaWithCwd(astGrepReplaceTool.parameters)
  },
  {
    name: "pilens_lsp_navigation",
    description: 'Navigate source with language-server operations. Example: use `{operation: "references", path: "src/app.ts", line: 12}`.',
    inputSchema: schemaWithCwd(lspNavigationTool.parameters)
  }
];
var TOOLS = canRebuildPiLens(REPO_ROOT) ? ALL_TOOLS : ALL_TOOLS.filter((tool) => tool.name !== "pilens_rebuild");
function enabledToolsForCwd(cwd) {
  const global = loadPiLensGlobalConfig();
  const project = loadPiLensProjectConfig(cwd);
  return TOOLS.filter((tool) => {
    const entry = toolRegistryEntryForMcp(tool.name);
    return entry !== void 0 && resolveLensToolEnabled(entry.name, global, project.raw);
  });
}
function formatAnalyze(result, cwd, mode, servedBy) {
  const lspNote = result.lsp ? ` \xB7 lsp ${result.lsp.diagnosticCount} (${result.lsp.status}${result.lsp.failureKind ? `: ${result.lsp.failureKind}` : ""}, ${result.lsp.durationMs}ms)` : "";
  const summary = `${path38.relative(cwd, result.filePath) || result.filePath} [${mode}] \u2014 ${result.counts.blockers} blocking, ${result.counts.warnings} warning(s), ${result.counts.advisories} advisory(ies), ${result.counts.diagnostics} total` + (result.latency ? ` \xB7 ${result.latency.totalDurationMs}ms` : "") + lspNote + (result.counts.fixed > 0 ? ` \xB7 ${result.counts.fixed} auto-fixed` : "") + (servedBy ? `

servedBy: ${servedBy}` : "");
  return renderToolText(summary, servedBy ? { ...result, servedBy } : result);
}
function healthConfigLine(provenance) {
  if (!provenance)
    return "Config: unavailable (resolution failed)";
  const tiers = Object.entries(provenance.tiers).filter(([, count]) => count > 0).map(([tier, count]) => `${tier} ${count}`).join(" \xB7 ");
  const codes = Object.entries(provenance.codes).map(([code, count]) => `${code}\xD7${count}`).join(", ");
  const notices = codes.length > 0 ? ` \xB7 notices ${codes}` : "";
  const documents = `${provenance.documents} file(s)`;
  return `Config: ${documents} \xB7 ${tiers} leaf/leaves${notices}`;
}
function effectiveServerLine(server) {
  const mark = server.selected ? "\u2713" : "\u2717";
  const decided = server.decidedBy;
  if (!decided)
    return `  ${mark} ${server.id} \u2014 ${server.reason}`;
  const file = decided.file === void 0 ? "" : ` ${decided.file}`;
  const source = `${decided.tier}${file} \u2192 ${decided.key}`;
  return `  ${mark} ${server.id} \u2014 ${server.reason} (${source})`;
}
function effectiveFileHeading(file) {
  const language = file.language === void 0 ? "" : ` \u2014 ${file.language}`;
  const kind = file.kind === void 0 ? "" : ` (kind ${file.kind})`;
  return `File: ${file.path}${language}${kind}`;
}
function effectiveDocumentLine(document) {
  const legacy = document.legacy ? " (legacy location \u2014 scheduled for removal)" : "";
  return `  ${document.tier}: ${document.file}${legacy}`;
}
async function callTool(name, args) {
  if (name === "pilens_lsp_diagnostics") {
    recordDegradationOnce({
      kind: "lsp-diagnostics-compatibility",
      subject: "lsp_diagnostics",
      reason: "retired tool name redirected to pilens_diagnostics source=lsp"
    });
    name = "pilens_diagnostics";
    args = {
      ...args,
      source: "lsp",
      // #2860: the retired tool's contract was always "path or paths is
      // required" (tools/lsp-diagnostics.ts's own early return) — it never
      // had a bare "sweep the whole workspace" mode. `scope: "paths"` is a
      // no-op for source=lsp (lens-diagnostics.ts only special-cases
      // scope==="workspace"), so whatever `path`/`paths` the caller sent
      // (or didn't) passes straight through to the probe's own path/paths
      // handling unchanged, reproducing master's behavior exactly —
      // including its error when neither is present. Mapping absent
      // `paths` to `scope:"workspace"` here previously caused every
      // no-args call to substitute `cwd` and run a whole-project LSP sweep
      // instead (root-eviction smoke: 129 sweeps, 180s timeout x3).
      scope: "paths"
    };
  }
  if (name === "pilens_ast_grep_dump") {
    recordDegradationOnce({
      kind: "ast-grep-dump-compatibility",
      subject: "ast_grep_dump",
      reason: "retired tool name redirected to ast_grep_search dump mode"
    });
    return astGrepDumpCompatibilityResult(args, "mcp");
  }
  if (name === "pilens_analyze") {
    const file = args.file;
    if (typeof file !== "string" || file.length === 0) {
      return {
        ...renderToolText("pilens_analyze requires a 'file' string."),
        isError: true
      };
    }
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    const requestedMode = args.mode === "fresh" ? "fresh" : "warm";
    const flags = args.flags && typeof args.flags === "object" ? args.flags : void 0;
    const forcedFresh = requestedMode === "warm" && isWarmBuildStale();
    const mode = requestedMode === "fresh" || forcedFresh ? "fresh" : "warm";
    if (mode === "fresh") {
      const outcome = await analyzeFileFresh(WORKER_PATH, file, cwd, { flags });
      if (outcome.error || !outcome.result) {
        return {
          ...renderToolText(`fresh analyze failed: ${outcome.error ?? "no result"}`),
          isError: true
        };
      }
      return formatAnalyze(outcome.result, cwd, "fresh", forcedFresh ? STALE_SERVED_BY_FRESH : void 0);
    }
    await ensureReady(cwd);
    const result = await analyzeFile(file, cwd, {
      flags,
      registerTurnState: true,
      updateGraph: true
    });
    return formatAnalyze(result, cwd, "warm");
  }
  if (name === "pilens_rebuild") {
    const outcome = await runRebuild(REPO_ROOT, REBUILD_SCRIPT);
    if (!outcome.packageManager) {
      return {
        ...renderToolText(outcome.output, {
          ok: false,
          script: outcome.script,
          repoRoot: REPO_ROOT
        }),
        isError: true
      };
    }
    const runCmd = `${outcome.packageManager} run ${outcome.script}`;
    const headline = outcome.ok ? `\u2713 rebuild succeeded (${runCmd}, ${outcome.durationMs}ms). Fresh analyses now reflect the latest build.` : `\u2717 rebuild FAILED (${runCmd}, ${outcome.durationMs}ms).`;
    return {
      ...renderToolText(outcome.ok ? headline : `${headline}

${outcome.output}`, {
        ok: outcome.ok,
        script: outcome.script,
        packageManager: outcome.packageManager,
        durationMs: outcome.durationMs,
        repoRoot: REPO_ROOT
      }),
      isError: !outcome.ok
    };
  }
  if (name === "pilens_project_scan") {
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    await ensureReady(cwd);
    const maxFiles = typeof args.maxFiles === "number" && Number.isFinite(args.maxFiles) ? Math.max(1, Math.floor(args.maxFiles)) : void 0;
    const includeGenerated = args.includeGenerated === true;
    const snapshot = await projectScan(cwd, maxFiles, includeGenerated);
    if (snapshot.unsafeRoot) {
      return renderToolText(`Refused to scan: the working directory (${cwd}) resolves at or above the home directory, so a project scan would walk every unrelated tree under it. Run pilens_project_scan from inside a project directory. This is NOT a clean result \u2014 nothing was scanned.`, { filesScanned: 0, unsafeRoot: true, cwd }, true);
    }
    const { deduped, byRule, byFile } = summarizeScan(snapshot.diagnostics);
    const topRules = Object.entries(byRule).sort((a, b) => b[1] - a[1]);
    const topFiles = Object.entries(byFile).sort((a, b) => b[1] - a[1]).slice(0, 15).map(([file, count]) => ({ file: path38.relative(cwd, file), count }));
    const summaryLines = [
      `Scanned ${snapshot.filesScanned} file(s) [${snapshot.runners.join(", ")}] \u2192 ${deduped.length} unique diagnostic(s)` + (snapshot.diagnostics.length !== deduped.length ? ` (${snapshot.diagnostics.length} raw, ${snapshot.diagnostics.length - deduped.length} duplicate)` : ""),
      ...topRules.slice(0, 12).map(([rule, count]) => `  ${count}\xD7 ${rule}`)
    ];
    const truncationNotice = scanTruncationNotice(snapshot);
    if (truncationNotice)
      summaryLines.push(truncationNotice);
    const skipNotice = generatedSkipNotice(snapshot, "mcp");
    if (skipNotice)
      summaryLines.push(skipNotice);
    return renderToolText(summaryLines.join("\n"), {
      filesScanned: snapshot.filesScanned,
      runners: snapshot.runners,
      uniqueDiagnostics: deduped.length,
      rawDiagnostics: snapshot.diagnostics.length,
      byRule,
      topFiles,
      sample: deduped.slice(0, 40),
      ...snapshot.scanTruncated ? { scanTruncated: true } : {},
      ...snapshot.treeSitterStatus ? { treeSitterStatus: snapshot.treeSitterStatus } : {},
      ...snapshot.generatedFileSkips ? { generatedFileSkips: snapshot.generatedFileSkips } : {},
      ...snapshot.generatedNameOnlySkips ? { generatedNameOnlySkips: snapshot.generatedNameOnlySkips } : {},
      ...snapshot.generatedDirSkips ? { generatedDirSkips: snapshot.generatedDirSkips } : {}
    });
  }
  if (name === "pilens_symbol_search") {
    const query = typeof args.query === "string" ? args.query : "";
    if (!query.trim())
      return renderToolText("Provide a non-empty `query`.");
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    const limit = typeof args.limit === "number" && Number.isFinite(args.limit) ? Math.max(1, Math.floor(args.limit)) : 20;
    const paths = Array.isArray(args.paths) ? args.paths.filter((p) => typeof p === "string") : void 0;
    const lang = typeof args.lang === "string" ? args.lang : void 0;
    const { available, results, hint, unavailableReason, coverage, snapshotGeneratedAt } = await symbolSearch(query, cwd, limit, { paths, lang });
    if (!available) {
      return renderToolText(hint ?? "No word index for this workspace yet \u2014 run pilens_session_start first.", { available: false, query, hint, unavailableReason }, true);
    }
    const stalenessNote = graphStalenessNote(snapshotGeneratedAt, "Project snapshot");
    if (results.length === 0) {
      return renderToolText(`No files matched "${query}".` + (coverage ? `
Index covers ${coverage.files} files${coverage.truncated ? " (capped \u2014 results may be incomplete)" : ""}.` : "") + (stalenessNote ? `

${stalenessNote}` : ""), {
        available: true,
        query,
        results: [],
        coverage,
        ...stalenessNote ? { staleness: stalenessNote } : {}
      }, true);
    }
    const lines = [
      `Top ${results.length} file(s) for "${query}":`,
      ...results.map((result, i) => `  ${i + 1}. ${path38.relative(cwd, result.file)} (score ${result.score.toFixed(2)}, ${result.hits} hit(s), line ${result.startLine})`),
      ...stalenessNote ? ["", stalenessNote] : [],
      ...coverage ? [
        `Index covers ${coverage.files} files${coverage.truncated ? " (capped \u2014 results may be incomplete)" : ""}.`
      ] : []
    ];
    return renderToolText(lines.join("\n"), {
      query,
      coverage,
      results: results.map((result) => {
        const relFile = path38.relative(cwd, result.file);
        return {
          file: relFile,
          score: result.score,
          hits: result.hits,
          startLine: result.startLine,
          endLine: result.endLine,
          ...result.annotations ? { annotations: result.annotations } : {},
          // #771: machine-actionable discovery-funnel hint, mirroring the
          // pi-tool-surface's suggestedNext (tools/symbol-search.ts).
          suggestedNext: { tool: "pilens_module_report", path: relFile }
        };
      }),
      ...stalenessNote ? { staleness: stalenessNote } : {}
    }, true);
  }
  if (name === "pilens_module_report") {
    const file = typeof args.file === "string" ? args.file : "";
    if (!file.trim())
      return { ...renderToolText("Provide a `file`."), isError: true };
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    const maxRefsPerSymbol = typeof args.maxRefsPerSymbol === "number" && Number.isFinite(args.maxRefsPerSymbol) ? Math.max(1, Math.floor(args.maxRefsPerSymbol)) : void 0;
    const blastRadius = args.blastRadius === true;
    const blastRadiusDepth = typeof args.blastRadiusDepth === "number" && Number.isFinite(args.blastRadiusDepth) ? Math.max(1, Math.floor(args.blastRadiusDepth)) : void 0;
    const callGraph = args.callGraph === true;
    const maxCallGraphEntries = typeof args.maxCallGraphEntries === "number" && Number.isFinite(args.maxCallGraphEntries) ? Math.max(1, Math.floor(args.maxCallGraphEntries)) : void 0;
    const view = args.view === "summary" || args.view === "compact" ? args.view : void 0;
    const focus = typeof args.focus === "string" ? args.focus : void 0;
    const report = await moduleReport(file, cwd, {
      maxRefsPerSymbol,
      blastRadius,
      blastRadiusDepth,
      callGraph,
      maxCallGraphEntries,
      view,
      focus
    });
    if (!report.available) {
      return {
        ...renderToolText(`No module report for ${path38.relative(cwd, path38.resolve(cwd, file))} \u2014 not a symbol-bearing file, or unreadable.`, report, true),
        isError: true
      };
    }
    const graphStaleness = graphStalenessNote(report.graphBuiltAt, "Review graph");
    const summary = `${path38.relative(cwd, report.path) || report.path} [${report.staleness}] \u2014 ${report.summary.symbols} symbol(s), ${report.summary.exports} exported, ${report.api.length} in public API` + (graphStaleness ? `

${graphStaleness}` : "");
    if (view === "compact") {
      const compactText = renderCompactModuleReport(report);
      return {
        content: [
          {
            type: "text",
            text: graphStaleness ? `${compactText}

${graphStaleness}` : compactText
          }
        ]
      };
    }
    return renderToolText(summary, graphStaleness ? { ...report, graphStalenessNote: graphStaleness } : report, true);
  }
  if (name === "pilens_project_report") {
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    const limit = typeof args.limit === "number" && Number.isFinite(args.limit) ? Math.max(1, Math.floor(args.limit)) : void 0;
    const focus = typeof args.focus === "string" ? args.focus : void 0;
    const view = args.view === "compact" ? "compact" : void 0;
    const report = await projectReport(cwd, { limit, focus, view });
    if (!report.available) {
      return {
        ...renderToolText(report.hint ?? "No review graph cached for this workspace yet.", report),
        isError: true
      };
    }
    const graphStaleness = report.trust ? graphStalenessNote(report.trust.graphBuiltAt, "Review graph") : void 0;
    if (view === "compact") {
      const compactText = renderCompactProjectReport(report);
      return {
        content: [
          {
            type: "text",
            text: graphStaleness ? `${compactText}

${graphStaleness}` : compactText
          }
        ]
      };
    }
    const summary = `Project report \u2014 ${report.hubs?.length ?? 0} hub(s), ${report.entryPoints?.length ?? 0} entry point(s), ${report.riskHotspots?.length ?? 0} risk hotspot(s)` + (graphStaleness ? `

${graphStaleness}` : "");
    return renderToolText(summary, graphStaleness ? { ...report, graphStalenessNote: graphStaleness } : report, true);
  }
  if (name === "pilens_read_symbol") {
    const file = typeof args.file === "string" ? args.file : "";
    const symbol = typeof args.symbol === "string" ? args.symbol : "";
    if (!file.trim() || !symbol.trim()) {
      return { ...renderToolText("Provide `file` and `symbol`."), isError: true };
    }
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    const kind = typeof args.kind === "string" ? args.kind : void 0;
    const result = await readSymbol(file, symbol, cwd, { kind });
    if (!result.found) {
      const suggestionSuffix = result.suggestions?.length ? ` Did you mean: ${result.suggestions.join(", ")}?` : " Use pilens_module_report to list symbols.";
      return {
        ...renderToolText(`Symbol "${symbol}" not found in ${path38.basename(file)}.${suggestionSuffix}`, {
          found: false,
          ...result.suggestions ? { suggestions: result.suggestions } : {}
        }),
        isError: true
      };
    }
    const sigSuffix = result.signature ? `  ${result.signature}` : "";
    const ambiguityNote = result.ambiguous ? ` (${result.ambiguous.count} matches \u2014 returned the ${result.kind}; pass \`kind\` to disambiguate: ${result.ambiguous.kinds.join(", ")})` : "";
    const header = `${result.kind} ${result.name}${ambiguityNote}${sigSuffix}  ${path38.relative(cwd, result.path)}:${result.startLine}-${result.endLine}`;
    return {
      content: [
        {
          type: "text",
          text: `${header}

${result.source ?? ""}`
        }
      ]
    };
  }
  if (name === "pilens_read_enclosing") {
    const file = typeof args.file === "string" ? args.file : "";
    const line = typeof args.line === "number" ? args.line : Number.NaN;
    if (!file.trim() || !Number.isFinite(line)) {
      return {
        ...renderToolText("Provide `file` and a numeric `line`."),
        isError: true
      };
    }
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    const kinds = Array.isArray(args.kinds) ? args.kinds.filter((k) => typeof k === "string") : void 0;
    const maxLines = typeof args.maxLines === "number" && Number.isFinite(args.maxLines) ? Math.max(1, Math.floor(args.maxLines)) : void 0;
    const onOversize = args.onOversize === "error" || args.onOversize === "slice" || args.onOversize === "outline" ? args.onOversize : void 0;
    const aroundLine = typeof args.aroundLine === "number" && Number.isFinite(args.aroundLine) ? Math.max(1, Math.floor(args.aroundLine)) : void 0;
    const result = await readEnclosing(file, line, cwd, {
      kinds,
      maxLines,
      onOversize,
      aroundLine
    });
    if (!result.found) {
      const warningSuffix = result.warnings?.length ? ` Warnings: ${result.warnings.join("; ")}` : "";
      const outlineSuffix = result.outline?.length ? `

Nested outline:
${JSON.stringify(result.outline)}` : "";
      const text = result.error ? `Could not read enclosing range in ${path38.basename(file)}:${result.line}: ${result.error}${warningSuffix}${outlineSuffix}` : `No enclosing symbol/callback found in ${path38.basename(file)}:${result.line}.${warningSuffix}`;
      return {
        ...renderToolText(text, { found: false, line: result.line }),
        isError: true
      };
    }
    const range = result.partial ? `${result.startLine}-${result.endLine} (partial of ${result.enclosingStartLine}-${result.enclosingEndLine})` : `${result.startLine}-${result.endLine}`;
    const header = `${result.kind} ${result.name}  ${path38.relative(cwd, result.path)}:${range}`;
    return {
      content: [
        {
          type: "text",
          text: `${header}

${result.source ?? ""}`
        }
      ]
    };
  }
  if (name === "pilens_health") {
    const { aliveClients, servers, brokenServers } = lspStatus();
    const last = recentLatency(1)[0];
    const stats = diagnosticStats();
    const autoSession = getAutoSessionStatus();
    const treeSitter = treeSitterRuntimeStatus();
    const degradations = getDegradationSummary();
    const turnEnd = readTurnEndStatus(DEFAULT_CWD) ?? null;
    const configProvenance = await effectiveConfig({ cwd: DEFAULT_CWD }).then((view) => ({
      documents: view.documents.length,
      tiers: view.provenanceCounts,
      codes: view.recordCounts
    })).catch(() => null);
    const footprint = await resourceFootprint().catch(() => null);
    const lines = [
      treeSitter.wasmAborted ? `Tree-sitter: DEGRADED \u2014 WASM runtime aborted${treeSitter.abortedAt ? ` at ${treeSitter.abortedAt}` : ""}; restart this server to recover` : "Tree-sitter: available",
      `LSP: ${aliveClients} alive client(s)`,
      ...servers.flatMap((server) => [
        `  ${server.connected ? "\u2713" : "\u2717"} ${server.serverId} (${server.root})`,
        ...server.pullFailureHistory.slice(-1).map((failure) => `    \u26A0 diagnostics pull failed: ${failure.method}${failure.code !== void 0 ? ` (${failure.code})` : ""} \u2014 ${failure.message.length > 200 ? `${failure.message.slice(0, 200)}\u2026` : failure.message}`)
      ]),
      ...renderLspBrokenStatusLines(brokenServers),
      ...renderDegradationLines(degradations),
      last ? `Last dispatch: ${path38.basename(last.filePath)} \u2014 ${last.totalDurationMs}ms, ${last.totalDiagnostics} diagnostic(s)` : "Last dispatch: none yet",
      `Diagnostics this session: ${stats.totalShown} shown \xB7 ${stats.totalAutoFixed} auto-fixed \xB7 ${stats.totalUnresolved} unresolved`,
      autoSession ? `Auto session_start: ${autoSession.succeeded ? "succeeded" : autoSession.error ? "FAILED" : autoSession.attempted ? "in progress" : "not yet attempted"}${autoSession.firedAt ? ` (fired ${autoSession.firedAt})` : ""}${autoSession.error ? ` \u2014 ${autoSession.error}` : ""}` : "Auto session_start: disabled (PI_LENS_MCP_AUTO_SESSION not set)",
      turnEnd ? `Stop-hook turn-end: ${turnEnd.ran} ran \xB7 ${turnEnd.skipped} skipped${turnEnd.lastRunAt ? ` (last ran ${turnEnd.lastRunAt})` : ""}${turnEnd.lastSkipReason ? ` \u2014 last skip: ${turnEnd.lastSkipReason}${turnEnd.lastSkipAt ? ` at ${turnEnd.lastSkipAt}` : ""}` : ""}` : "Stop-hook turn-end: no activity recorded (hook not installed, or no Stop yet)",
      turnEnd?.failed ? `Analyzer invocations: ${turnEnd.failed} failed; last ${turnEnd.lastFailureOperation} failure at ${turnEnd.lastFailureAt}: ${turnEnd.lastFailureReason}` : "Analyzer invocations: no failures recorded",
      healthConfigLine(configProvenance),
      footprint ? `Resource footprint: ${footprint.instanceCount} pi-lens instance(s) \xB7 ${(footprint.totalRssBytes / 1024 / 1024).toFixed(0)}MB RSS \xB7 ${footprint.totalCpuPercent.toFixed(1)}% CPU \xB7 ${footprint.totalLspChildCount} LSP child process(es)` : "Resource footprint: unavailable (instance registry unreadable)"
    ];
    return renderToolText(lines.join("\n"), {
      aliveClients,
      servers,
      brokenServers,
      lastDispatch: last ? {
        filePath: last.filePath,
        totalDurationMs: last.totalDurationMs,
        totalDiagnostics: last.totalDiagnostics
      } : void 0,
      diagnostics: {
        shown: stats.totalShown,
        autoFixed: stats.totalAutoFixed,
        unresolved: stats.totalUnresolved
      },
      autoSession,
      treeSitter,
      turnEnd,
      resourceFootprint: footprint,
      degradations,
      configProvenance
    });
  }
  if (name === "pilens_effective_config") {
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    const view = await effectiveConfig({
      cwd,
      ...typeof args.file === "string" ? { file: args.file } : {},
      redact: true
    });
    const fileLines = view.file === void 0 ? [] : isEffectiveFileViewError(view.file) ? [`File: ${view.file.error}`] : [
      effectiveFileHeading(view.file),
      ...view.file.servers.filter((server) => server.reason !== "extension-mismatch").map(effectiveServerLine),
      ...view.file.tools.map((tool) => `  ${tool.selected ? "\u2713" : "\u2717"} ${tool.id} \u2014 ${tool.reason}`)
    ];
    const lines = [
      `Config: ${view.documents.length} file(s) contributing, ${view.provenance.length} resolved leaf/leaves`,
      ...view.documents.map(effectiveDocumentLine),
      ...fileLines
    ];
    return renderToolText(lines.join("\n"), view);
  }
  if (name === "pilens_diagnostics") {
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    await ensureReady(cwd);
    const out = await lensDiagnosticsTool.execute(
      "mcp",
      args,
      new AbortController().signal,
      void 0,
      // #2535: the shared tool renders host-callable advisory names.
      { cwd, host: "mcp" }
    );
    return out;
  }
  if (name === "pilens_latency") {
    const limit = typeof args.limit === "number" && Number.isFinite(args.limit) ? Math.max(1, Math.floor(args.limit)) : 5;
    const fileFilter = typeof args.file === "string" ? args.file : void 0;
    const recent = recentLatency(limit, fileFilter);
    const summary = recent.length === 0 ? "No dispatch latency reports yet." : recent.map((report) => `${path38.basename(report.filePath)}: ${report.totalDurationMs}ms (${report.totalDiagnostics} diag${report.stoppedEarly ? ", stopped early" : ""})`).join("\n");
    return renderToolText(summary, recent);
  }
  if (name === "pilens_session_start") {
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    await ensureReady(cwd);
    const outcome = await runSessionStart(cwd);
    const lines = [
      `Session started for ${cwd}.`,
      `LSP: ${outcome.aliveLspClients} alive client(s) (warming continues in background).`,
      outcome.errorDebtBaseline ? `Error-debt baseline: tests ${outcome.errorDebtBaseline.testsPassed ? "pass" : "FAIL"}, build ${outcome.errorDebtBaseline.buildPassed ? "pass" : "FAIL"}.` : "Error-debt baseline: computing in background.",
      "knip/jscpd/type-coverage/dep scans run in background \u2014 query pilens_diagnostics shortly.",
      outcome.guidance ? `
${outcome.guidance}` : ""
    ];
    return renderToolText(lines.filter(Boolean).join("\n"), outcome);
  }
  if (name === "pilens_session_end") {
    endSituationalToolTelemetry();
    return renderToolText("Session ended.");
  }
  if (name === "pilens_turn_end") {
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    await ensureReady(cwd);
    const files = Array.isArray(args.files) ? args.files.filter((file) => typeof file === "string") : [];
    const outcome = await runTurnEnd(cwd, files);
    const parts = [
      `Turn-end over ${outcome.filesRegistered} file(s).`,
      outcome.turnEnd ?? "No turn-end advisory.",
      outcome.tests ? `
Tests:
${outcome.tests}` : ""
    ];
    return renderToolText(parts.filter(Boolean).join("\n"), outcome);
  }
  if (name === "pilens_ast_grep_search" || name === "pilens_ast_grep_replace") {
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    const tool = name === "pilens_ast_grep_search" ? astGrepSearchTool : astGrepReplaceTool;
    const out = await tool.execute("mcp", args, new AbortController().signal, void 0, { cwd, resultMaxItems: Number.POSITIVE_INFINITY });
    return out;
  }
  if (name === "pilens_lsp_navigation") {
    const cwd = typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD;
    await ensureReady(cwd);
    const tool = lspNavigationTool;
    const out = await tool.execute("mcp", args, new AbortController().signal, void 0, { cwd });
    return out;
  }
  return { ...renderToolText(`Unknown tool: ${name}`), isError: true };
}
var WARN_ONLY_STALE_TOOLS = /* @__PURE__ */ new Set([
  "pilens_module_report",
  "pilens_project_report",
  "pilens_symbol_search",
  "pilens_project_scan",
  "pilens_health",
  "pilens_latency",
  "pilens_session_start",
  "pilens_turn_end",
  "pilens_ast_grep_search",
  "pilens_ast_grep_replace",
  "pilens_lsp_navigation",
  "pilens_read_symbol",
  "pilens_read_enclosing",
  "pilens_effective_config"
]);
function withStaleWarning(result) {
  if (result.content.length === 0)
    return result;
  const last = result.content[result.content.length - 1];
  return {
    ...result,
    content: [
      ...result.content.slice(0, -1),
      {
        ...last,
        text: `${last.text}

warmCodeStale: true
${STALE_WARN_ONLY}`
      }
    ]
  };
}
async function handleRequest(request) {
  const { id, method, params } = request;
  const isNotification = id === void 0;
  switch (method) {
    case "initialize": {
      startSituationalToolTelemetrySession("mcp");
      const requested = params?.protocolVersion;
      sendResult(id ?? null, {
        protocolVersion: typeof requested === "string" ? requested : FALLBACK_PROTOCOL_VERSION,
        capabilities: { tools: {} },
        serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
      });
      maybeAutoSessionStart();
      return;
    }
    case "notifications/initialized":
    case "initialized":
      return;
    // notification — no response
    case "ping":
      if (!isNotification)
        sendResult(id ?? null, {});
      return;
    case "tools/list":
      sendResult(id ?? null, { tools: enabledToolsForCwd(DEFAULT_CWD) });
      return;
    case "tools/call": {
      const name = params?.name;
      const rawArguments = params?.arguments;
      const argumentsAreObject = typeof rawArguments === "object" && rawArguments !== null && !Array.isArray(rawArguments);
      const args = argumentsAreObject ? rawArguments : {};
      if (typeof name !== "string") {
        sendError(id ?? null, -32602, "tools/call requires a string 'name'");
        return;
      }
      if (!argumentsAreObject && rawArguments !== void 0 && rawArguments !== null && !(Array.isArray(rawArguments) && rawArguments.length === 0)) {
        sendError(id ?? null, -32602, "tools/call 'arguments' must be an object");
        return;
      }
      const enabledName = name === "pilens_lsp_diagnostics" ? "pilens_diagnostics" : name;
      if (name !== "pilens_ast_grep_dump" && name !== "pilens_rebuild" && !enabledToolsForCwd(typeof args.cwd === "string" ? args.cwd : DEFAULT_CWD).some((tool) => tool.name === enabledName)) {
        sendResult(id ?? null, stripResultDetails(finalizeToolResult({
          ...renderToolText(`Unknown or disabled tool: ${name}`),
          isError: true
        })));
        return;
      }
      const inputSchema = ALL_TOOLS.find((tool) => tool.name === enabledName)?.inputSchema;
      const argumentReport = inputSchema ? findIgnoredArguments(inputSchema, args) : void 0;
      if (argumentReport) {
        incrementDegradationCount({
          kind: "mcp-ignored-arguments",
          subject: name,
          reason: `ignored argument(s): ${ignoredArgumentsStructured(argumentReport).ignoredArguments.join(", ")}`
        });
        const refused = refusalResult(name, argumentReport);
        if (refused) {
          sendResult(id ?? null, stripResultDetails(finalizeToolResult(refused)));
          return;
        }
      }
      const entry = toolRegistryEntryForMcp(name);
      if (entry && "situational" in entry && entry.situational) {
        startSituationalToolTelemetrySession("mcp");
        observeSituationalToolCall(entry.name);
      }
      maybeAutoSessionStart();
      try {
        let result = await callTool(name, args);
        if (WARN_ONLY_STALE_TOOLS.has(name) && !result.isError && isWarmBuildStale()) {
          result = withStaleWarning(result);
        }
        if (argumentReport)
          result = withIgnoredArguments(result, name, argumentReport);
        const delivery = finalizeToolResultWithDelivery(result);
        sendResult(id ?? null, stripResultDetails(delivery.result));
      } catch (err) {
        sendResult(id ?? null, stripResultDetails(finalizeToolResult({
          ...renderToolText(`pi-lens tool '${name}' failed: ${err.message}`),
          isError: true
        })));
      }
      return;
    }
    default:
      if (!isNotification)
        sendError(id ?? null, -32601, `Method not found: ${method}`);
      return;
  }
}
process.stdin.setEncoding("utf8");
process.stdin.on("data", createWarmIpcLineReader((raw) => {
  const line = raw.trim();
  if (line.length === 0)
    return;
  let request;
  try {
    request = JSON.parse(line);
  } catch {
    sendError(null, -32700, "Parse error");
  }
  if (request)
    void handleRequest(request);
}, {
  label: "mcp-stdio",
  continuous: true,
  onOverflow: () => sendError(null, -32700, "Parse error: request line exceeded the framing limit")
}));
process.stdin.on("end", () => {
  endSituationalToolTelemetry();
  void flushExtensionLog().finally(() => process.exit(0));
});
startIpcServer();
console.error(`[pi-lens-mcp] ready (cwd=${DEFAULT_CWD})`);