{
  "version": 3,
  "sources": ["../../../src/hooks/ts/lib/worker-session.ts", "../../../src/hooks/ts/session-start/load-core-context.ts", "../../../src/hooks/ts/lib/pai-paths.ts", "../../../src/hooks/ts/lib/project-utils/paths.ts", "../../../src/session/checkpoint-block.ts", "../../../src/hooks/ts/lib/frontmatter.ts"],
  "sourcesContent": ["/**\n * Worker-session detection.\n *\n * A disposable headless worker (a `claude -p` run started by an orchestrating\n * session, possibly against a different model provider with a different\n * context window) shares the project directory and the hook configuration\n * with the real session that spawned it. Left alone, the hooks treat it as a\n * session in its own right: they inject project context into it, create and\n * rename a numbered session note for it, autosave it, and enqueue a\n * model-written handover for it. Its compactions also land in the project's\n * transcript folder, where they are indistinguishable from a real session's\n * and drag the measured compaction trigger down (observed 2026-09-17: two\n * workers compacting at ~151k tokens pulled a project's trigger from ~784k\n * to ~151k, and the real session's handover fired at ~50k tokens).\n *\n * The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does\n * per-session bookkeeping returns immediately when this predicate is true.\n * Deliberately NOT guarded: the security validator (a worker's shell\n * commands must still be checked) and observability capture.\n */\nexport function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {\n  return env.PAI_WORKER === \"1\";\n}\n", "#!/usr/bin/env node\n\n/**\n * load-core-context.ts\n *\n * Automatically loads your CORE skill context at session start by reading and injecting\n * the CORE SKILL.md file contents directly into Claude's context as a system-reminder.\n *\n * Purpose:\n * - Read CORE SKILL.md file content\n * - Output content as system-reminder for Claude to process\n * - Ensure complete context (contacts, preferences, security, identity) available at session start\n * - Bypass skill activation logic by directly injecting context\n *\n * Setup:\n * 1. Customize your ${ADAPTER_DIR}/skills/CORE/SKILL.md with your personal context\n * 2. Add this hook to settings.json SessionStart hooks\n * 3. Ensure ADAPTER_DIR environment variable is set (defaults to $HOME/.claude)\n *\n * How it works:\n * - Runs at the start of every Claude Code session\n * - Skips execution for subagent sessions (they don't need CORE context)\n * - Reads your CORE SKILL.md file\n * - Injects content as <system-reminder> which Claude processes automatically\n * - Gives your AI immediate access to your complete personal context\n */\n\nimport { isWorkerSession } from \"../lib/worker-session.js\";\nimport { readFileSync, existsSync } from 'fs';\nimport { join } from 'path';\nimport { SKILLS_DIR } from '../lib/pai-paths';\nimport { isProbeSession } from '../lib/project-utils';\nimport { stripFrontmatter } from '../lib/frontmatter.js';\n\nasync function main() {\n  if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping\n  try {\n    // Check if this is a subagent session - if so, exit silently\n    const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || '';\n    const isSubagent = claudeProjectDir.includes('/.claude/agents/') ||\n                      process.env.CLAUDE_AGENT_TYPE !== undefined;\n\n    if (isSubagent) {\n      // Subagent sessions don't need CORE context loading\n      console.error('Subagent session - skipping CORE context loading');\n      process.exit(0);\n    }\n\n    // Skip probe/health-check sessions (e.g. CodexBar ClaudeProbe)\n    if (isProbeSession()) {\n      console.error('Probe session detected - skipping CORE context loading');\n      process.exit(0);\n    }\n\n    // Get CORE skill path using PAI paths library\n    const coreSkillPath = join(SKILLS_DIR, 'CORE/SKILL.md');\n\n    // Verify CORE skill file exists\n    if (!existsSync(coreSkillPath)) {\n      console.error(`CORE skill not found at: ${coreSkillPath}`);\n      console.error(`Ensure CORE/SKILL.md exists or check ADAPTER_DIR environment variable`);\n      process.exit(1);\n    }\n\n    console.error('Reading CORE context from skill file...');\n\n    // Read the CORE SKILL.md file content\n    let coreContent = readFileSync(coreSkillPath, 'utf-8');\n\n    // Perform Dynamic Variable Substitution\n    // This allows SKILL.md to be generic while the session is personalized\n    const daName = process.env.DA || 'PAI';\n    const daColor = process.env.DA_COLOR || 'blue';\n    const engineerName = process.env.ENGINEER_NAME || '';\n\n    // Replace placeholders {{DA}}, {{DA_COLOR}}, {{ENGINEER_NAME}}\n    coreContent = coreContent\n      .replace(/\\{\\{DA\\}\\}/g, daName)\n      .replace(/\\{\\{DA_COLOR\\}\\}/g, daColor)\n      .replace(/\\{\\{ENGINEER_NAME\\}\\}/g, engineerName);\n\n    console.error(`Read ${coreContent.length} characters from CORE SKILL.md (Personalized for ${engineerName} & ${daName})`);\n\n    // Output the CORE content as a system-reminder, minus the frontmatter\n    // (catalogue metadata the session already has from its skill list) \u2014\n    // this will be injected into Claude's context at session start\n    const message = `<system-reminder>\nPAI CORE CONTEXT\n\n${stripFrontmatter(coreContent)}\n</system-reminder>`;\n\n    // Write to stdout (will be captured by Claude Code)\n    console.log(message);\n\n    console.error('CORE context injected into session');\n    process.exit(0);\n  } catch (error) {\n    console.error('Error in load-core-context hook:', error);\n    process.exit(1);\n  }\n}\n\nmain();\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 * 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 * 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", "/**\n * Strip a leading YAML frontmatter block (`---` ... `---` at the very top of\n * the file) and trim surrounding blank lines. A `---` that appears later in\n * the body (not at the top) is left untouched.\n */\nexport function stripFrontmatter(md: string): string {\n  const match = md.match(/^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n?/);\n  const body = match ? md.slice(match[0].length) : md;\n  return body.replace(/^\\s+|\\s+$/g, '');\n}\n"],
  "mappings": "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",
  "names": ["readFileSync", "existsSync", "join", "join", "join", "join", "existsSync", "readFileSync"]
}
