{
  "version": 3,
  "sources": ["../../../src/hooks/ts/pre-tool-use/mcp-deferred-gate.ts", "../../../src/hooks/ts/lib/mcp-deferred-gate.ts", "../../../src/hooks/ts/lib/project-utils/paths.ts", "../../../src/hooks/ts/lib/pai-paths.ts", "../../../src/session/checkpoint-block.ts"],
  "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * mcp-deferred-gate.ts \u2014 PreToolUse hook on mcp__* tools.\n *\n * Claude Code defers MCP tool schemas: calling a deferred mcp__server__tool\n * without surfacing it via ToolSearch fails input validation, and sessions\n * misread that as \"MCP server disconnected\" \u2014 as they do the stale handles a\n * mid-session server re-registration leaves behind (\"deferred tools are no\n * longer available\"). PreToolUse is the only event on\n * the attempt path \u2014 when the schema gate rejects the call the tool never\n * runs, so no PostToolUse fires to correct it. A deny here lands the remedy\n * in the same channel the model already reads (same mechanism as\n * route-agents-to-worker), and the transcript evidence check in\n * ../lib/mcp-deferred-gate.ts keeps already-loaded tools passing through\n * untouched.\n *\n * Every correction is appended to the observation store via daemon IPC (same\n * fire-and-forget path as observe.ts), so false-outage reports are countable.\n * Never blocks Claude Code: always exits 0, tolerant of missing transcript.\n */\n\nimport { closeSync, fstatSync, openSync, readSync } from \"node:fs\";\nimport { connect } from \"node:net\";\nimport { decideMcpGate, PASS_OUTPUT, type GateHookInput } from \"../lib/mcp-deferred-gate.js\";\nimport { isProbeSession } from \"../lib/project-utils.js\";\n\n/** Enough transcript to see the whole recent tool history; transcripts can be huge. */\nconst TRANSCRIPT_TAIL_BYTES = 512 * 1024;\n\n// ---------------------------------------------------------------------------\n// Transcript tail \u2014 bounded read, never loads the whole file\n// ---------------------------------------------------------------------------\n\nfunction readTranscriptTail(path: string): string {\n  try {\n    const fd = openSync(path, \"r\");\n    try {\n      const size = fstatSync(fd).size;\n      const length = Math.min(size, TRANSCRIPT_TAIL_BYTES);\n      const buffer = Buffer.alloc(length);\n      readSync(fd, buffer, 0, length, size - length);\n      return buffer.toString(\"utf-8\");\n    } finally {\n      closeSync(fd);\n    }\n  } catch {\n    return \"\";\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Inline IPC sender \u2014 avoids importing src/daemon/ipc-client.ts at build time\n// ---------------------------------------------------------------------------\n\nfunction sendToDaemon(method: string, params: Record<string, unknown>): Promise<void> {\n  return new Promise((resolve) => {\n    const timeout = setTimeout(() => {\n      resolve();\n    }, 5000);\n    try {\n      const socket = connect(\"/tmp/pai.sock\", () => {\n        const req = JSON.stringify({ id: Date.now().toString(), method, params }) + \"\\n\";\n        socket.write(req);\n        socket.on(\"data\", () => {\n          clearTimeout(timeout);\n          socket.destroy();\n          resolve();\n        });\n        socket.on(\"error\", () => {\n          clearTimeout(timeout);\n          resolve();\n        });\n      });\n      socket.on(\"error\", () => {\n        clearTimeout(timeout);\n        resolve();\n      });\n    } catch {\n      clearTimeout(timeout);\n      resolve();\n    }\n  });\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\nasync function main(): Promise<void> {\n  let text = \"\";\n  try {\n    for await (const chunk of process.stdin) text += chunk;\n  } catch {\n    process.stdout.write(PASS_OUTPUT);\n    return;\n  }\n  if (!text.trim()) {\n    process.stdout.write(PASS_OUTPUT);\n    return;\n  }\n\n  let input: GateHookInput;\n  try {\n    input = JSON.parse(text) as GateHookInput;\n  } catch {\n    process.stdout.write(PASS_OUTPUT);\n    return;\n  }\n\n  const transcript =\n    typeof input.transcript_path === \"string\" ? readTranscriptTail(input.transcript_path) : \"\";\n  const decision = decideMcpGate(input, transcript);\n  process.stdout.write(decision.output);\n\n  if (decision.observation && !isProbeSession(input.cwd)) {\n    await sendToDaemon(\"observation_store\", {\n      session_id: input.session_id ?? \"\",\n      ...decision.observation,\n      content_hash: \"\",\n      cwd: input.cwd ?? \"\",\n    });\n  }\n}\n\nmain().catch(() => process.stdout.write(PASS_OUTPUT));\n", "/**\n * mcp-deferred-gate.ts \u2014 pure pieces of the deferred MCP tool gate.\n *\n * Claude Code defers MCP tool schemas: an mcp__server__tool call fails input\n * validation unless the session surfaced the schema via ToolSearch first.\n * Sessions misread that error as \"MCP server disconnected\" and report an\n * outage that never happened (observed with a rename tool while every server\n * was connected).\n *\n * A second failure shape: when an MCP server re-registers mid-session, the\n * session's deferred tool handles are invalidated and calls fail with the\n * harness text \"N deferred tools are no longer available\" (observed 2026-09-18,\n * N=411, a rename tool). Sessions misread that as a disconnect too. The gate\n * denies that shape with the same ToolSearch remedy; a ToolSearch that\n * surfaces the tool after the last invalidation re-arms the pass-through.\n *\n * The decision is evidence-based: the session transcript either shows the tool\n * was surfaced (a ToolSearch mentioning it) or already ran successfully \u2014 in\n * which case the schema is loaded and the call passes through untouched \u2014 or\n * it shows neither, in which case the call is about to hit the gate (or is a\n * retry that just did) and gets the corrective instruction.\n *\n * Split out of the pre-tool-use entrypoint (which calls main() at import\n * time) so the decision is testable without spawning a hook process, per the\n * transcript-text.ts precedent.\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface GateHookInput {\n  session_id?: string;\n  transcript_path?: string;\n  cwd?: string;\n  tool_name?: string;\n  tool_input?: unknown;\n}\n\nexport interface GateEvidence {\n  /** A ToolSearch call in the transcript mentions this tool by full name. */\n  surfacedByToolSearch: boolean;\n  /** This tool already ran successfully \u2014 its schema must be loaded. */\n  succeededBefore: boolean;\n}\n\nexport interface GateObservation {\n  type: \"decision\";\n  title: string;\n  narrative: string;\n  tool_name: string;\n  tool_input_summary: string;\n  files_read: string[];\n  files_modified: string[];\n  concepts: string[];\n}\n\nexport interface GateDecision {\n  action: \"pass\" | \"correct\";\n  /** The JSON string to write to stdout, same shape as sibling PreToolUse hooks. */\n  output: string;\n  /** Observation payload for the daemon when a correction fired, else null. */\n  observation: GateObservation | null;\n}\n\n// ---------------------------------------------------------------------------\n// Decision\n// ---------------------------------------------------------------------------\n\nexport const PASS_OUTPUT = JSON.stringify({\n  hookSpecificOutput: { hookEventName: \"PreToolUse\", permissionDecision: \"allow\" },\n});\n\n/**\n * Harness text emitted when a mid-session MCP server re-registration\n * invalidates the session's deferred tool handles.\n */\nexport const STALE_HANDLE_MARKER = \"deferred tools are no longer available\";\n\n/** True for MCP tool names (`mcp__<server>__<tool>`), false for everything else. */\nexport function isMcpTool(name: string): boolean {\n  return name.startsWith(\"mcp__\") && name.split(\"__\").length >= 3 && !!name.split(\"__\")[2];\n}\n\n/**\n * Scan transcript text (JSONL, one entry per line, tolerant of junk) for\n * evidence that `toolName` is loadable in this session.\n */\nexport function scanTranscriptEvidence(transcriptText: string, toolName: string): GateEvidence {\n  const toolUseIds = new Set<string>();\n  let surfacedByToolSearch = false;\n  let succeededBefore = false;\n\n  for (const line of transcriptText.split(\"\\n\")) {\n    const trimmed = line.trim();\n    if (!trimmed) continue;\n    let entry: unknown;\n    try {\n      entry = JSON.parse(trimmed);\n    } catch {\n      continue;\n    }\n    const message = (entry as { message?: { content?: unknown } })?.message;\n    const content = message?.content;\n    if (!Array.isArray(content)) continue;\n\n    for (const block of content as Array<Record<string, unknown>>) {\n      if (!block || typeof block !== \"object\") continue;\n      if (block.type === \"tool_use\" && typeof block.name === \"string\") {\n        if (block.name === toolName && typeof block.id === \"string\") {\n          toolUseIds.add(block.id);\n        } else if (block.name === \"ToolSearch\") {\n          // Liberal match: the query may be \"select:<tool>\" or a keyword list.\n          if (JSON.stringify(block.input ?? \"\").includes(toolName)) {\n            surfacedByToolSearch = true;\n          }\n        }\n      } else if (\n        block.type === \"tool_result\" &&\n        typeof block.tool_use_id === \"string\" &&\n        toolUseIds.has(block.tool_use_id) &&\n        !block.is_error\n      ) {\n        succeededBefore = true;\n      }\n    }\n  }\n\n  return { surfacedByToolSearch, succeededBefore };\n}\n\n/**\n * Byte index of the last transcript line where a ToolSearch call surfaced\n * `toolName` (same liberal match as scanTranscriptEvidence), or -1 if never.\n * Used to order the stale-handle remedy against the last invalidation.\n */\nexport function lastToolSearchSurfacingIndex(\n  transcriptText: string,\n  toolName: string,\n): number {\n  let last = -1;\n  let offset = 0;\n  for (const line of transcriptText.split(\"\\n\")) {\n    const start = offset;\n    offset += line.length + 1;\n    const trimmed = line.trim();\n    if (!trimmed) continue;\n    let entry: unknown;\n    try {\n      entry = JSON.parse(trimmed);\n    } catch {\n      continue;\n    }\n    const content = (entry as { message?: { content?: unknown } })?.message?.content;\n    if (!Array.isArray(content)) continue;\n    for (const block of content as Array<Record<string, unknown>>) {\n      if (\n        block?.type === \"tool_use\" &&\n        block.name === \"ToolSearch\" &&\n        JSON.stringify(block.input ?? \"\").includes(toolName)\n      ) {\n        last = start;\n      }\n    }\n  }\n  return last;\n}\n\n/** The ToolSearch remedy shared by both correction shapes. */\nfunction buildRemedy(toolName: string): string {\n  return (\n    `Remedy: call ToolSearch with query \"select:${toolName}\" to load the fresh registry ` +\n    `entry, then call ${toolName} again with the same arguments.`\n  );\n}\n\n/** The corrective instruction fed back to the session on a deny. */\nexport function buildCorrection(toolName: string): string {\n  return (\n    `${toolName} is a deferred MCP tool: its schema is not loaded in this session yet, so this ` +\n    `call would fail input validation. The MCP server is NOT disconnected \u2014 do not report an ` +\n    `outage and do not retry the call unchanged. ${buildRemedy(toolName)}`\n  );\n}\n\n/** Same remedy for the stale-handle shape: a re-registration invalidated a loaded schema. */\nexport function buildStaleHandleCorrection(toolName: string): string {\n  return (\n    `${toolName}'s deferred tool handle is stale: the MCP server re-registered mid-session, ` +\n    `invalidating the schema this session had loaded (\"${STALE_HANDLE_MARKER}\"). The MCP ` +\n    `server is NOT disconnected \u2014 do not report an outage and do not retry the call ` +\n    `unchanged. ${buildRemedy(toolName)}`\n  );\n}\n\n/** The deny decision shared by both failure shapes. */\nfunction correctDecision(toolName: string, reason: string, narrative: string): GateDecision {\n  return {\n    action: \"correct\",\n    output: JSON.stringify({\n      hookSpecificOutput: {\n        hookEventName: \"PreToolUse\",\n        permissionDecision: \"deny\",\n        permissionDecisionReason: reason,\n      },\n    }),\n    observation: {\n      type: \"decision\",\n      title: `MCP deferred-tool gate corrected: ${toolName}`,\n      narrative,\n      tool_name: toolName,\n      tool_input_summary: toolName,\n      files_read: [],\n      files_modified: [],\n      concepts: [\"mcp\", \"deferred\", \"toolsearch\"],\n    },\n  };\n}\n\n/**\n * The whole gate: given hook input and the session transcript, decide whether\n * this mcp__ call passes through or gets the deferred-schema correction.\n */\nexport function decideMcpGate(input: GateHookInput, transcriptText: string): GateDecision {\n  const toolName = typeof input.tool_name === \"string\" ? input.tool_name : \"\";\n  if (!isMcpTool(toolName)) {\n    return { action: \"pass\", output: PASS_OUTPUT, observation: null };\n  }\n\n  // Stale-handle shape: a mid-session re-registration invalidated loaded\n  // schemas. Overrides the normal evidence pass-through, because a tool that\n  // was surfaced (or even ran) earlier can still have a dead handle now. The\n  // gate re-arms to pass-through only once a ToolSearch re-surfaced the tool\n  // after the last invalidation.\n  const toolInputText = JSON.stringify(input.tool_input ?? \"\");\n  const lastStale = transcriptText.lastIndexOf(STALE_HANDLE_MARKER);\n  if (toolInputText.includes(STALE_HANDLE_MARKER) || lastStale !== -1) {\n    const remedyAt = lastToolSearchSurfacingIndex(transcriptText, toolName);\n    if (remedyAt === -1 || remedyAt <= lastStale) {\n      return correctDecision(\n        toolName,\n        buildStaleHandleCorrection(toolName),\n        `Blocked a retry against a stale deferred-tool handle for ${toolName} and issued ` +\n          \"the ToolSearch remedy (server not disconnected)\",\n      );\n    }\n    return { action: \"pass\", output: PASS_OUTPUT, observation: null };\n  }\n\n  const evidence = scanTranscriptEvidence(transcriptText, toolName);\n  if (evidence.surfacedByToolSearch || evidence.succeededBefore) {\n    return { action: \"pass\", output: PASS_OUTPUT, observation: null };\n  }\n\n  return correctDecision(\n    toolName,\n    buildCorrection(toolName),\n    `Blocked a call to deferred MCP tool ${toolName} and issued the ToolSearch remedy ` +\n      \"(server not disconnected)\",\n  );\n}\n", "/**\n * Path utilities \u2014 encoding, Notes/Sessions directory discovery and creation.\n */\n\nimport { existsSync, mkdirSync, readdirSync, linkSync, copyFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { ADAPTER_DIR, PAI_DIR } from '../pai-paths.js';\n\n// Re-export for consumers. PROJECTS_DIR is a harness-adjacent data dir, not\n// PAI_HOME state \u2014 out of scope for the PAI_DIR\u2192PAI_HOME fold (see pai-paths.ts).\nexport { ADAPTER_DIR, PAI_DIR };\nexport const PROJECTS_DIR = join(ADAPTER_DIR, 'projects');\n\n/**\n * Directories known to be automated health-check / probe sessions.\n * Hooks should exit early for these to avoid registry clutter and wasted work.\n */\nconst PROBE_CWD_PATTERNS = [\n  '/CodexBar/ClaudeProbe',\n  '/ClaudeProbe',\n];\n\n/**\n * Check if the current working directory belongs to a probe/health-check session.\n * Returns true if hooks should skip this session entirely.\n */\nexport function isProbeSession(cwd?: string): boolean {\n  const dir = cwd || process.cwd();\n  return PROBE_CWD_PATTERNS.some(pattern => dir.includes(pattern));\n}\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with -\n * - Replace space with -\n */\nexport function encodePath(path: string): string {\n  return path\n    .replace(/\\//g, '-')\n    .replace(/\\./g, '-')\n    .replace(/ /g, '-');\n}\n\n/** Get the project directory for a given working directory. */\nexport function getProjectDir(cwd: string): string {\n  const encoded = encodePath(cwd);\n  return join(PROJECTS_DIR, encoded);\n}\n\n/** Get the Notes directory for a project (central location). */\nexport function getNotesDir(cwd: string): string {\n  return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory \u2014 checks local first, falls back to central.\n * Does NOT create the directory.\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n  const cwdBasename = basename(cwd).toLowerCase();\n  if (cwdBasename === 'notes' && existsSync(cwd)) {\n    return { path: cwd, isLocal: true };\n  }\n\n  const localPaths = [\n    join(cwd, 'Notes'),\n    join(cwd, 'notes'),\n    join(cwd, '.claude', 'Notes'),\n  ];\n\n  for (const path of localPaths) {\n    if (existsSync(path)) {\n      return { path, isLocal: true };\n    }\n  }\n\n  return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/** Get the sessions/ directory for a project (stores .jsonl transcripts). */\nexport function getSessionsDir(cwd: string): string {\n  return join(getProjectDir(cwd), 'sessions');\n}\n\n/** Get the sessions/ directory from a project directory path. */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n  return join(projectDir, 'sessions');\n}\n\n// ---------------------------------------------------------------------------\n// Directory creation helpers\n// ---------------------------------------------------------------------------\n\n/** Ensure the Notes directory exists for a project. @deprecated Use ensureNotesDirSmart() */\nexport function ensureNotesDir(cwd: string): string {\n  const notesDir = getNotesDir(cwd);\n  if (!existsSync(notesDir)) {\n    mkdirSync(notesDir, { recursive: true });\n    console.error(`Created Notes directory: ${notesDir}`);\n  }\n  return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists \u2192 use it (don't create anything new)\n * - If no local Notes/ \u2192 ensure central exists and use that\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n  const found = findNotesDir(cwd);\n  if (found.isLocal) return found;\n  if (!existsSync(found.path)) {\n    mkdirSync(found.path, { recursive: true });\n    console.error(`Created central Notes directory: ${found.path}`);\n  }\n  return found;\n}\n\n/** Ensure the sessions/ directory exists for a project. */\nexport function ensureSessionsDir(cwd: string): string {\n  const sessionsDir = getSessionsDir(cwd);\n  if (!existsSync(sessionsDir)) {\n    mkdirSync(sessionsDir, { recursive: true });\n    console.error(`Created sessions directory: ${sessionsDir}`);\n  }\n  return sessionsDir;\n}\n\n/** Ensure the sessions/ directory exists (from project dir path). */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n  const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n  if (!existsSync(sessionsDir)) {\n    mkdirSync(sessionsDir, { recursive: true });\n    console.error(`Created sessions directory: ${sessionsDir}`);\n  }\n  return sessionsDir;\n}\n\n/**\n * Publish every project-root .jsonl into sessions/ as well, WITHOUT removing it.\n *\n * This used to renameSync, and that is how PAI destroyed its users' sessions.\n *\n * `claude --resume <uuid>` finds a transcript only at the project root. Move it\n * into sessions/ and the session becomes permanently unresumable \u2014 measured\n * 2026-08-04: `claude --resume b3462801` (867 KB of real work, sessions/ only)\n * answers \"No conversation found with session ID\", while a top-level id is found\n * fine. Nothing warned; the id still looked valid everywhere PAI displayed it.\n *\n * The damage was not occasional. This ran from a UserPromptSubmit hook excluding\n * only the CURRENT session, so every prompt anyone typed unresumed every other\n * session in the project. One PAI project measured 1 transcript at top level\n * against 52 underneath. Among the casualties was 046bb712 \u2014 the exact id PAI's\n * own handover tells the user to resume.\n *\n * A hardlink satisfies both sides, which is why this is a two-line fix rather\n * than a redesign: the archive genuinely has consumers that read sessions/\n * (session-summary-worker, registry/moved, session/autosave), and `--resume`\n * needs the root path. One inode, two names, no copy, no window where the file\n * is missing from either place.\n *\n * Never unlink the source. Tidying up another tool's store was the whole\n * mistake; a stale duplicate is free, a lost session is not. Every caller of\n * `transcriptFiles()` was checked before choosing this \u2014 they all test emptiness\n * (`.length > 0`), never count, so the duplicate cannot skew a project's stats.\n *\n * `excludeFile` keeps a hook from archiving the transcript it is itself watching\n * being written. It is not a \"finished sessions only\" guard and must not be read\n * as one: it excludes exactly one file, the caller's own, so with two sessions\n * live in one project each still archives the other mid-turn. A consumer that\n * needs \"finished only\" has to enforce that itself \u2014 this function cannot know\n * what is live.\n *\n * Returns the number of files newly archived.\n */\nexport function archiveSessionFilesToSessionsDir(\n  projectDir: string,\n  excludeFile?: string,\n  silent = false\n): number {\n  const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n  if (!existsSync(projectDir)) return 0;\n\n  const files = readdirSync(projectDir);\n  let archivedCount = 0;\n\n  for (const file of files) {\n    if (!file.endsWith('.jsonl') || file === excludeFile) continue;\n\n    const sourcePath = join(projectDir, file);\n    const destPath = join(sessionsDir, file);\n\n    // Already archived \u2014 including by an earlier rename, before this was a\n    // hardlink. Those are the sessions that need restoring to the root, which is\n    // a separate repair and not this function's job.\n    if (existsSync(destPath)) continue;\n\n    try {\n      linkSync(sourcePath, destPath);\n      if (!silent) console.error(`Archived ${file} \u2192 sessions/ (still resumable)`);\n      archivedCount++;\n    } catch (error) {\n      // Cross-device (EXDEV) is the realistic failure: sessions/ on another\n      // volume. Copy instead, and still leave the original alone.\n      try {\n        copyFileSync(sourcePath, destPath);\n        if (!silent) console.error(`Copied ${file} \u2192 sessions/ (hardlink unavailable)`);\n        archivedCount++;\n      } catch {\n        if (!silent) console.error(`Could not archive ${file}: ${error}`);\n      }\n    }\n  }\n\n  return archivedCount;\n}\n\n/**\n * @deprecated Renamed to `archiveSessionFilesToSessionsDir`, which is what it\n * now does. Kept so an out-of-tree caller fails loudly at the type level rather\n * than silently keeping the old destructive name for a non-destructive action.\n */\nexport const moveSessionFilesToSessionsDir = archiveSessionFilesToSessionsDir;\n\n// ---------------------------------------------------------------------------\n// CLAUDE.md / TODO.md discovery\n// ---------------------------------------------------------------------------\n\n/** Find TODO.md \u2014 check local first, fallback to central. */\nexport function findTodoPath(cwd: string): string {\n  const localPaths = [\n    join(cwd, 'TODO.md'),\n    join(cwd, 'notes', 'TODO.md'),\n    join(cwd, 'Notes', 'TODO.md'),\n    join(cwd, '.claude', 'TODO.md'),\n  ];\n\n  for (const path of localPaths) {\n    if (existsSync(path)) return path;\n  }\n\n  return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/** Find CLAUDE.md \u2014 returns the FIRST found path. */\nexport function findClaudeMdPath(cwd: string): string | null {\n  const paths = findAllClaudeMdPaths(cwd);\n  return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations in priority order.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n  const foundPaths: string[] = [];\n\n  const localPaths = [\n    join(cwd, '.claude', 'CLAUDE.md'),\n    join(cwd, 'CLAUDE.md'),\n    join(cwd, 'Notes', 'CLAUDE.md'),\n    join(cwd, 'notes', 'CLAUDE.md'),\n    join(cwd, 'Prompts', 'CLAUDE.md'),\n    join(cwd, 'prompts', 'CLAUDE.md'),\n  ];\n\n  for (const path of localPaths) {\n    if (existsSync(path)) foundPaths.push(path);\n  }\n\n  return foundPaths;\n}\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n *\n * Two different things live here, and they must not be confused:\n *\n * - ADAPTER_DIR (~/.claude by default) is the Claude Code harness adapter \u2014\n *   fixed, hardcoded paths the harness itself loads from (Hooks/, Skills/,\n *   Agents/, Commands/, settings.json, statusline-command.sh,\n *   tab-color-command.sh). It is harness-specific: a future harness would\n *   need its own adapter directory with its own conventions.\n * - PAI_HOME (~/.claude/pai by default \u2014 see ../../../config/pai-home.ts) is\n *   where PAI's own state lives: everything hooks WRITE (History/,\n *   agent-sessions.json, session-routing.json, ...) resolves there, with a\n *   fallback to the pre-2026-09-19 ADAPTER_DIR location and a one-time\n *   stderr notice, exactly like every other PAI_HOME file.\n *\n * ALSO loads .env file from ADAPTER_DIR so all hooks get environment\n * variables without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n *   import { ADAPTER_DIR, HOOKS_DIR, SKILLS_DIR, historyDir } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\nimport {\n  paiHomePath,\n  resolvePaiFile,\n  migratePaiFile,\n  migratePaiDir,\n  type MigrateFileResult,\n  type MigrateDirResult,\n} from '../../../config/pai-home.js';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE ADAPTER_DIR resolution so .env can set ADAPTER_DIR/PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n  // Check common locations for .env\n  const possiblePaths = [\n    resolve(process.env.ADAPTER_DIR || process.env.PAI_DIR || '', '.env'),\n    resolve(homedir(), '.claude', '.env'),\n  ];\n\n  for (const envPath of possiblePaths) {\n    if (existsSync(envPath)) {\n      try {\n        const content = readFileSync(envPath, 'utf-8');\n        for (const line of content.split('\\n')) {\n          const trimmed = line.trim();\n          // Skip comments and empty lines\n          if (!trimmed || trimmed.startsWith('#')) continue;\n\n          const eqIndex = trimmed.indexOf('=');\n          if (eqIndex > 0) {\n            const key = trimmed.substring(0, eqIndex).trim();\n            let value = trimmed.substring(eqIndex + 1).trim();\n\n            // Remove surrounding quotes if present\n            if ((value.startsWith('\"') && value.endsWith('\"')) ||\n                (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n              value = value.slice(1, -1);\n            }\n\n            // Expand $HOME and ~ in values\n            value = value.replace(/\\$HOME/g, homedir());\n            value = value.replace(/^~(?=\\/|$)/, homedir());\n\n            // Only set if not already defined (env vars take precedence)\n            if (process.env[key] === undefined) {\n              process.env[key] = value;\n            }\n          }\n        }\n        // Found and loaded, don't check other paths\n        break;\n      } catch {\n        // Silently continue if .env can't be read\n      }\n    }\n  }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\nfunction defaultAdapterDir(): string {\n  return resolve(homedir(), '.claude');\n}\n\n/**\n * Never print when stdout/stderr is a data channel, not a human: hook\n * bundles and worker-status-line.mjs set PAI_QUIET_NOTICES=1 via their\n * esbuild banner (scripts/build-hooks.mjs), and a `pai worker run\n * --output-format json` invocation is detected directly off argv since its\n * env can't be set before this module's static imports resolve.\n */\nfunction suppressDeprecationNotices(): boolean {\n  if (process.env.PAI_QUIET_NOTICES === '1') return true;\n  const idx = process.argv.indexOf('--output-format');\n  return idx !== -1 && process.argv[idx + 1] === 'json';\n}\n\n/**\n * At most once per process \u2014 a `globalThis` flag rather than a module-level\n * `let` because hooks and the CLI can end up with more than one instance of\n * this module loaded into the same process (separate bundles), each with its\n * own module scope; only a property on the shared global survives that.\n */\nfunction warnPaiDirDeprecatedOnce(message: string): void {\n  const g = globalThis as typeof globalThis & { __paiDirNoticePrinted?: boolean };\n  if (g.__paiDirNoticePrinted || suppressDeprecationNotices()) return;\n  g.__paiDirNoticePrinted = true;\n  process.stderr.write(`pai: ${message}\\n`);\n}\n\n/**\n * Smart ADAPTER_DIR detection with fallback\n * Priority:\n * 1. ADAPTER_DIR environment variable (if set) \u2014 warns once if PAI_DIR is\n *    ALSO set and resolves to a different path (naming both).\n * 2. PAI_DIR environment variable (deprecated alias, one release) \u2014 warns\n *    once only when it differs from the default adapter root; PAI_DIR set to\n *    the same value as the default is the operator's current, correct\n *    setting and stays silent.\n * 3. ~/.claude (standard location)\n */\nfunction resolveAdapterDir(): string {\n  if (process.env.ADAPTER_DIR) {\n    const adapterDir = resolve(process.env.ADAPTER_DIR);\n    if (process.env.PAI_DIR) {\n      const paiDir = resolve(process.env.PAI_DIR);\n      if (paiDir !== adapterDir) {\n        warnPaiDirDeprecatedOnce(\n          `ADAPTER_DIR (${adapterDir}) and PAI_DIR (${paiDir}) are both set and differ \u2014 ADAPTER_DIR wins. PAI_DIR still works for one release.`\n        );\n      }\n    }\n    return adapterDir;\n  }\n  if (process.env.PAI_DIR) {\n    const paiDir = resolve(process.env.PAI_DIR);\n    if (paiDir !== defaultAdapterDir()) {\n      warnPaiDirDeprecatedOnce(\n        'PAI_DIR is deprecated \u2014 use ADAPTER_DIR for the harness adapter root (~/.claude). PAI_DIR still works for one release.'\n      );\n    }\n    return paiDir;\n  }\n  return defaultAdapterDir();\n}\n\n/** The Claude Code harness adapter root \u2014 NOT PAI's state home. See module doc above. */\nexport const ADAPTER_DIR = resolveAdapterDir();\n\n/** @deprecated alias for ADAPTER_DIR, kept for one release. Prefer ADAPTER_DIR. */\nexport const PAI_DIR = ADAPTER_DIR;\n\n/**\n * Adapter directories \u2014 fixed paths the Claude Code harness itself loads\n * from. These stay under ADAPTER_DIR; they are not PAI state.\n */\nexport const HOOKS_DIR = join(ADAPTER_DIR, 'Hooks');\nexport const SKILLS_DIR = join(ADAPTER_DIR, 'Skills');\nexport const AGENTS_DIR = join(ADAPTER_DIR, 'Agents');\nexport const COMMANDS_DIR = join(ADAPTER_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n  if (!existsSync(ADAPTER_DIR)) {\n    console.error(`ADAPTER_DIR does not exist: ${ADAPTER_DIR}`);\n    console.error(`   Expected ~/.claude or set ADAPTER_DIR environment variable`);\n    process.exit(1);\n  }\n\n  if (!existsSync(HOOKS_DIR)) {\n    console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n    console.error(`   Your ADAPTER_DIR may be misconfigured`);\n    console.error(`   Current ADAPTER_DIR: ${ADAPTER_DIR}`);\n    process.exit(1);\n  }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n// ---------------------------------------------------------------------------\n// PAI state written by hooks \u2014 resolves under PAI_HOME, falling back to the\n// pre-2026-09-19 ADAPTER_DIR location (one-time stderr notice) until\n// `pai config migrate --history` moves it. Actively written on every hook\n// event across every session, so unlike most PAI_HOME files the live move is\n// deliberately NOT automatic \u2014 see `pai config migrate --history`.\n// ---------------------------------------------------------------------------\n\nfunction oldHistoryDir(): string {\n  return join(ADAPTER_DIR, 'History');\n}\n\n/** Read/write location for hook-captured history: PAI_HOME/History if\n *  present, else the old ADAPTER_DIR/History (one-time stderr notice). */\nexport function historyDir(): string {\n  return resolvePaiFile(paiHomePath('History'), [oldHistoryDir()], 'pai config migrate --history');\n}\n\nexport function migrateHistoryDir(opts: { dryRun?: boolean } = {}): MigrateDirResult {\n  return migratePaiDir(paiHomePath('History'), [oldHistoryDir()], opts);\n}\n\nfunction oldAgentSessionsPath(): string {\n  return join(ADAPTER_DIR, 'agent-sessions.json');\n}\n\nexport function agentSessionsPath(): string {\n  return resolvePaiFile(paiHomePath('agent-sessions.json'), [oldAgentSessionsPath()], 'pai config migrate --history');\n}\n\nexport function migrateAgentSessions(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n  return migratePaiFile(paiHomePath('agent-sessions.json'), [oldAgentSessionsPath()], opts);\n}\n\nfunction oldSecurityEventsPath(): string {\n  return join(ADAPTER_DIR, 'history', 'security', 'security-events.jsonl');\n}\n\nexport function securityEventsPath(): string {\n  return resolvePaiFile(\n    paiHomePath('History', 'security', 'security-events.jsonl'),\n    [oldSecurityEventsPath()],\n    'pai config migrate --history'\n  );\n}\n\nexport function migrateSecurityEvents(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n  return migratePaiFile(paiHomePath('History', 'security', 'security-events.jsonl'), [oldSecurityEventsPath()], opts);\n}\n\nfunction oldSessionRoutingPath(): string {\n  return join(ADAPTER_DIR, 'session-routing.json');\n}\n\nexport function sessionRoutingPath(): string {\n  return resolvePaiFile(paiHomePath('session-routing.json'), [oldSessionRoutingPath()], 'pai config migrate --history');\n}\n\nexport function migrateSessionRouting(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n  return migratePaiFile(paiHomePath('session-routing.json'), [oldSessionRoutingPath()], opts);\n}\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n  const now = new Date();\n  const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n  const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n  const year = localDate.getFullYear();\n  const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n  return join(historyDir(), subdir, `${year}-${month}`, filename);\n}\n", "/**\n * Shared \"## Continue\" checkpoint logic for a project's TODO.md.\n *\n * WHY THIS MODULE EXISTS\n * ----------------------\n * Before this, `pause.ts` and `handover.ts` each carried their own copy of\n * findProjectTodo / stripContinueSection / block-builder. Both wrote the same\n * fixed four-line block, and both stripped any existing ## Continue section\n * unconditionally. The consequence was that a rich, model-authored checkpoint\n * could never survive:\n *\n *   1. `pai pause` had no way to accept a body, so the model printed its\n *      checkpoint to the terminal and it was lost.\n *   2. Even if a body had been written by hand, the session-stop hook runs\n *      `pai session handover` on every clean exit, which regenerated the\n *      generic block and erased it.\n *\n * So there are two jobs here:\n *\n *   - AUTHORED writes (`pai pause --body-file`) carry the model's markdown\n *     verbatim, wrapped in explicit start/end markers.\n *   - AUTO writes (hooks) must never destroy an authored checkpoint belonging\n *     to the *same* session. They may replace a stale one left by an earlier\n *     session, otherwise TODO.md would show a checkpoint that no longer\n *     describes where the work stands.\n *\n * PARSING\n * -------\n * A rich body can legitimately contain `---` rules and `##` headings, which the\n * old heuristic scanner treated as section terminators. Authored blocks are\n * therefore delimited by an explicit HTML-comment pair; the legacy heuristic is\n * kept only as a fallback for blocks written before this change.\n */\n\nimport {\n  existsSync,\n  readFileSync,\n  writeFileSync,\n  readdirSync,\n  renameSync,\n  mkdirSync,\n  realpathSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Locations searched for a project TODO.md, in priority order. */\nexport const TODO_LOCATIONS = [\n  \"Notes/TODO.md\",\n  \".claude/Notes/TODO.md\",\n  \"tasks/todo.md\",\n  \"TODO.md\",\n];\n\nexport const MARKER_OPEN = \"<!-- pai:checkpoint\";\nexport const MARKER_CLOSE = \"<!-- /pai:checkpoint -->\";\n\n/**\n * Markers for a handover that has been superseded but not thrown away.\n *\n * Deliberately NOT `pai:checkpoint`: `locateContinue` scans for that marker and\n * for the `## Continue` heading, so an archived block written verbatim would be\n * found as the live section and rewritten in place of it. Archived entries are\n * inert by construction \u2014 different marker, no heading.\n */\nexport const ARCHIVE_OPEN = \"<!-- pai:archived-handover\";\nexport const ARCHIVE_CLOSE = \"<!-- /pai:archived-handover -->\";\nexport const ARCHIVE_HEADING = \"## Previous handovers\";\n\n/**\n * How many superseded handovers TODO.md keeps.\n *\n * Enough that a handover survives a run of sessions that each end without\n * pausing, which is the sequence that destroyed one on 2026-08-04. Not\n * unbounded: TODO.md is read by a human at the top of every session, and a file\n * that grows without limit stops being read, which is its own kind of loss.\n */\nexport const ARCHIVE_LIMIT = 5;\n\nexport const CONTINUE_HEADING = \"## Continue\";\n\n/** Who wrote the checkpoint currently in TODO.md. */\nexport type Authorship = \"model\" | \"auto\";\n\n/** Result of applying a checkpoint. */\nexport type ApplyAction = \"written\" | \"preserved\" | \"failed\";\n\n// ---------------------------------------------------------------------------\n// TODO.md discovery\n// ---------------------------------------------------------------------------\n\nexport function findProjectTodo(\n  rootPath: string\n): { path: string; content: string } | null {\n  for (const rel of TODO_LOCATIONS) {\n    const full = join(rootPath, rel);\n    if (existsSync(full)) {\n      try {\n        return { path: full, content: readFileSync(full, \"utf8\") };\n      } catch {\n        // unreadable \u2014 try next\n      }\n    }\n  }\n  return null;\n}\n\n/**\n * Resolve the TODO.md to write to, creating Notes/ if nothing exists yet.\n * Returns null only when the directory could not be created.\n */\nexport function resolveTodoTarget(\n  rootPath: string,\n  opts: { create?: boolean } = {}\n): { path: string; content: string } | null {\n  const found = findProjectTodo(rootPath);\n  if (found) return found;\n\n  const notesDir = join(rootPath, \"Notes\");\n  if (opts.create !== false) {\n    try {\n      if (!existsSync(notesDir)) mkdirSync(notesDir, { recursive: true });\n    } catch {\n      return null;\n    }\n  }\n  return { path: join(notesDir, \"TODO.md\"), content: \"\" };\n}\n\n// ---------------------------------------------------------------------------\n// Marker parsing\n// ---------------------------------------------------------------------------\n\nexport interface CheckpointMeta {\n  authored: Authorship;\n  /** Session line the checkpoint describes, e.g. \"0003 - 2026-08-01 - Title\". */\n  session?: string;\n  /** Claude Code session UUID, when known. */\n  sessionId?: string;\n  ts?: string;\n}\n\n/**\n * Parse a `<!-- pai:checkpoint key=\"value\" ... -->` marker line.\n * Returns null when the line is not a marker.\n */\nexport function parseMarker(line: string): CheckpointMeta | null {\n  const trimmed = line.trim();\n  if (!trimmed.startsWith(MARKER_OPEN)) return null;\n\n  const attrs: Record<string, string> = {};\n  for (const m of trimmed.matchAll(/([a-zA-Z][\\w-]*)=\"([^\"]*)\"/g)) {\n    attrs[m[1]] = m[2];\n  }\n\n  return {\n    authored: attrs.authored === \"model\" ? \"model\" : \"auto\",\n    session: attrs.session || undefined,\n    sessionId: attrs[\"session-id\"] || undefined,\n    ts: attrs.ts || undefined,\n  };\n}\n\nexport interface LocatedContinue {\n  /** Index of the \"## Continue\" heading line. */\n  startIdx: number;\n  /** Exclusive end index, past any trailing `---` separator. */\n  endIdx: number;\n  meta: CheckpointMeta | null;\n  /** Raw lines of the section, heading included. */\n  lines: string[];\n}\n\n/**\n * Lines an auto-generated block is made of. Anything else in a section is\n * content somebody put there deliberately.\n *\n * The blockquote marker is optional on every pattern. These lines appear\n * quoted when the block builder emits them as its header, and unquoted when a\n * caller passes the same text as a BODY \u2014 which the session-stop hook does: it\n * builds `Working directory: ${cwd}` and hands it over as state. Matching only\n * the quoted form made such a body read as content, which is how a session\n * that did no work at all overwrote a real handover on 2026-08-04. A line that\n * merely restates the generated header is boilerplate wherever it sits.\n */\nconst BOILERPLATE_PATTERNS = [\n  /^##\\s+Continue$/,\n  /^<!--\\s*\\/?pai:checkpoint/,\n  /^>?\\s*\\*\\*Last session:\\*\\*/,\n  /^>?\\s*\\*\\*Paused at:\\*\\*/,\n  /^>?\\s*Working directory:/,\n  /^>?\\s*Resume with:/,\n  /^>?\\s*_No checkpoint body was recorded/,\n  /^-{3,}$/,\n];\n\n/** A blank line, with or without a blockquote marker. */\nconst BLANK_LINE = /^>?\\s*$/;\n\n/**\n * True when a section contains nothing but generated header lines.\n *\n * This is the guard that stops an auto write from destroying content it did\n * not author. A session that hit the old clobbering bug may have worked around\n * it by hand \u2014 writing its state into a subsection underneath the generated\n * header lines, in an unmarked block. Those blocks predate the marker, so\n * authorship cannot be read off them; the only safe signal is whether anything\n * beyond boilerplate is present.\n */\nexport function isBoilerplateOnly(lines: string[]): boolean {\n  return lines.every((line) => {\n    const t = line.trim();\n    return BLANK_LINE.test(t) || BOILERPLATE_PATTERNS.some((re) => re.test(t));\n  });\n}\n\n/**\n * True when a checkpoint body says something the generated header does not.\n *\n * \"Has a body\" and \"has something to say\" are not the same question, and the\n * preservation rules care only about the second. An unattended writer that\n * emits a single line restating the working directory has produced a body by\n * any string test and a handover by none.\n */\nexport function hasSubstance(body: string | null | undefined): boolean {\n  const text = (body ?? \"\").trim();\n  if (!text) return false;\n  return !isBoilerplateOnly(text.split(\"\\n\"));\n}\n\n/**\n * Extract the non-boilerplate content of a section, or \"\" if there is none.\n *\n * Interior blank lines are kept. They are not decoration \u2014 in Markdown they\n * are what separates a paragraph from the table or list that follows, so\n * dropping them (as this did while it treated every blank line as boilerplate)\n * silently welds a checkpoint body into one unreadable run.\n */\nexport function extractSectionContent(lines: string[]): string {\n  const kept: string[] = [];\n  for (const line of lines) {\n    const t = line.trim();\n    if (BOILERPLATE_PATTERNS.some((re) => re.test(t))) continue;\n    kept.push(line);\n  }\n  return trimBlankEdges(collapseBlankRuns(kept)).join(\"\\n\");\n}\n\n/** Drop leading and trailing blank lines, leaving interior spacing alone. */\nfunction trimBlankEdges(lines: string[]): string[] {\n  let start = 0;\n  let end = lines.length;\n  while (start < end && BLANK_LINE.test(lines[start].trim())) start += 1;\n  while (end > start && BLANK_LINE.test(lines[end - 1].trim())) end -= 1;\n  return lines.slice(start, end);\n}\n\n/**\n * Collapse runs of blank lines to a single blank.\n *\n * Removing a boilerplate line leaves the blank that surrounded it behind, so\n * stripping the header can open a three-line gap in the middle of the body.\n * One blank line is all Markdown needs.\n */\nfunction collapseBlankRuns(lines: string[]): string[] {\n  const out: string[] = [];\n  let lastWasBlank = false;\n  for (const line of lines) {\n    const isBlank = BLANK_LINE.test(line.trim());\n    if (isBlank && lastWasBlank) continue;\n    out.push(line);\n    lastWasBlank = isBlank;\n  }\n  return out;\n}\n\n/**\n * Locate the existing ## Continue section.\n *\n * When the section carries an explicit marker pair, the close marker defines\n * the end \u2014 this is what lets a rich body contain `---` and `##` safely.\n * Otherwise the legacy heuristic applies: stop at the first `---` or the next\n * `##` heading.\n */\nexport function locateContinue(content: string): LocatedContinue | null {\n  const lines = content.split(\"\\n\");\n  const startIdx = lines.findIndex((l) => l.trim() === CONTINUE_HEADING);\n  if (startIdx === -1) return null;\n\n  // Look for an open marker in the first few lines of the section.\n  let meta: CheckpointMeta | null = null;\n  let markerIdx = -1;\n  for (let i = startIdx + 1; i < Math.min(startIdx + 6, lines.length); i++) {\n    const parsed = parseMarker(lines[i]);\n    if (parsed) {\n      meta = parsed;\n      markerIdx = i;\n      break;\n    }\n    // A non-blank, non-marker line means there is no marker for this section.\n    if (lines[i].trim() !== \"\") break;\n  }\n\n  let endIdx = lines.length;\n\n  if (markerIdx !== -1) {\n    const closeIdx = lines.findIndex(\n      (l, i) => i > markerIdx && l.trim() === MARKER_CLOSE\n    );\n    if (closeIdx !== -1) {\n      endIdx = closeIdx + 1;\n    } else {\n      // Malformed (open without close) \u2014 fall back to the heuristic so we do\n      // not swallow the rest of the file.\n      endIdx = heuristicEnd(lines, startIdx);\n    }\n  } else {\n    endIdx = heuristicEnd(lines, startIdx);\n  }\n\n  // Consume one trailing `---` separator and the blank lines around it.\n  let trailingEnd = endIdx;\n  while (trailingEnd < lines.length && lines[trailingEnd].trim() === \"\") {\n    trailingEnd += 1;\n  }\n  if (trailingEnd < lines.length && lines[trailingEnd].trim() === \"---\") {\n    trailingEnd += 1;\n  } else {\n    trailingEnd = endIdx;\n  }\n\n  return {\n    startIdx,\n    endIdx: trailingEnd,\n    meta,\n    lines: lines.slice(startIdx, trailingEnd),\n  };\n}\n\n/**\n * Legacy scanner for blocks written before checkpoint markers existed.\n *\n * Terminates on a horizontal rule or the next level-1 or level-2 heading. Note\n * the `(?!#)` \u2014 `###` is a *subsection* of `## Continue`, not a terminator. The\n * original scanner stopped at any run of `#`, which meant a `### Restored\n * state` subsection fell outside the section entirely and could not be seen,\n * let alone carried forward.\n *\n * `#{1,2}` rather than `##`, because matching only `##` did not stop at an H1\n * and therefore ATE IT. A TODO.md whose `## Continue` block precedes its\n * `# TODO` title had the title absorbed into the section and destroyed on the\n * next regenerate \u2014 reported independently by three sessions on 2026-08-03,\n * one of which lost it three times and recovered from git each time.\n *\n * An H1 is a document-level heading. It can never be part of a `## Continue`\n * section, so treating it as a terminator is not a heuristic improvement but a\n * structural fact.\n */\nfunction heuristicEnd(lines: string[], startIdx: number): number {\n  for (let i = startIdx + 1; i < lines.length; i++) {\n    const trimmed = lines[i].trim();\n    if (\n      trimmed === \"---\" ||\n      (/^#{1,2}(?!#)/.test(trimmed) && trimmed !== CONTINUE_HEADING)\n    ) {\n      return i;\n    }\n  }\n  return lines.length;\n}\n\n/** Remove the ## Continue section, returning the remainder of the document. */\nexport function stripContinue(content: string): string {\n  const found = locateContinue(content);\n  if (!found) return content;\n\n  const lines = content.split(\"\\n\");\n  const before = lines.slice(0, found.startIdx);\n  const after = lines.slice(found.endIdx);\n  while (after.length > 0 && after[0].trim() === \"\") after.shift();\n\n  return [...before, ...after].join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Reading a checkpoint back\n// ---------------------------------------------------------------------------\n\nexport interface ContinueCheckpoint {\n  meta: CheckpointMeta | null;\n  /** The authored body \u2014 the section with generated header lines removed. */\n  body: string;\n  /** The full section, heading included. */\n  raw: string;\n}\n\n/**\n * Read the `## Continue` checkpoint out of a TODO.md.\n *\n * Writing a checkpoint is only half of a handover; something has to deliver it\n * to the next session. This is the read side, used by the SessionStart hook.\n *\n * Returns null when there is no section, or when the section holds nothing but\n * generated header lines \u2014 a bodyless block carries no information a new\n * session does not already have, and injecting it would only add noise.\n */\nexport function readContinueCheckpoint(\n  content: string\n): ContinueCheckpoint | null {\n  const found = locateContinue(content);\n  if (!found) return null;\n\n  const body = found.meta\n    ? extractMarkedBody(found.lines)\n    : extractSectionContent(found.lines);\n  if (!body) return null;\n\n  return { meta: found.meta, body, raw: found.lines.join(\"\\n\") };\n}\n\n/**\n * Extract the body of a marker-delimited block by position rather than by\n * pattern.\n *\n * The layout is fixed: open marker, a contiguous run of `>` header lines, the\n * body, then the close marker. Because the boundaries are known exactly, the\n * body comes back byte-for-byte \u2014 including any `---` rules or `##` headings\n * of its own, which a pattern-based filter would mistake for boilerplate and\n * delete. Falls back to the pattern filter if the shape is not as expected.\n */\nfunction extractMarkedBody(lines: string[]): string {\n  const markerIdx = lines.findIndex((l) => parseMarker(l) !== null);\n  if (markerIdx === -1) return extractSectionContent(lines);\n\n  const closeIdx = lines.findIndex(\n    (l, i) => i > markerIdx && l.trim() === MARKER_CLOSE\n  );\n  if (closeIdx === -1) return extractSectionContent(lines);\n\n  // Skip the blank lines and the `>` header run that follow the open marker.\n  let bodyStart = markerIdx + 1;\n  while (bodyStart < closeIdx) {\n    const t = lines[bodyStart].trim();\n    if (BLANK_LINE.test(t) || t.startsWith(\">\")) {\n      bodyStart += 1;\n      continue;\n    }\n    break;\n  }\n\n  return trimBlankEdges(lines.slice(bodyStart, closeIdx)).join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Block construction\n// ---------------------------------------------------------------------------\n\nexport interface BuildOptions {\n  authored: Authorship;\n  /** Human-readable session line, or \"Unknown session\". */\n  sessionLine: string;\n  /** Claude Code session UUID \u2014 the `claude --resume` handle. */\n  sessionId?: string;\n  cwd: string;\n  /** Model-authored markdown. Omitted for auto blocks. */\n  body?: string;\n  /** Overridable for deterministic tests. */\n  timestamp?: string;\n}\n\nfunction escapeAttr(value: string): string {\n  return value.replace(/\"/g, \"'\");\n}\n\nexport function buildContinueBlock(opts: BuildOptions): string {\n  const ts = opts.timestamp ?? new Date().toISOString();\n\n  const attrs = [\n    `authored=\"${opts.authored}\"`,\n    `session=\"${escapeAttr(opts.sessionLine)}\"`,\n    opts.sessionId ? `session-id=\"${escapeAttr(opts.sessionId)}\"` : null,\n    `ts=\"${ts}\"`,\n  ]\n    .filter(Boolean)\n    .join(\" \");\n\n  const header = [\n    `> **Last session:** ${opts.sessionLine}`,\n    `> **Paused at:** ${ts}`,\n    \">\",\n    `> Working directory: ${opts.cwd}`,\n  ];\n\n  if (opts.sessionId) {\n    header.push(\">\", `> Resume with: \\`claude --resume ${opts.sessionId}\\``);\n  }\n\n  const body = (opts.body ?? \"\").trim();\n\n  const parts = [\n    CONTINUE_HEADING,\n    \"\",\n    `${MARKER_OPEN} ${attrs} -->`,\n    \"\",\n    ...header,\n  ];\n\n  if (body) {\n    parts.push(\"\", body);\n  } else {\n    parts.push(\n      \">\",\n      \"> _No checkpoint body was recorded \u2014 see the latest session note._\"\n    );\n  }\n\n  parts.push(\"\", MARKER_CLOSE, \"\", \"---\", \"\");\n\n  return parts.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Apply\n// ---------------------------------------------------------------------------\n\nexport interface ApplyOptions extends BuildOptions {\n  /** Project root; TODO.md is resolved beneath it. */\n  rootPath: string;\n  /** Preview only \u2014 nothing is written. */\n  dryRun?: boolean;\n}\n\nexport interface ApplyResult {\n  action: ApplyAction;\n  path: string | null;\n  block: string;\n  /** Set when action === \"preserved\". */\n  preservedMeta?: CheckpointMeta;\n  /** Set when unattributed content was carried forward into the new block. */\n  carriedForward?: boolean;\n  /** Set when a superseded model-authored handover was moved to the archive. */\n  archived?: boolean;\n  error?: string;\n}\n\n/**\n * Write the ## Continue block, honouring the preservation rules.\n *\n * An AUTO write is unattended \u2014 it fires from the session-stop and pre-compact\n * hooks \u2014 so it operates under one governing rule: **never destroy content it\n * did not author.** Three cases follow from that:\n *\n *   1. An authored checkpoint for the SAME session is left untouched. The hooks\n *      fire after the model has already recorded the real state; overwriting it\n *      with metadata is the bug this module exists to fix.\n *\n *      \"Same session\" is decided by the Claude session UUID whenever both sides\n *      know it, and only falls back to the human-readable session line when one\n *      of them does not. The line is derived from the session note filename,\n *      and `session-stop.sh` *renames and renumbers that file* \u2014 via `session\n *      slug --apply` and `session cleanup --execute` \u2014 before it reaches the\n *      handover step. So the key the hook computes at exit is not the key the\n *      model wrote seconds earlier, and a filename-keyed comparison mismatches\n *      by construction. Observed live on 2026-08-01: notes renumbered twice\n *      within a single session. The UUID is the only identifier that holds\n *      still.\n *   2. An authored checkpoint from an EARLIER session is stale \u2014 TODO.md would\n *      otherwise keep pointing at the wrong session \u2014 so it is replaced. Its\n *      content is not lost: `pai pause` mirrors every authored body into the\n *      session note.\n *   3. An UNMARKED block predates the marker, so authorship cannot be read off\n *      it. If it is nothing but generated header lines it is replaced. If it\n *      carries anything else, that content was put there deliberately \u2014 quite\n *      possibly as a hand-rolled workaround for the very clobbering this fixes \u2014\n *      and is carried forward into the new block rather than dropped.\n *\n * A MODEL write always replaces: the model is authoring the checkpoint, and a\n * newer one supersedes an older one.\n */\n/**\n * Do an existing checkpoint and an incoming write describe the same session?\n *\n * The UUID is authoritative when both sides carry one: it is assigned by Claude\n * Code and never changes for the life of the session. The session line is a\n * derived, mutable label and is only consulted when there is no UUID to compare\n * \u2014 a checkpoint written before `--session-id` was threaded through, or an auto\n * write from a caller that was not given one.\n */\nfunction isSameSession(\n  meta: CheckpointMeta,\n  opts: Pick<ApplyOptions, \"sessionId\" | \"sessionLine\">\n): boolean {\n  if (meta.sessionId && opts.sessionId) {\n    return meta.sessionId === opts.sessionId;\n  }\n  return meta.session === opts.sessionLine;\n}\n\n/**\n * How long a model-authored checkpoint is protected from automated overwriting.\n *\n * Sized for the race, not for the session: the gap between a model writing its\n * checkpoint and a hook firing is seconds to minutes, and an hour covers even a\n * slow checkpoint on a loaded machine. Anything genuinely from an earlier\n * session is hours or days old and still falls through to case 2, which is what\n * keeps TODO.md from freezing on a checkpoint nobody will ever replace.\n */\nconst AUTHORED_GRACE_MS = 60 * 60 * 1000;\n\n/** Was this checkpoint written recently enough to still belong to the run in progress? */\nfunction isRecent(meta: CheckpointMeta, now?: string): boolean {\n  if (!meta.ts) return false; // no stamp, no claim \u2014 case 2 decides as before\n  const then = Date.parse(meta.ts);\n  if (Number.isNaN(then)) return false;\n  const nowMs = now ? Date.parse(now) : Date.now();\n  if (Number.isNaN(nowMs)) return false;\n  // Guard the future too: a clock skew that puts the stamp ahead of now must\n  // not read as \"very old\" and license an overwrite.\n  return nowMs - then < AUTHORED_GRACE_MS;\n}\n\n/**\n * Turn a live checkpoint block into an inert archive entry.\n *\n * Two things have to go: the `## Continue` heading and the `pai:checkpoint`\n * marker. Both are what `locateContinue` looks for, so leaving either in place\n * would let a later write treat the archive as the live section \u2014 and the next\n * one after that would then archive the archive. Everything else is kept\n * verbatim, because the body is the whole reason this exists.\n */\nfunction toArchiveEntry(lines: string[], meta: CheckpointMeta | null): string[] {\n  const out: string[] = [];\n  const label = meta?.session ?? \"Unknown session\";\n  const stamp = meta?.ts ? ` \u2014 checkpointed ${meta.ts}` : \"\";\n  out.push(`${ARCHIVE_OPEN} session=\"${label}\"${meta?.ts ? ` ts=\"${meta.ts}\"` : \"\"} -->`);\n  out.push(\"\");\n  out.push(`### ${label}${stamp}`);\n  out.push(\"\");\n  for (const line of lines) {\n    if (line.trim() === CONTINUE_HEADING) continue;\n    if (line.trim().startsWith(MARKER_OPEN)) continue;\n    if (line.trim() === MARKER_CLOSE) continue;\n    if (line.trim() === \"---\") continue;\n    out.push(line);\n  }\n  while (out.length > 0 && out[out.length - 1].trim() === \"\") out.pop();\n  out.push(\"\");\n  out.push(ARCHIVE_CLOSE);\n  return out;\n}\n\n/**\n * Put a superseded handover into the archive section, newest first.\n *\n * `rest` is the document with the Continue section already stripped. The\n * archive lives immediately below it so a reader meets the current handover\n * first and the previous ones directly after, rather than hunting for them at\n * the bottom of a file that also holds the project's open work.\n */\nfunction archiveInto(rest: string, entry: string[]): string {\n  const lines = rest.split(\"\\n\");\n  const headingIdx = lines.findIndex((l) => l.trim() === ARCHIVE_HEADING);\n\n  if (headingIdx === -1) {\n    return [ARCHIVE_HEADING, \"\", ...entry, \"\", \"---\", \"\", rest.trimStart()].join(\"\\n\");\n  }\n\n  // Insert directly under the heading, then drop whatever falls past the cap.\n  const before = lines.slice(0, headingIdx + 1);\n  const after = lines.slice(headingIdx + 1);\n  while (after.length > 0 && after[0].trim() === \"\") after.shift();\n\n  const merged = [...entry, \"\", ...after];\n  const kept: string[] = [];\n  let seen = 0;\n  for (let i = 0; i < merged.length; i++) {\n    if (merged[i].trim().startsWith(ARCHIVE_OPEN)) {\n      seen += 1;\n      if (seen > ARCHIVE_LIMIT) {\n        // Everything from here to the end of THIS entry goes; keep scanning so\n        // anything after the archive section (other headings, open work) stays.\n        while (i < merged.length && merged[i].trim() !== ARCHIVE_CLOSE) i++;\n        continue; // skips the close marker too\n      }\n    }\n    kept.push(merged[i]);\n  }\n  return [...before, \"\", ...kept].join(\"\\n\");\n}\n\nexport function applyContinue(opts: ApplyOptions): ApplyResult {\n  const target = resolveTodoTarget(opts.rootPath, { create: !opts.dryRun });\n  if (!target) {\n    return {\n      action: \"failed\",\n      path: null,\n      block: buildContinueBlock(opts),\n      error: \"Could not resolve or create a TODO.md target\",\n    };\n  }\n\n  const existing = locateContinue(target.content);\n  let carriedForward = false;\n  let effectiveBody = opts.body;\n  let archiveEntry: string[] | null = null;\n\n  if (opts.authored === \"auto\" && existing) {\n    // Case 1 \u2014 authored, same session: hands off.\n    if (existing.meta?.authored === \"model\" && isSameSession(existing.meta, opts)) {\n      return {\n        action: \"preserved\",\n        path: target.path,\n        block: buildContinueBlock(opts),\n        preservedMeta: existing.meta,\n      };\n    }\n\n    // Case 1b \u2014 the incoming write has nothing to say, and what is already\n    // there does. A metadata-only block says \"see the latest session note\", so\n    // replacing a real handover with one trades content for a pointer \u2014 and the\n    // pointer is not always good: observed live on 2026-08-01 in the AIBroker\n    // project, where the stop hook's bodyless handover overwrote the autosave's\n    // body and named a session note that had never been created, leaving the\n    // next session nothing to resume from.\n    //\n    // Deferring to the session note is only safe when the note demonstrably has\n    // the content. This code cannot see that, so it does the one thing that is\n    // never wrong: a write with nothing to say does not get to destroy\n    // something that does. A stale-but-real handover beats a fresh dead link.\n    // Scoped to blocks we marked. An unmarked block falls through to case 3,\n    // which salvages its content into the new block rather than freezing the\n    // old one \u2014 better, because an unmarked block has no session metadata worth\n    // preserving and may predate this scheme entirely.\n    //\n    // The test is SUBSTANCE, not emptiness. It was emptiness until 2026-08-04,\n    // when a session that was opened and immediately exited destroyed the\n    // previous day's handover for every project it touched. Its stop hook found\n    // no work items and no completion message, so it sent the one line it\n    // always builds unconditionally \u2014 `Working directory: \u2026`, a verbatim copy\n    // of a line the header already generates. Non-empty by a string test, so\n    // this guard stood down; worthless by any other reading. `hasSubstance`\n    // asks the question the comment above always claimed to be asking.\n    if (\n      existing.meta &&\n      !hasSubstance(opts.body) &&\n      !isBoilerplateOnly(existing.lines)\n    ) {\n      return {\n        action: \"preserved\",\n        path: target.path,\n        block: buildContinueBlock(opts),\n        preservedMeta: existing.meta ?? undefined,\n      };\n    }\n\n    // Case 2b \u2014 a model checkpoint written moments ago is THIS session's,\n    // whatever the identity comparison says.\n    //\n    // Case 2 deliberately lets an auto write replace an authored checkpoint\n    // from an EARLIER session, and that policy is right: TODO.md would freeze\n    // otherwise, and `pai pause` mirrors authored bodies into the session note.\n    // The bug is not the policy, it is that identity DEGRADES and healthy\n    // sessions fall into it.\n    //\n    // `sessionId` is optional on `pai pause`. Without it isSameSession compares\n    // a display line containing the note TITLE \u2014 and pausing renames the note.\n    // So a session writes a checkpoint, its note is renamed, the hook fires\n    // seconds later, no longer recognises its own work, and classifies it as a\n    // stale earlier session. Three sessions reported exactly this on\n    // 2026-08-03 from one `pai pause all`; one lost its checkpoint three times\n    // and recovered from git each time. Sessions without a clean repo would\n    // never have known it happened.\n    //\n    // Recency is the tiebreaker that identity cannot supply. A model checkpoint\n    // minutes old is not a stale predecessor by any reading, so an automated\n    // digest does not get to overwrite it. Older ones still fall through to\n    // case 2 and are replaced as before, which is what stops TODO.md freezing\n    // and keeps this from nesting carried-forward blocks without end.\n    if (\n      existing.meta?.authored === \"model\" &&\n      isRecent(existing.meta, opts.timestamp) &&\n      !isBoilerplateOnly(existing.lines)\n    ) {\n      return {\n        action: \"preserved\",\n        path: target.path,\n        block: buildContinueBlock(opts),\n        preservedMeta: existing.meta,\n      };\n    }\n\n    // Case 2c \u2014 an authored handover from an EARLIER session, past the grace\n    // window. Case 2 deleted it, on the reasoning that TODO.md must not freeze\n    // and that `pai pause` mirrors authored bodies into the session note.\n    //\n    // The first half is right. The second is an assumption, and on 2026-08-04\n    // it was false in the plainest way available: this project keeps no session\n    // notes at all (`Notes/*` is gitignored and none exist on disk), so the\n    // checkpoint WAS the only copy. It went at 08:36:29Z, to the autosave of a\n    // session that had been open for sixteen minutes and had typed one line \u2014\n    // destroying, as its first act, the handover it had been started to read.\n    //\n    // This is also the escape the v0.27.2 guard could not close: that one asks\n    // whether the block is RECENT, and a handover written the night before is\n    // not. Age was never the question. Nothing here needs the old block gone;\n    // it only needs the slot. So the block moves down the file instead, and the\n    // freeze argument and the loss argument stop being in tension.\n    //\n    // Only for `model` blocks with something in them. An auto block is a\n    // transcript scrape that regenerates on the next tick, and archiving those\n    // would bury the real handovers under mechanical noise.\n    if (\n      existing.meta?.authored === \"model\" &&\n      !isBoilerplateOnly(existing.lines) &&\n      !isSameSession(existing.meta, opts)\n    ) {\n      archiveEntry = toArchiveEntry(existing.lines, existing.meta);\n    }\n\n    // Case 3 \u2014 unmarked block holding content nobody can attribute to us.\n    if (!existing.meta && !isBoilerplateOnly(existing.lines)) {\n      const salvaged = extractSectionContent(existing.lines);\n      if (salvaged) {\n        effectiveBody = [\n          \"_Carried forward from the previous checkpoint (author unknown \u2014 this\",\n          \"block predates checkpoint authorship markers):_\",\n          \"\",\n          salvaged,\n        ].join(\"\\n\");\n        carriedForward = true;\n      }\n    }\n  }\n\n  const block = buildContinueBlock({ ...opts, body: effectiveBody });\n\n  if (opts.dryRun) {\n    return { action: \"written\", path: target.path, block, carriedForward, archived: !!archiveEntry };\n  }\n\n  const rest = stripContinue(target.content).trimStart();\n  const newContent = block + (archiveEntry ? archiveInto(rest, archiveEntry) : rest);\n  const tmpPath = `${target.path}.continue.tmp`;\n\n  try {\n    writeFileSync(tmpPath, newContent, \"utf8\");\n    renameSync(tmpPath, target.path);\n  } catch (err) {\n    try {\n      if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);\n    } catch {\n      /* ignore */\n    }\n    return {\n      action: \"failed\",\n      path: target.path,\n      block,\n      error: String(err),\n    };\n  }\n\n  return { action: \"written\", path: target.path, block, carriedForward, archived: !!archiveEntry };\n}\n\n// ---------------------------------------------------------------------------\n// Session-note discovery\n//\n// Lifted verbatim from end.ts so pause, end and any future caller share one\n// implementation. end.ts now imports these rather than carrying its own copy.\n// ---------------------------------------------------------------------------\n\n/** ADAPTER_DIR (harness adapter root) \u2014 mirrors pai-paths.ts's resolveAdapterDir(),\n *  kept self-contained here to avoid importing pai-paths.ts's process.exit(1)\n *  validation side effect. PAI_DIR is the deprecated alias, same as there. */\nfunction getPaiDir(): string {\n  const envDir = process.env.ADAPTER_DIR || process.env.PAI_DIR;\n  if (envDir) {\n    try {\n      return realpathSync(envDir);\n    } catch {\n      return envDir;\n    }\n  }\n  return join(homedir(), \".claude\");\n}\n\n/**\n * Find the notes directory for a project \u2014 local first, then the central\n * ~/.claude/projects/<encoded>/Notes fallback. Never creates.\n */\nexport function findNotesDir(\n  rootPath: string,\n  encodedDir: string\n): string | null {\n  for (const rel of [\"Notes\", \"notes\", \".claude/Notes\"]) {\n    const p = join(rootPath, rel);\n    if (existsSync(p)) return p;\n  }\n  const central = join(getPaiDir(), \"projects\", encodedDir, \"Notes\");\n  if (existsSync(central)) return central;\n  return null;\n}\n\n/**\n * Find the current (highest-numbered) session note: current month, then the\n * previous month, then a flat notesDir as legacy fallback.\n */\nexport function findLatestNote(notesDir: string): string | null {\n  const findIn = (dir: string): string | null => {\n    if (!existsSync(dir)) return null;\n    let files: string[];\n    try {\n      files = readdirSync(dir);\n    } catch {\n      return null;\n    }\n    const notes = files\n      .filter((f) => /^\\d{3,4}[\\s_-].*\\.md$/.test(f))\n      .sort((a, b) => {\n        const na = parseInt(a.match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n        const nb = parseInt(b.match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n        return na - nb;\n      });\n    return notes.length > 0 ? join(dir, notes[notes.length - 1]) : null;\n  };\n\n  const now = new Date();\n  const year = String(now.getFullYear());\n  const month = String(now.getMonth() + 1).padStart(2, \"0\");\n\n  const current = findIn(join(notesDir, year, month));\n  if (current) return current;\n\n  const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n  const py = String(prev.getFullYear());\n  const pm = String(prev.getMonth() + 1).padStart(2, \"0\");\n  const prevFound = findIn(join(notesDir, py, pm));\n  if (prevFound) return prevFound;\n\n  return findIn(notesDir);\n}\n\n// ---------------------------------------------------------------------------\n// Session-note append\n// ---------------------------------------------------------------------------\n\n/**\n * Append the checkpoint body to a session note.\n *\n * TODO.md's ## Continue is a single slot that every later checkpoint\n * overwrites. The session note is the durable record, so the body goes to both.\n * Idempotent per timestamp: re-running with the same stamp will not duplicate.\n */\nexport function appendCheckpointToNote(\n  notePath: string,\n  body: string,\n  timestamp?: string\n): { appended: boolean; error?: string } {\n  const ts = timestamp ?? new Date().toISOString();\n  const heading = `## Pause Checkpoint \u2014 ${ts}`;\n\n  let existing: string;\n  try {\n    existing = readFileSync(notePath, \"utf8\");\n  } catch (err) {\n    return { appended: false, error: String(err) };\n  }\n\n  if (existing.includes(heading)) return { appended: false };\n\n  const block = `\\n\\n---\\n\\n${heading}\\n\\n${body.trim()}\\n`;\n  const tmpPath = `${notePath}.checkpoint.tmp`;\n\n  try {\n    writeFileSync(tmpPath, existing.trimEnd() + block, \"utf8\");\n    renameSync(tmpPath, notePath);\n  } catch (err) {\n    try {\n      if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);\n    } catch {\n      /* ignore */\n    }\n    return { appended: false, error: String(err) };\n  }\n\n  return { appended: true };\n}\n\n// ---------------------------------------------------------------------------\n// Body loading\n// ---------------------------------------------------------------------------\n\n/** Read a checkpoint body from a file, or from stdin when path is \"-\". */\nexport function readBodyFile(path: string): string {\n  if (path === \"-\") {\n    return readFileSync(0, \"utf8\");\n  }\n  return readFileSync(path, \"utf8\");\n}\n"],
  "mappings": "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",
  "names": ["join", "join", "resolve"]
}
