{
  "version": 3,
  "sources": ["../../../src/hooks/ts/lib/worker-session.ts", "../../../src/hooks/ts/session-start/load-project-context.ts", "../../../src/memory/wakeup.ts", "../../../src/config/pai-home.ts", "../../../src/session/checkpoint-block.ts", "../../../src/hooks/ts/lib/handover-budget.ts", "../../../src/hooks/ts/lib/pai-paths.ts", "../../../src/hooks/ts/lib/project-utils/paths.ts", "../../../src/hooks/ts/lib/project-utils/notify.ts", "../../../src/hooks/ts/lib/project-utils/session-notes.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-project-context.ts\n *\n * SessionStart hook that sets up project context:\n * - Checks for CLAUDE.md in various locations (Claude Code handles loading)\n * - Sets up Notes/ directory in ~/.claude/projects/{encoded-path}/\n * - Ensures TODO.md exists\n * - Sends ntfy.sh notification (mandatory)\n * - Displays session continuity info (like session-init.sh)\n *\n * This hook complements Claude Code's native CLAUDE.md loading by:\n * - Setting up the Notes infrastructure\n * - Showing the latest session note for continuity\n * - Sending ntfy.sh notifications\n */\n\nimport { isWorkerSession } from \"../lib/worker-session.js\";\nimport { existsSync, readdirSync, readFileSync, statSync } from 'fs';\nimport { join, basename, dirname, resolve } from 'path';\nimport { homedir } from 'os';\nimport { execSync } from 'child_process';\nimport { buildWakeupContext } from '../../../memory/wakeup.js';\nimport { readContinueCheckpoint } from '../../../session/checkpoint-block.js';\nimport { applyHandoverBudget } from '../lib/handover-budget.js';\nimport { sessionRoutingPath } from '../lib/pai-paths.js';\nimport {\n  findNotesDir,\n  getProjectDir,\n  getCurrentNotePath,\n  createSessionNote,\n  findTodoPath,\n  findAllClaudeMdPaths,\n  sendNtfyNotification,\n  isProbeSession,\n  archiveSessionFilesToSessionsDir\n} from '../lib/project-utils';\n\n/**\n * Find the pai CLI binary path dynamically.\n * Tries `which pai` first, then common fallback locations.\n */\nfunction findPaiBinary(): string {\n  try {\n    return execSync('which pai', { encoding: 'utf-8' }).trim();\n  } catch {\n    // Fallback locations in order of preference\n    const fallbacks = [\n      '/usr/local/bin/pai',\n      '/opt/homebrew/bin/pai',\n      `${process.env.HOME}/.local/bin/pai`,\n    ];\n    for (const p of fallbacks) {\n      if (existsSync(p)) return p;\n    }\n  }\n  return 'pai'; // Last resort: rely on PATH at runtime\n}\n\n/**\n * Check session-routing.json for an active route.\n * Returns the routed Notes path if set, or null to use default behavior.\n */\nfunction getRoutedNotesPath(): string | null {\n  const routingFile = sessionRoutingPath();\n  if (!existsSync(routingFile)) return null;\n\n  try {\n    const routing = JSON.parse(readFileSync(routingFile, 'utf-8'));\n    const active = routing?.active_session;\n    if (active?.notes_path) {\n      return active.notes_path;\n    }\n  } catch {\n    // Ignore parse errors\n  }\n  return null;\n}\n\n/**\n * Project signals that indicate a directory is a real project root.\n */\nconst PROJECT_SIGNALS = [\n  '.git',\n  'package.json',\n  'pubspec.yaml',\n  'Cargo.toml',\n  'go.mod',\n  'pyproject.toml',\n  'setup.py',\n  'build.gradle',\n  'pom.xml',\n  'composer.json',\n  'Gemfile',\n  'Makefile',\n  'CMakeLists.txt',\n  'tsconfig.json',\n  'CLAUDE.md',\n  join('Notes', 'PAI.md'),\n];\n\n/**\n * Returns true if the given directory looks like a project root.\n * Checks for the presence of well-known project signal files/dirs.\n */\nfunction hasProjectSignals(dir: string): boolean {\n  for (const signal of PROJECT_SIGNALS) {\n    if (existsSync(join(dir, signal))) return true;\n  }\n  return false;\n}\n\n/**\n * Returns true if the directory should NOT be auto-registered.\n * Guards: home directory, shallow paths, temp directories.\n */\nfunction isGuardedPath(dir: string): boolean {\n  const home = homedir();\n  const resolved = resolve(dir);\n\n  // Never register the home directory itself\n  if (resolved === home) return true;\n\n  // Depth guard: require at least 3 path segments beyond root\n  // e.g. /Users/owner/foo is depth 3 on macOS \u2014 reject it\n  const parts = resolved.split('/').filter(Boolean);\n  if (parts.length < 3) return true;\n\n  // Temp/system directories\n  const forbidden = ['/tmp', '/var', '/private/tmp', '/private/var/folders'];\n  for (const prefix of forbidden) {\n    if (resolved === prefix || resolved.startsWith(prefix + '/')) return true;\n  }\n\n  return false;\n}\n\ninterface HookInput {\n  session_id: string;\n  cwd: string;\n  hook_event_name: string;\n}\n\nasync function main() {\n  if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping\n  console.error('\\nload-project-context.ts starting...');\n\n  // Skip probe/health-check sessions (e.g. CodexBar ClaudeProbe)\n  if (isProbeSession()) {\n    console.error('Probe session detected - skipping project context loading');\n    process.exit(0);\n  }\n\n  // Read hook input from stdin\n  let hookInput: HookInput | null = null;\n  try {\n    const chunks: Buffer[] = [];\n    for await (const chunk of process.stdin) {\n      chunks.push(chunk);\n    }\n    const input = Buffer.concat(chunks).toString('utf-8');\n    if (input.trim()) {\n      hookInput = JSON.parse(input);\n    }\n  } catch (error) {\n    console.error('Could not parse hook input, using process.cwd()');\n  }\n\n  // Get current working directory\n  const cwd = hookInput?.cwd || process.cwd();\n\n  // Determine meaningful project name\n  // If cwd is a Notes directory, use parent directory name instead\n  let projectName = basename(cwd);\n  if (projectName.toLowerCase() === 'notes') {\n    projectName = basename(dirname(cwd));\n  }\n\n  console.error(`Working directory: ${cwd}`);\n  console.error(`Project: ${projectName}`);\n\n  // Check if this is a subagent session - skip for subagents\n  const isSubagent = process.env.CLAUDE_AGENT_TYPE !== undefined ||\n                     (process.env.CLAUDE_PROJECT_DIR || '').includes('/.claude/agents/');\n\n  if (isSubagent) {\n    console.error('Subagent session - skipping project context setup');\n    process.exit(0);\n  }\n\n  // 1. Find and READ all CLAUDE.md files - inject them into context\n  // This ensures Claude actually processes the instructions, not just sees them in headers\n  const claudeMdPaths = findAllClaudeMdPaths(cwd);\n  const claudeMdContents: { path: string; content: string }[] = [];\n\n  if (claudeMdPaths.length > 0) {\n    console.error(`Found ${claudeMdPaths.length} CLAUDE.md file(s):`);\n    for (const path of claudeMdPaths) {\n      console.error(`   - ${path}`);\n      try {\n        const content = readFileSync(path, 'utf-8');\n        claudeMdContents.push({ path, content });\n        console.error(`     Read ${content.length} chars`);\n      } catch (error) {\n        console.error(`     Could not read: ${error}`);\n      }\n    }\n  } else {\n    console.error('No CLAUDE.md found in project');\n    console.error('   Consider creating one at ./CLAUDE.md or ./.claude/CLAUDE.md');\n  }\n\n  // 2. Find or create Notes directory\n  // Priority:\n  //   1. Active session routing (pai route <project>) \u2192 routed Obsidian path\n  //   2. Local Notes/ in cwd \u2192 use it (git-trackable, e.g. symlink to Obsidian)\n  //   3. Central ~/.claude/projects/.../Notes/ \u2192 fallback\n  const routedPath = getRoutedNotesPath();\n  let notesDir: string;\n\n  if (routedPath) {\n    // Routing is active - use the configured Obsidian Notes path\n    const { mkdirSync } = await import('fs');\n    if (!existsSync(routedPath)) {\n      mkdirSync(routedPath, { recursive: true });\n      console.error(`Created routed Notes: ${routedPath}`);\n    } else {\n      console.error(`Notes directory: ${routedPath} (routed via pai route)`);\n    }\n    notesDir = routedPath;\n  } else {\n    const notesInfo = findNotesDir(cwd);\n\n    if (notesInfo.isLocal) {\n      notesDir = notesInfo.path;\n      console.error(`Notes directory: ${notesDir} (local)`);\n    } else {\n      // Create central Notes directory\n      if (!existsSync(notesInfo.path)) {\n        const { mkdirSync } = await import('fs');\n        mkdirSync(notesInfo.path, { recursive: true });\n        console.error(`Created central Notes: ${notesInfo.path}`);\n      } else {\n        console.error(`Notes directory: ${notesInfo.path} (central)`);\n      }\n      notesDir = notesInfo.path;\n    }\n  }\n\n  // 3. Archive the project's transcripts into sessions/ \u2014 by LINKING, never moving.\n  //\n  // This used to renameSync every .jsonl except the newest out of the project\n  // root. `claude --resume <uuid>` reads ~/.claude/projects/<encoded>/<uuid>.jsonl\n  // and only that path, so moving the file is what makes a session unresumable \u2014\n  // and because this is a SessionStart hook, the act of STARTING a session in a\n  // project destroyed the resumability of every earlier session in it. The old\n  // comment (\"keep the newest one for potential resume\") shows the dependency was\n  // known; keeping one file was not enough.\n  //\n  // Measured 2026-08-04: this repo had 1 transcript at the top level and 52 under\n  // sessions/, every one of the 52 unresumable. Among them was the id PAI prints\n  // in its own archived handovers as `claude --resume <uuid>`, so the instruction\n  // we ship in every checkpoint could not work.\n  //\n  // It also completes the incident of that morning: `pai Paperfull` failed to\n  // resume b3462801 because of a probe bug, started a fresh session instead, and\n  // THIS hook then moved b3462801 \u2014 867 KB of real work \u2014 out of reach. The\n  // failed resume destroyed what it had failed to open.\n  //\n  // A hard link keeps both truths: the archive under sessions/ is populated for\n  // everything that reads it, and the file Claude Code owns stays where Claude\n  // Code put it. Same inode, so it costs nothing.\n  // This was the fourth mover, with its own inline loop. It now calls the one\n  // shared archiver instead: a second implementation is exactly how the earlier\n  // probeResume fix came to land in one of three copies and leave `pai <Name>`\n  // broken for a day.\n  const projectDir = getProjectDir(cwd);\n  if (existsSync(projectDir)) {\n    try {\n      // Exclude this session's own transcript: it is being written right now,\n      // and the archive is meant to hold finished sessions.\n      const own = hookInput?.session_id ? `${hookInput.session_id}.jsonl` : undefined;\n      archiveSessionFilesToSessionsDir(projectDir, own, true);\n    } catch (error) {\n      console.error(`Could not archive session transcripts: ${error}`);\n    }\n  }\n\n  // 4. Find or create TODO.md\n  const todoPath = findTodoPath(cwd);\n  const hasTodo = existsSync(todoPath);\n  if (hasTodo) {\n    console.error(`TODO.md: ${todoPath}`);\n  } else {\n    // Create TODO.md in the Notes directory\n    const newTodoPath = join(notesDir, 'TODO.md');\n    const { writeFileSync } = await import('fs');\n    writeFileSync(newTodoPath, `# TODO\\n\\n## Offen\\n\\n- [ ] \\n\\n---\\n\\n*Created: ${new Date().toISOString()}*\\n`);\n    console.error(`Created TODO.md: ${newTodoPath}`);\n  }\n\n  // 5. Check for existing note or create new one\n  let activeNotePath: string | null = null;\n\n  if (notesDir) {  // notesDir is always set now (local or central)\n    const currentNotePath = getCurrentNotePath(notesDir);\n\n    // Only create a new note if there is truly no note at all.\n    // A completed note is still used \u2014 it will be updated or continued.\n    // This prevents duplicate notes at month boundaries and on every compaction.\n    if (!currentNotePath) {\n      // Defensive: ensure projectName is a usable string\n      const safeProjectName = (typeof projectName === 'string' && projectName.trim().length > 0)\n        ? projectName.trim()\n        : 'Untitled Session';\n      console.error('\\nNo previous session notes found - creating new one');\n      activeNotePath = createSessionNote(notesDir, String(safeProjectName));\n      console.error(`Created: ${basename(activeNotePath)}`);\n    } else {\n      activeNotePath = currentNotePath!;\n      console.error(`\\nUsing existing session note: ${basename(activeNotePath)}`);\n      // Show preview of current note\n      try {\n        const content = readFileSync(activeNotePath, 'utf-8');\n        const lines = content.split('\\n').slice(0, 12);\n        console.error('--- Current Note Preview ---');\n        for (const line of lines) {\n          console.error(line);\n        }\n        console.error('--- End Preview ---\\n');\n      } catch {\n        // Ignore read errors\n      }\n    }\n  }\n\n  // 6. Show TODO.md preview\n  if (existsSync(todoPath)) {\n    try {\n      const todoContent = readFileSync(todoPath, 'utf-8');\n      const todoLines = todoContent.split('\\n').filter(l => l.includes('[ ]')).slice(0, 5);\n      if (todoLines.length > 0) {\n        console.error('\\nOpen TODOs:');\n        for (const line of todoLines) {\n          console.error(`   ${line.trim()}`);\n        }\n      }\n    } catch {\n      // Ignore read errors\n    }\n  }\n\n  // 7. Send ntfy.sh notification (MANDATORY)\n  await sendNtfyNotification(`Session started in ${projectName}`);\n\n  // 7.5. Run pai project detect to identify the registered PAI project\n  const paiBin = findPaiBinary();\n  let paiProjectBlock = '';\n  try {\n    const { execFileSync } = await import('child_process');\n    const raw = execFileSync(paiBin, ['project', 'detect', '--json', cwd], {\n      encoding: 'utf-8',\n      env: process.env,\n    }).trim();\n\n    if (raw) {\n      const detected = JSON.parse(raw) as {\n        slug?: string;\n        display_name?: string;\n        root_path?: string;\n        match_type?: string;\n        relative_path?: string | null;\n        session_count?: number;\n        status?: string;\n        error?: string;\n        cwd?: string;\n      };\n\n      /**\n       * Attempt to auto-register the CWD as a new PAI project.\n       * Calls `pai project add <cwd>`, then re-detects to confirm registration.\n       * Returns true if registration succeeded (or was attempted and project add ran),\n       * and sets paiProjectBlock as a side effect on success.\n       */\n      const tryAutoRegister = async (): Promise<boolean> => {\n        if (isGuardedPath(cwd) || !hasProjectSignals(cwd)) return false;\n\n        try {\n          execFileSync(paiBin, ['project', 'add', cwd], {\n            encoding: 'utf-8',\n            env: process.env,\n          });\n          console.error(`PAI auto-registered project at: ${cwd}`);\n\n          // Re-run detect to confirm registration\n          try {\n            const raw2 = execFileSync(paiBin, ['project', 'detect', '--json', cwd], {\n              encoding: 'utf-8',\n              env: process.env,\n            }).trim();\n\n            if (raw2) {\n              const detected2 = JSON.parse(raw2) as typeof detected;\n              if (detected2.slug) {\n                const name2 = detected2.display_name || detected2.slug;\n                console.error(`PAI auto-registered: \"${detected2.slug}\" (${detected2.match_type})`);\n                paiProjectBlock = `PAI Project Registry: ${name2} (slug: ${detected2.slug}) [AUTO-REGISTERED]\nMatch: ${detected2.match_type ?? 'exact'} | Sessions: 0`;\n                return true;\n              }\n            }\n          } catch (detectErr) {\n            console.error('PAI auto-registration: project added but re-detect failed:', detectErr);\n            return true; // project IS registered, just can't load context\n          }\n        } catch (addErr) {\n          console.error('PAI auto-registration failed (project add):', addErr);\n        }\n        return false;\n      };\n\n      if (detected.error === 'no_match') {\n        // Attempt auto-registration if the directory looks like a real project\n        const autoRegistered = await tryAutoRegister();\n\n        if (!autoRegistered) {\n          paiProjectBlock = `PAI Project Registry: No registered project matches this directory.\nRun \"pai project add .\" to register this project, or use /route to tag the session.`;\n          console.error('PAI detect: no match for', cwd);\n        }\n      } else if (\n        detected.match_type === 'parent' &&\n        detected.relative_path &&\n        !isGuardedPath(cwd) &&\n        hasProjectSignals(cwd)\n      ) {\n        // The CWD is inside a broader registered parent (e.g. \"owner\" or \"apps\"),\n        // but it has its own project signals \u2014 register it as a distinct project.\n        console.error(\n          `PAI detect: parent match to \"${detected.slug}\" via relative path \"${detected.relative_path}\" \u2014 CWD looks like its own project, attempting auto-registration`\n        );\n        const autoRegistered = await tryAutoRegister();\n\n        if (!autoRegistered) {\n          // Fall through: show the parent match as normal\n          const name = detected.display_name || detected.slug;\n          const nameSlug = ` (slug: ${detected.slug})`;\n          const matchDesc = `parent (+${detected.relative_path ?? ''})`;\n          const statusFlag = detected.status && detected.status !== 'active'\n            ? ` [${detected.status.toUpperCase()}]`\n            : '';\n          paiProjectBlock = `PAI Project Registry: ${name}${statusFlag}${nameSlug}\nMatch: ${matchDesc} | Sessions: ${detected.session_count ?? 0}${detected.status && detected.status !== 'active' ? `\\nWARNING: Project status is \"${detected.status}\". Run: pai project health --fix` : ''}`;\n          console.error(`PAI detect: kept parent match \"${detected.slug}\" (auto-register not applicable)`);\n        }\n      } else if (detected.slug) {\n        const name = detected.display_name || detected.slug;\n        const nameSlug = ` (slug: ${detected.slug})`;\n        const matchDesc = detected.match_type === 'exact'\n          ? 'exact'\n          : `parent (+${detected.relative_path ?? ''})`;\n        const statusFlag = detected.status && detected.status !== 'active'\n          ? ` [${detected.status.toUpperCase()}]`\n          : '';\n        paiProjectBlock = `PAI Project Registry: ${name}${statusFlag}${nameSlug}\nMatch: ${matchDesc} | Sessions: ${detected.session_count ?? 0}${detected.status && detected.status !== 'active' ? `\\nWARNING: Project status is \"${detected.status}\". Run: pai project health --fix` : ''}`;\n        console.error(`PAI detect: matched \"${detected.slug}\" (${detected.match_type})`);\n      }\n    }\n  } catch (e) {\n    // Non-fatal \u2014 don't break session start if pai is unavailable\n    console.error('pai project detect failed:', e);\n  }\n\n  // 8. Output system reminder with session info\n  const reminder = `\n<system-reminder>\nPROJECT CONTEXT LOADED\n\nProject: ${projectName}\nWorking Directory: ${cwd}\n${notesDir ? `Notes Directory: ${notesDir}${routedPath ? ' (routed via pai route)' : ''}` : 'Notes: disabled (no local Notes/ directory)'}\n${hasTodo ? `TODO: ${todoPath}` : 'TODO: not found'}\n${claudeMdPaths.length > 0 ? `CLAUDE.md: ${claudeMdPaths.join(', ')}` : 'No CLAUDE.md found'}\n${activeNotePath ? `Active Note: ${basename(activeNotePath)}` : ''}\n${routedPath ? `\\nNote Routing: ACTIVE (pai route is set - notes go to Obsidian vault)` : ''}\n${paiProjectBlock ? `\\n${paiProjectBlock}` : ''}\n</system-reminder>\n`;\n\n  // Output to stdout for Claude to receive\n  console.log(reminder);\n\n  // 8.5. INJECT THE PAUSE CHECKPOINT\n  //\n  // `pai pause` writes the handover to TODO.md under `## Continue`. Until now\n  // nothing read it back: CORE SKILL.md only told the model to look there *if\n  // the user typed \"go\"*, so a resumed session started blind unless the user\n  // knew the magic word. Writing a handover nobody delivers is not a\n  // handover. Inject it, and say plainly that it is the previous session's\n  // state rather than an instruction to act on.\n  let handoverInjected = false;\n  if (existsSync(todoPath)) {\n    try {\n      const checkpoint = readContinueCheckpoint(readFileSync(todoPath, 'utf-8'));\n      if (checkpoint) {\n        handoverInjected = true;\n        const from = checkpoint.meta?.session\n          ? `\\nFrom session: ${checkpoint.meta.session}`\n          : '';\n        const when = checkpoint.meta?.ts ? `\\nPaused at: ${checkpoint.meta.ts}` : '';\n        const resume = checkpoint.meta?.sessionId\n          ? `\\nResume that session with: claude --resume ${checkpoint.meta.sessionId}`\n          : '';\n        const budgeted = applyHandoverBudget(checkpoint.body, todoPath);\n\n        console.log(`\n<system-reminder>\nHANDOVER FROM THE PREVIOUS SESSION\n\nSource: ${todoPath} (## Continue)${from}${when}${resume}\n\n${budgeted.body}\n\n---\nThis is recorded state, not a new instruction. Do not start acting on it\nunprompted. If the user says \"go\", \"continue\", or \"weiter\", resume from here.\n</system-reminder>\n`);\n        const budgetNote = budgeted.truncated\n          ? ', truncated'\n          : budgeted.elided.length > 0\n            ? `, elided: ${budgeted.elided.join(',')}`\n            : '';\n        console.error(`Injected pause checkpoint (${checkpoint.body.length} chars${budgetNote})`);\n      } else {\n        console.error('No pause checkpoint body in TODO.md \u2014 nothing to hand over');\n      }\n    } catch (error) {\n      // Non-fatal \u2014 a malformed TODO.md must never block session start.\n      console.error('Checkpoint injection failed:', error);\n    }\n  }\n\n  // 9. INJECT CLAUDE.md contents as system-reminders\n  // This ensures Claude actually reads and processes the instructions\n  for (const { path, content } of claudeMdContents) {\n    const claudeMdReminder = `\n<system-reminder>\nLOCAL CLAUDE.md LOADED (MANDATORY - READ AND FOLLOW)\n\nSource: ${path}\n\n${content}\n\n---\nTHE ABOVE INSTRUCTIONS ARE MANDATORY. Follow them exactly.\n</system-reminder>\n`;\n    console.log(claudeMdReminder);\n    console.error(`Injected CLAUDE.md content from: ${path}`);\n  }\n\n  // 10. Inject wake-up context (L0 identity + L1 essential story) if available\n  // The detected PAI project root_path is used for L1 note lookup.\n  // We derive it from the `paiProjectBlock` detection result by re-running a\n  // lightweight registry lookup, or by falling back to cwd for local-notes projects.\n  try {\n    // Attempt to find the project root path from the registry via `pai project detect --json`\n    let wakeupRootPath: string | undefined;\n    try {\n      const { execFileSync: efs } = await import('child_process');\n      const raw2 = efs(paiBin, ['project', 'detect', '--json', cwd], {\n        encoding: 'utf-8',\n        env: process.env,\n      }).trim();\n      if (raw2) {\n        const det = JSON.parse(raw2) as { root_path?: string; slug?: string };\n        if (det.root_path) wakeupRootPath = det.root_path;\n      }\n    } catch {\n      // Non-fatal \u2014 fall back to cwd\n      wakeupRootPath = cwd;\n    }\n\n    // Handover supersedes the L1 story: both summarise the same recent note,\n    // and the handover is a curated message while the story is auto-extracted.\n    const wakeupBlock = buildWakeupContext(wakeupRootPath, undefined, { skipStory: handoverInjected });\n    if (wakeupBlock) {\n      const wakeupReminder = `\\n<system-reminder>\\nWAKEUP CONTEXT\\n\\n${wakeupBlock}\\n</system-reminder>\\n`;\n      console.log(wakeupReminder);\n      console.error('Injected wake-up context (L0+L1)');\n    } else {\n      console.error('No wake-up context to inject (no identity file or session notes)');\n    }\n  } catch (wakeupError) {\n    // Non-fatal \u2014 don't block session start\n    console.error('Wake-up context injection failed:', wakeupError);\n  }\n\n  console.error('\\nProject context setup complete\\n');\n  process.exit(0);\n}\n\nmain().catch(error => {\n  console.error('load-project-context.ts error:', error);\n  process.exit(0); // Don't block session start\n});\n", "/**\n * Wake-up context system \u2014 progressive context loading inspired by mempalace.\n *\n * Layers:\n *   L0 Identity     (~100 tokens)   \u2014 user identity from ~/.pai/identity.txt. Always loaded.\n *   L1 Essential Story (~500-800t)  \u2014 top session notes for the project, key lines extracted.\n *   L2 On-Demand                    \u2014 triggered by topic queries (handled by memory_search).\n *   L3 Deep Search                  \u2014 unlimited federated memory search (memory_search tool).\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { paiHomePath, migratePaiFile, type MigrateFileResult } from \"../config/pai-home.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Maximum tokens for the L1 essential story block. Approx 4 chars/token.\n * Measured 2026-09-20: 800 tokens was mostly stale bullets. Reduced to 300.\n */\nconst L1_TOKEN_BUDGET = 300;\nconst L1_CHAR_BUDGET = L1_TOKEN_BUDGET * 4; // ~1600 chars\n\n/** Maximum session notes to scan when building L1. Reduced from 10 to 3 (2026-09-20). */\nconst L1_MAX_NOTES = 3;\n\n/** Sections to extract from session notes (in priority order). */\nconst EXTRACT_SECTIONS = [\n  \"Work Done\",\n  \"Key Decisions\",\n  \"Next Steps\",\n  \"Checkpoint\",\n];\n\n/** Identity file location. */\nconst IDENTITY_FILE = join(homedir(), \".pai\", \"identity.txt\");\n\n// ---------------------------------------------------------------------------\n// L0: Identity\n// ---------------------------------------------------------------------------\n\n/**\n * Load L0 identity. Prefers the PAI_HOME location (identity.txt migrated by\n * `pai config migrate`), falling back to the legacy ~/.pai/identity.txt so a\n * not-yet-migrated file keeps working. Returns \"\" if neither exists. Never\n * throws.\n */\nexport function loadL0Identity(\n  opts: { newPath?: string; legacyPath?: string } = {}\n): string {\n  const newPath = opts.newPath ?? paiHomePath(\"identity.txt\");\n  const legacyPath = opts.legacyPath ?? IDENTITY_FILE;\n  const path = existsSync(newPath) ? newPath : legacyPath;\n  if (!existsSync(path)) return \"\";\n  try {\n    return readFileSync(path, \"utf-8\").trim();\n  } catch {\n    return \"\";\n  }\n}\n\n/**\n * Move ~/.pai/identity.txt into PAI_HOME, following the same copy /\n * verify-byte-identical / rename-aside semantics as every other per-user\n * file (see migratePaiFile). `from`/`to` are overridable for tests.\n */\nexport function migrateIdentityFile(\n  opts: { dryRun?: boolean; from?: string; to?: string } = {}\n): MigrateFileResult {\n  const toPath = opts.to ?? paiHomePath(\"identity.txt\");\n  const fromPath = opts.from ?? IDENTITY_FILE;\n  return migratePaiFile(toPath, [fromPath], { dryRun: opts.dryRun });\n}\n\n// ---------------------------------------------------------------------------\n// L1: Essential Story\n// ---------------------------------------------------------------------------\n\n/**\n * Find the Notes directory for a project given its root_path from the registry.\n * Checks local Notes/ first, then central ~/.claude/projects/... path.\n */\nfunction findNotesDirForProject(rootPath: string): string | null {\n  // Check local Notes directories first\n  const localCandidates = [\n    join(rootPath, \"Notes\"),\n    join(rootPath, \"notes\"),\n    join(rootPath, \".claude\", \"Notes\"),\n  ];\n  for (const p of localCandidates) {\n    if (existsSync(p)) return p;\n  }\n\n  // Fall back to central ~/.claude/projects/{encoded}/Notes\n  const encoded = rootPath\n    .replace(/\\//g, \"-\")\n    .replace(/\\./g, \"-\")\n    .replace(/ /g, \"-\");\n  const centralNotes = join(\n    homedir(),\n    \".claude\",\n    \"projects\",\n    encoded,\n    \"Notes\"\n  );\n  if (existsSync(centralNotes)) return centralNotes;\n\n  return null;\n}\n\n/**\n * Recursively find all .md session note files in a Notes directory.\n * Handles both flat layout (Notes/*.md) and month-subdirectory layout\n * (Notes/YYYY/MM/*.md). Returns files sorted newest-first by filename\n * (note numbers are monotonically increasing, so lexicographic = newest-last,\n * so we reverse).\n */\nfunction findSessionNotes(notesDir: string): string[] {\n  const result: string[] = [];\n\n  const scanDir = (dir: string) => {\n    if (!existsSync(dir)) return;\n    let entries: string[];\n    try {\n      entries = readdirSync(dir, { withFileTypes: true } as Parameters<typeof readdirSync>[1] as any)\n        .map((e: any) => ({ name: e.name, isDir: e.isDirectory() }));\n    } catch {\n      return;\n    }\n\n    for (const entry of entries as Array<{ name: string; isDir: boolean }>) {\n      const fullPath = join(dir, entry.name);\n      if (entry.isDir) {\n        // Recurse into YYYY/MM subdirectories\n        scanDir(fullPath);\n      } else if (entry.name.match(/^\\d{3,4}[\\s_-].*\\.md$/)) {\n        result.push(fullPath);\n      }\n    }\n  };\n\n  scanDir(notesDir);\n\n  // Sort newest-first by the DATE in the filename, not the note number.\n  //\n  // The note number is only monotonic within one uninterrupted numbering run.\n  // It restarts \u2014 per month directory, and after a registry merge renumbers a\n  // project \u2014 so \"highest number\" and \"most recent\" diverge. Observed live:\n  // `0184 - 2026-02-22` outranked `0008 - 2026-08-01`, and the wake-up context\n  // handed a resumed session five-month-old material as its recent history.\n  //\n  // `NNNN - YYYY-MM-DD - Title.md` is the enforced layout, so the date is the\n  // reliable key. Number breaks ties within a day; mtime is the last resort\n  // for notes that predate the naming convention.\n  const dateOf = (p: string): string =>\n    basename(p).match(/(\\d{4}-\\d{2}-\\d{2})/)?.[1] ?? \"\";\n  const numberOf = (p: string): number =>\n    parseInt(basename(p).match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n  const mtimeOf = (p: string): number => {\n    try {\n      return statSync(p).mtimeMs;\n    } catch {\n      return 0;\n    }\n  };\n\n  result.sort((a, b) => {\n    const dateA = dateOf(a);\n    const dateB = dateOf(b);\n    // Undated notes sort last rather than winning on an empty string.\n    if (dateA !== dateB) {\n      if (!dateA) return 1;\n      if (!dateB) return -1;\n      return dateB.localeCompare(dateA);\n    }\n    const byNumber = numberOf(b) - numberOf(a);\n    if (byNumber !== 0) return byNumber;\n    return mtimeOf(b) - mtimeOf(a);\n  });\n\n  return result;\n}\n\n/**\n * Extract the most important lines from a session note.\n * Prioritises: Work Done items, Key Decisions, Next Steps, Checkpoint headings.\n * Returns a condensed string under maxChars.\n */\nfunction normalizeLine(line: string): string {\n  return line\n    .replace(/^(- \\[[ x]\\] |- |\\* |\\d+\\.\\s)/, \"\")\n    .trim()\n    .replace(/\\s+/g, \" \");\n}\n\nfunction extractKeyLines(\n  content: string,\n  maxChars: number,\n  seen: Set<string> = new Set(),\n  seenLabels: Set<string> = new Set()\n): string {\n  const lines = content.split(\"\\n\");\n  const selected: string[] = [];\n  let inTargetSection = false;\n  let currentSection = \"\";\n  let charCount = 0;\n  // Heading text seen since the last line was emitted. Buffered rather than\n  // pushed immediately: a heading with no surviving lines under it (every\n  // line deduped against an earlier note) must not appear at all.\n  let pendingLabel: string | null = null;\n\n  // First pass: collect lines from priority sections\n  for (const line of lines) {\n    // Detect section headers\n    const h2Match = line.match(/^## (.+)$/);\n    const h3Match = line.match(/^### (.+)$/);\n    if (h2Match) {\n      currentSection = h2Match[1];\n      inTargetSection = EXTRACT_SECTIONS.some((s) =>\n        currentSection.toLowerCase().includes(s.toLowerCase())\n      );\n      pendingLabel = null;\n      continue;\n    }\n    if (h3Match) {\n      // Checkpoints / sub-sections \u2014 buffer as a candidate label.\n      if (inTargetSection) {\n        pendingLabel = h3Match[1];\n      }\n      continue;\n    }\n\n    if (!inTargetSection) continue;\n\n    // Skip blank lines and HTML comments\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith(\"<!--\") || trimmed === \"---\") continue;\n\n    // Include checkbox items, bold text, and plain text lines\n    if (\n      trimmed.startsWith(\"- \") ||\n      trimmed.startsWith(\"* \") ||\n      trimmed.match(/^\\d+\\./) ||\n      trimmed.startsWith(\"**\")\n    ) {\n      const normalized = normalizeLine(trimmed);\n      if (seen.has(normalized)) continue;\n\n      let labelText = \"\";\n      let labelKey = \"\";\n      if (pendingLabel !== null) {\n        labelKey = pendingLabel.toLowerCase().trim();\n        if (!seenLabels.has(labelKey)) {\n          labelText = `[${pendingLabel}]`;\n        }\n      }\n\n      if (charCount + labelText.length + trimmed.length + 2 > maxChars) break;\n\n      if (labelText) {\n        selected.push(labelText);\n        seenLabels.add(labelKey);\n        charCount += labelText.length + 1;\n      }\n      pendingLabel = null;\n\n      seen.add(normalized);\n      selected.push(trimmed);\n      charCount += trimmed.length + 1;\n    }\n  }\n\n  return selected.join(\"\\n\");\n}\n\n/**\n * Build the L1 essential story block.\n *\n * Reads the most recent session notes for the project and extracts the key\n * lines (Work Done, Key Decisions, Next Steps) within the token budget.\n *\n * @param rootPath   The project root path (from the registry).\n * @param tokenBudget  Max tokens to consume. Default 800 (~3200 chars).\n * @returns Formatted L1 block, or empty string if no notes found.\n */\nexport function buildL1EssentialStory(\n  rootPath: string,\n  tokenBudget = L1_TOKEN_BUDGET\n): string {\n  const charBudget = tokenBudget * 4;\n  const notesDir = findNotesDirForProject(rootPath);\n  if (!notesDir) return \"\";\n\n  const noteFiles = findSessionNotes(notesDir).slice(0, L1_MAX_NOTES);\n  if (noteFiles.length === 0) return \"\";\n\n  const sections: string[] = [];\n  const seen = new Set<string>();\n  const seenLabels = new Set<string>();\n  let remaining = charBudget;\n\n  for (const noteFile of noteFiles) {\n    if (remaining <= 50) break;\n\n    let content: string;\n    try {\n      content = readFileSync(noteFile, \"utf-8\");\n    } catch {\n      continue;\n    }\n\n    // Extract the note date and title from the filename\n    const name = basename(noteFile);\n    const titleMatch = name.match(/^\\d+ - (\\d{4}-\\d{2}-\\d{2}) - (.+)\\.md$/);\n    const dateLabel = titleMatch ? titleMatch[1] : \"\";\n    const titleLabel = titleMatch\n      ? titleMatch[2]\n      : name.replace(/^\\d+ - /, \"\").replace(/\\.md$/, \"\");\n\n    // Skip if nothing useful extracted from this note\n    const perNoteChars = Math.min(remaining, Math.floor(charBudget / noteFiles.length) + 200);\n    const extracted = extractKeyLines(content, perNoteChars, seen, seenLabels);\n    // A note whose every extracted line survived only as a label (all its\n    // real lines deduped against an earlier, newer note) contributes nothing\n    // \u2014 including its title.\n    const hasContentLine = extracted\n      .split(\"\\n\")\n      .some((l) => !/^\\[.*\\]$/.test(l));\n    if (!extracted || !hasContentLine) continue;\n\n    const noteBlock = `[${dateLabel} - ${titleLabel}]\\n${extracted}`;\n    sections.push(noteBlock);\n    remaining -= noteBlock.length + 1;\n  }\n\n  if (sections.length === 0) return \"\";\n\n  return sections.join(\"\\n\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Combined: buildWakeupContext\n// ---------------------------------------------------------------------------\n\n/**\n * Build the combined wake-up context block (L0 + L1).\n *\n * Returns a formatted string suitable for injection as a system-reminder,\n * or an empty string if both layers are empty.\n *\n * @param rootPath   Project root path for L1 note lookup. Optional.\n * @param tokenBudget  L1 token budget. Default 800.\n * @param opts.skipStory  Skip the L1 essential story. Set when a handover\n *   checkpoint was already injected \u2014 the story re-summarises the same note\n *   the handover came from, at lower quality.\n */\nexport function buildWakeupContext(\n  rootPath?: string,\n  tokenBudget = L1_TOKEN_BUDGET,\n  opts: { skipStory?: boolean } = {}\n): string {\n  const identity = loadL0Identity();\n  const essentialStory = rootPath && !opts.skipStory\n    ? buildL1EssentialStory(rootPath, tokenBudget)\n    : \"\";\n\n  if (!identity && !essentialStory) return \"\";\n\n  const parts: string[] = [];\n\n  if (identity) {\n    parts.push(`## L0 Identity\\n\\n${identity}`);\n  }\n\n  if (essentialStory) {\n    parts.push(`## L1 Essential Story\\n\\n${essentialStory}`);\n  }\n\n  return parts.join(\"\\n\\n\");\n}\n", "/**\n * pai-home.ts \u2014 the PAI_HOME namespace directory and generic per-user file\n * migration into it.\n *\n * Every PAI-owned per-user file (workers.yaml, config.json, whisper-rules.md,\n * advisor-mode.json, ...) lives under PAI_HOME (~/.claude/pai by default) so\n * nothing PAI writes can ever collide with a file Claude Code itself\n * introduces under ~/.claude. Decided 2026-09-19 \u2014 see docs/workers-config.md.\n */\n\nimport { existsSync, mkdirSync, readFileSync, copyFileSync, renameSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, dirname } from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\n\n/**\n * Byte-identical check that works for files of any size. `cmp` compares by\n * streaming rather than loading either file into memory, unlike a\n * readFileSync + Buffer.compare \u2014 which throws (\"File size is greater than\n * 2 GiB\") on anything past Node's 2GiB single-read ceiling, a real limit hit\n * migrating a multi-gigabyte federation.db.\n */\nfunction filesByteIdentical(a: string, b: string): boolean {\n  try {\n    execFileSync(\"cmp\", [\"-s\", a, b], { stdio: \"pipe\" });\n    return true;\n  } catch (e) {\n    if (e && typeof e === \"object\" && \"status\" in e && (e as { status: number }).status === 1) return false;\n    // cmp missing or errored for an unrelated reason \u2014 fall back to an\n    // in-memory compare (fine for the small files this path normally sees).\n    return Buffer.compare(readFileSync(a), readFileSync(b)) === 0;\n  }\n}\n\n/** PAI_HOME (test isolation, power users) overrides the default namespace dir. */\nexport function paiHomeDir(): string {\n  return process.env.PAI_HOME || join(homedir(), \".claude\", \"pai\");\n}\n\n/** A path under PAI_HOME, e.g. `paiHomePath(\"config.json\")`. */\nexport function paiHomePath(...segments: string[]): string {\n  return join(paiHomeDir(), ...segments);\n}\n\nconst noticesPrinted = new Set<string>();\n\n/**\n * Resolve a per-user PAI file: the new PAI_HOME path if it exists, else the\n * first existing entry in `oldCandidates` (checked in order \u2014 most recent\n * old location first), else the new path (the target a first write\n * creates). Prints one stderr notice per process per new-path when a\n * fallback location is actually used.\n */\nexport function resolvePaiFile(newPath: string, oldCandidates: string[], migrateHint: string): string {\n  if (existsSync(newPath)) return newPath;\n  for (const old of oldCandidates) {\n    if (existsSync(old)) {\n      if (!noticesPrinted.has(newPath)) {\n        noticesPrinted.add(newPath);\n        process.stderr.write(\n          `pai: ${old} is at an old location \u2014 run \\`${migrateHint}\\` to move it to ${newPath}\\n`\n        );\n      }\n      return old;\n    }\n  }\n  return newPath;\n}\n\nexport class PaiFileMigrationError extends Error {}\n\nexport interface MigrateFileResult {\n  fromPath: string | null;\n  toPath: string;\n  dryRun: boolean;\n  note?: string;\n}\n\n/**\n * Move a per-user PAI file to its new PAI_HOME location: copy, verify\n * byte-identical, then rename the source to `<name>.migrated-<YYYYMMDD>`\n * (never deleted). Idempotent: if the new path already holds bytes\n * identical to the found source, the source is just renamed aside; if it\n * differs, this refuses rather than overwrite silently.\n */\nexport function migratePaiFile(\n  newPath: string,\n  oldCandidates: string[],\n  opts: { dryRun?: boolean } = {}\n): MigrateFileResult {\n  const fromPath = oldCandidates.find((p) => existsSync(p)) ?? null;\n  if (!fromPath) {\n    const note = existsSync(newPath) ? \"already at new location\" : \"nothing to migrate \u2014 file does not exist yet\";\n    return { fromPath: null, toPath: newPath, dryRun: !!opts.dryRun, note };\n  }\n  if (opts.dryRun) return { fromPath, toPath: newPath, dryRun: true };\n\n  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n\n  if (existsSync(newPath)) {\n    if (filesByteIdentical(fromPath, newPath)) {\n      renameSync(fromPath, `${fromPath}.migrated-${stamp}`);\n      return { fromPath, toPath: newPath, dryRun: false, note: \"identical \u2014 old file renamed aside\" };\n    }\n    throw new PaiFileMigrationError(\n      `${newPath} already exists and differs from ${fromPath} \u2014 resolve manually, nothing changed`\n    );\n  }\n\n  const dir = dirname(newPath);\n  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n  copyFileSync(fromPath, newPath);\n\n  if (!filesByteIdentical(fromPath, newPath)) {\n    throw new PaiFileMigrationError(\n      `${newPath}: copy did not match ${fromPath} byte-for-byte \u2014 aborting, old file left in place`\n    );\n  }\n\n  renameSync(fromPath, `${fromPath}.migrated-${stamp}`);\n  return { fromPath, toPath: newPath, dryRun: false };\n}\n\nexport interface MigrateDirResult {\n  fromDir: string | null;\n  toDir: string;\n  dryRun: boolean;\n  movedCount?: number;\n  note?: string;\n}\n\n/**\n * Move a per-user PAI directory (queries/, session-state/, ...) into\n * PAI_HOME: move every entry from the first found old candidate into the\n * new dir (never overwriting an existing entry there), then rename the now-\n * empty old dir aside to `<name>.migrated-<YYYYMMDD>` (never deleted).\n * Idempotent \u2014 an already-migrated dir has nothing left to find.\n */\nexport function migratePaiDir(\n  newDir: string,\n  oldCandidates: string[],\n  opts: { dryRun?: boolean } = {}\n): MigrateDirResult {\n  const fromDir = oldCandidates.find((p) => existsSync(p) && statSync(p).isDirectory()) ?? null;\n  if (!fromDir) {\n    const note = existsSync(newDir) ? \"already at new location\" : \"nothing to migrate \u2014 directory does not exist yet\";\n    return { fromDir: null, toDir: newDir, dryRun: !!opts.dryRun, note };\n  }\n\n  const entries = readdirSync(fromDir);\n  if (opts.dryRun) return { fromDir, toDir: newDir, dryRun: true, movedCount: entries.length };\n\n  if (!existsSync(newDir)) mkdirSync(newDir, { recursive: true });\n\n  let moved = 0;\n  const collided: string[] = [];\n  for (const entry of entries) {\n    const src = join(fromDir, entry);\n    const dst = join(newDir, entry);\n    if (existsSync(dst)) {\n      collided.push(entry);\n      continue;\n    }\n    renameSync(src, dst);\n    moved++;\n  }\n\n  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n  const remaining = readdirSync(fromDir);\n  const note = collided.length\n    ? `${collided.length} entrie(s) left in ${fromDir} \u2014 name already existed in ${newDir}`\n    : undefined;\n\n  if (remaining.length === 0) {\n    renameSync(fromDir, `${fromDir}.migrated-${stamp}`);\n  }\n\n  return { fromDir, toDir: newDir, dryRun: false, movedCount: moved, note };\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 * handover-budget.ts \u2014 cap the `## Continue` handover body injected at\n * SessionStart.\n *\n * Text tokenises at ~2.6 chars per token; a 1,600-char budget is ~620 tokens.\n * Together with the hook's 117-token fixed part, this stays under the 1,000-token\n * SessionStart limit, eliding retrospective `d=` (done) and `t=` (tests) fields\n * while preserving `g=` (goal), `@n` file pointers, and `z=` (state).\n */\n\nexport const HANDOVER_CHAR_BUDGET = 1600;\n\nexport interface BudgetResult {\n  body: string;\n  elided: string[];\n  truncated: boolean;\n}\n\ninterface Field {\n  key: string;\n  lines: string[];\n}\n\nconst FIELD_START = /^([a-zA-Z@][a-zA-Z0-9]*)=/;\n\nfunction splitFields(body: string): { preamble: string[]; fields: Field[] } {\n  const lines = body.split(\"\\n\");\n  const preamble: string[] = [];\n  const fields: Field[] = [];\n  let current: Field | null = null;\n  for (const line of lines) {\n    const m = line.match(FIELD_START);\n    if (m) {\n      current = { key: m[1], lines: [line] };\n      fields.push(current);\n    } else if (current) {\n      current.lines.push(line);\n    } else {\n      preamble.push(line);\n    }\n  }\n  return { preamble, fields };\n}\n\nfunction elisionLine(key: string, charCount: number, sourcePath: string): string {\n  return `${key}= elided, ${charCount} chars; full text in ${sourcePath} (## Continue)`;\n}\n\nfunction render(preamble: string[], fields: Field[]): string {\n  const parts = [...preamble];\n  for (const f of fields) parts.push(...f.lines);\n  return parts.join(\"\\n\");\n}\n\n/**\n * Elide the `d=` and `t=` fields (in that order, only while still over\n * budget) before falling back to a hard truncation \u2014 those two fields are\n * retrospective, everything else (`g=`, `@n` pointers, `z=`) is what a\n * resumer actually needs.\n */\nexport function applyHandoverBudget(\n  body: string,\n  sourcePath: string,\n  budget: number = HANDOVER_CHAR_BUDGET\n): BudgetResult {\n  if (body.length <= budget) {\n    return { body, elided: [], truncated: false };\n  }\n\n  const { preamble, fields } = splitFields(body);\n  const elided: string[] = [];\n\n  if (fields.length > 0) {\n    for (const key of [\"d\", \"t\"]) {\n      if (render(preamble, fields).length <= budget) break;\n      const field = fields.find((f) => f.key === key);\n      if (!field) continue;\n      const charCount = field.lines.join(\"\\n\").length;\n      field.lines = [elisionLine(key, charCount, sourcePath)];\n      elided.push(key);\n    }\n  }\n\n  const result = render(preamble, fields);\n  if (fields.length === 0 || result.length > budget) {\n    const source = fields.length === 0 ? body : result;\n    const truncated =\n      source.slice(0, budget) +\n      `\\n[handover truncated at ${budget} chars; full text in ${sourcePath} (## Continue)]`;\n    return { body: truncated, elided, truncated: true };\n  }\n\n  return { body: result, elided, truncated: false };\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 * 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 * Push notification helpers \u2014 WhatsApp-aware with ntfy.sh fallback.\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\n/**\n * Check if a messaging MCP server (AIBroker, Whazaa, or Telex) is configured.\n * When any messaging server is active, the AI handles notifications via MCP\n * and ntfy is skipped to avoid duplicates.\n */\nexport function isWhatsAppEnabled(): boolean {\n  try {\n    const settingsPath = join(homedir(), '.claude', 'settings.json');\n    if (!existsSync(settingsPath)) return false;\n\n    const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n    const enabled: string[] = settings.enabledMcpjsonServers || [];\n    return enabled.includes('aibroker') || enabled.includes('whazaa') || enabled.includes('telex');\n  } catch {\n    return false;\n  }\n}\n\n/**\n * Send push notification \u2014 WhatsApp-aware with ntfy fallback.\n *\n * When WhatsApp (Whazaa) is enabled in MCP config, ntfy is SKIPPED\n * because the AI sends WhatsApp messages directly via MCP.\n * When WhatsApp is NOT configured, ntfy fires as the fallback channel.\n */\nexport async function sendNtfyNotification(message: string, retries = 2): Promise<boolean> {\n  if (isWhatsAppEnabled()) {\n    console.error(`WhatsApp (Whazaa) enabled in MCP config \u2014 skipping ntfy`);\n    return true;\n  }\n\n  const topic = process.env.NTFY_TOPIC;\n\n  if (!topic) {\n    console.error('NTFY_TOPIC not set and WhatsApp not active \u2014 notifications disabled');\n    return false;\n  }\n\n  for (let attempt = 0; attempt <= retries; attempt++) {\n    try {\n      const response = await fetch(`https://ntfy.sh/${topic}`, {\n        method: 'POST',\n        body: message,\n        headers: {\n          'Title': 'Claude Code',\n          'Priority': 'default',\n        },\n      });\n\n      if (response.ok) {\n        console.error(`ntfy.sh notification sent (WhatsApp inactive): \"${message}\"`);\n        return true;\n      } else {\n        console.error(`ntfy.sh attempt ${attempt + 1} failed: ${response.status}`);\n      }\n    } catch (error) {\n      console.error(`ntfy.sh attempt ${attempt + 1} error: ${error}`);\n    }\n\n    if (attempt < retries) {\n      await new Promise(resolve => setTimeout(resolve, 1000));\n    }\n  }\n\n  console.error('ntfy.sh notification failed after all retries');\n  return false;\n}\n", "/**\n * Session note creation, editing, checkpointing, renaming, and finalization.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Get or create the YYYY/MM subdirectory for the current month inside notesDir. */\nfunction getMonthDir(notesDir: string): string {\n  const now = new Date();\n  const year = String(now.getFullYear());\n  const month = String(now.getMonth() + 1).padStart(2, '0');\n  const monthDir = join(notesDir, year, month);\n  if (!existsSync(monthDir)) {\n    mkdirSync(monthDir, { recursive: true });\n  }\n  return monthDir;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Get the next note number (4-digit format: 0001, 0002, etc.).\n * Numbers are scoped per YYYY/MM directory.\n */\nexport function getNextNoteNumber(notesDir: string): string {\n  const monthDir = getMonthDir(notesDir);\n\n  const files = readdirSync(monthDir)\n    .filter(f => f.match(/^\\d{3,4}[\\s_-]/))\n    .sort();\n\n  if (files.length === 0) return '0001';\n\n  let maxNumber = 0;\n  for (const file of files) {\n    const digitMatch = file.match(/^(\\d+)/);\n    if (digitMatch) {\n      const num = parseInt(digitMatch[1], 10);\n      if (num > maxNumber) maxNumber = num;\n    }\n  }\n\n  return String(maxNumber + 1).padStart(4, '0');\n}\n\n/**\n * Get the current (latest) note file path, or null if none exists.\n * Searches current month \u2192 previous month \u2192 flat notesDir (legacy).\n */\nexport function getCurrentNotePath(notesDir: string): string | null {\n  if (!existsSync(notesDir)) return null;\n\n  const findLatestIn = (dir: string): string | null => {\n    if (!existsSync(dir)) return null;\n    const files = readdirSync(dir)\n      .filter(f => f.match(/^\\d{3,4}[\\s_-].*\\.md$/))\n      .sort((a, b) => {\n        const numA = parseInt(a.match(/^(\\d+)/)?.[1] || '0', 10);\n        const numB = parseInt(b.match(/^(\\d+)/)?.[1] || '0', 10);\n        return numA - numB;\n      });\n    if (files.length === 0) return null;\n    return join(dir, files[files.length - 1]);\n  };\n\n  const now = new Date();\n  const year = String(now.getFullYear());\n  const month = String(now.getMonth() + 1).padStart(2, '0');\n  const currentMonthDir = join(notesDir, year, month);\n  const found = findLatestIn(currentMonthDir);\n  if (found) return found;\n\n  const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n  const prevYear = String(prevDate.getFullYear());\n  const prevMonth = String(prevDate.getMonth() + 1).padStart(2, '0');\n  const prevMonthDir = join(notesDir, prevYear, prevMonth);\n  const prevFound = findLatestIn(prevMonthDir);\n  if (prevFound) return prevFound;\n\n  return findLatestIn(notesDir);\n}\n\n/**\n * Create a new session note.\n * Format: \"NNNN - YYYY-MM-DD - New Session.md\" filed into YYYY/MM subdirectory.\n * Claude MUST rename at session end with a meaningful description.\n */\nexport function createSessionNote(notesDir: string, description: string): string {\n  const noteNumber = getNextNoteNumber(notesDir);\n  const date = new Date().toISOString().split('T')[0];\n  const monthDir = getMonthDir(notesDir);\n  const filename = `${noteNumber} - ${date} - New Session.md`;\n  const filepath = join(monthDir, filename);\n\n  const content = `# Session ${noteNumber}: ${description}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n<!-- PAI will add completed work here during session -->\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n  writeFileSync(filepath, content);\n  console.error(`Created session note: ${filename}`);\n\n  return filepath;\n}\n\n/** Append a checkpoint to the current session note. */\nexport function appendCheckpoint(notePath: string, checkpoint: string): void {\n  if (!existsSync(notePath)) {\n    console.error(`Note file not found, recreating: ${notePath}`);\n    try {\n      const parentDir = join(notePath, '..');\n      if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n      const noteFilename = basename(notePath);\n      const numberMatch = noteFilename.match(/^(\\d+)/);\n      const noteNumber = numberMatch ? numberMatch[1] : '0000';\n      const date = new Date().toISOString().split('T')[0];\n      const content = `# Session ${noteNumber}: Recovered\\n\\n**Date:** ${date}\\n**Status:** In Progress\\n\\n---\\n\\n## Work Done\\n\\n<!-- PAI will add completed work here during session -->\\n\\n---\\n\\n## Next Steps\\n\\n<!-- To be filled at session end -->\\n\\n---\\n\\n**Tags:** #Session\\n`;\n      writeFileSync(notePath, content);\n      console.error(`Recreated session note: ${noteFilename}`);\n    } catch (err) {\n      console.error(`Failed to recreate note: ${err}`);\n      return;\n    }\n  }\n\n  const content = readFileSync(notePath, 'utf-8');\n  const timestamp = new Date().toISOString();\n  const checkpointText = `\\n### Checkpoint ${timestamp}\\n\\n${checkpoint}\\n`;\n\n  const nextStepsIndex = content.indexOf('## Next Steps');\n  const newContent = nextStepsIndex !== -1\n    ? content.substring(0, nextStepsIndex) + checkpointText + content.substring(nextStepsIndex)\n    : content + checkpointText;\n\n  writeFileSync(notePath, newContent);\n  console.error(`Checkpoint added to: ${basename(notePath)}`);\n}\n\n/** Work item for session notes. */\nexport interface WorkItem {\n  title: string;\n  details?: string[];\n  completed?: boolean;\n}\n\n/** Add work items to the \"Work Done\" section of a session note. */\nexport function addWorkToSessionNote(notePath: string, workItems: WorkItem[], sectionTitle?: string): void {\n  if (!existsSync(notePath)) {\n    console.error(`Note file not found: ${notePath}`);\n    return;\n  }\n\n  let content = readFileSync(notePath, 'utf-8');\n\n  let workText = '';\n  if (sectionTitle) workText += `\\n### ${sectionTitle}\\n\\n`;\n\n  for (const item of workItems) {\n    const checkbox = item.completed !== false ? '[x]' : '[ ]';\n    workText += `- ${checkbox} **${item.title}**\\n`;\n    if (item.details && item.details.length > 0) {\n      for (const detail of item.details) {\n        workText += `  - ${detail}\\n`;\n      }\n    }\n  }\n\n  const workDoneMatch = content.match(/## Work Done\\n\\n(<!-- .*? -->)?/);\n  if (workDoneMatch) {\n    const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;\n    content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);\n  } else {\n    const nextStepsIndex = content.indexOf('## Next Steps');\n    if (nextStepsIndex !== -1) {\n      content = content.substring(0, nextStepsIndex) + workText + '\\n' + content.substring(nextStepsIndex);\n    }\n  }\n\n  writeFileSync(notePath, content);\n  console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);\n}\n\n/**\n * Check if a candidate title is meaningless / garbage.\n * Public wrapper around the internal filter for use by other hooks.\n */\nexport function isMeaningfulTitle(text: string): boolean {\n  return !isMeaninglessCandidate(text);\n}\n\n/** Sanitize a string for use in a filename. */\nexport function sanitizeForFilename(str: string): string {\n  return str\n    .toLowerCase()\n    .replace(/[^a-z0-9\\s-]/g, '')\n    .replace(/\\s+/g, '-')\n    .replace(/-+/g, '-')\n    .replace(/^-|-$/g, '')\n    .substring(0, 50);\n}\n\n/**\n * Return true if the candidate string should be rejected as a meaningful name.\n * Rejects file paths, shebangs, timestamps, system noise, XML tags, hashes, etc.\n */\nfunction isMeaninglessCandidate(text: string): boolean {\n  const t = text.trim();\n  if (!t) return true;\n  if (t.length < 5) return true;                              // too short to be meaningful\n  if (t.startsWith('/') || t.startsWith('~')) return true;    // file path\n  if (t.startsWith('#!')) return true;                         // shebang\n  if (t.includes('[object Object]')) return true;              // serialization artifact\n  if (/^\\d{4}-\\d{2}-\\d{2}(T[\\d:.Z+-]+)?$/.test(t)) return true; // ISO timestamp\n  if (/^\\d{1,2}:\\d{2}(:\\d{2})?(\\s*(AM|PM))?$/i.test(t)) return true; // time-only\n  if (/^<[a-z-]+[\\s/>]/i.test(t)) return true;               // XML/HTML tags (<task-notification>, etc.)\n  if (/^[0-9a-f]{10,}$/i.test(t)) return true;               // hex hash strings\n  if (/^Exit code \\d+/i.test(t)) return true;                 // exit code messages\n  if (/^Error:/i.test(t)) return true;                        // error messages\n  if (/^This session is being continued/i.test(t)) return true; // continuation boilerplate\n  if (/^\\(Bash completed/i.test(t)) return true;              // bash output noise\n  if (/^Task Notification$/i.test(t)) return true;            // literal \"Task Notification\"\n  if (/^New Session$/i.test(t)) return true;                  // placeholder title\n  if (/^Recovered Session$/i.test(t)) return true;            // placeholder title\n  if (/^Continued Session$/i.test(t)) return true;            // placeholder title\n  if (/^Untitled Session$/i.test(t)) return true;             // placeholder title\n  if (/^Context Compression$/i.test(t)) return true;          // compression artifact\n  if (/^[A-Fa-f0-9]{8,}\\s+Output$/i.test(t)) return true;   // hash + \"Output\" pattern\n  return false;\n}\n\n/**\n * Extract a meaningful name from session note content and summary.\n * Looks at Work Done section headers, bold text, and summary.\n */\nexport function extractMeaningfulName(noteContent: string, summary: string): string {\n  const workDoneMatch = noteContent.match(/## Work Done\\n\\n([\\s\\S]*?)(?=\\n---|\\n## Next)/);\n\n  if (workDoneMatch) {\n    const workDoneSection = workDoneMatch[1];\n\n    const subheadings = workDoneSection.match(/### ([^\\n]+)/g);\n    if (subheadings && subheadings.length > 0) {\n      const firstHeading = subheadings[0].replace('### ', '').trim();\n      if (!isMeaninglessCandidate(firstHeading) && firstHeading.length > 5 && firstHeading.length < 60) {\n        return sanitizeForFilename(firstHeading);\n      }\n    }\n\n    const boldMatches = workDoneSection.match(/\\*\\*([^*]+)\\*\\*/g);\n    if (boldMatches && boldMatches.length > 0) {\n      const firstBold = boldMatches[0].replace(/\\*\\*/g, '').trim();\n      if (!isMeaninglessCandidate(firstBold) && firstBold.length > 3 && firstBold.length < 50) {\n        return sanitizeForFilename(firstBold);\n      }\n    }\n\n    const numberedItems = workDoneSection.match(/^\\d+\\.\\s+\\*\\*([^*]+)\\*\\*/m);\n    if (numberedItems && !isMeaninglessCandidate(numberedItems[1])) {\n      return sanitizeForFilename(numberedItems[1]);\n    }\n  }\n\n  if (summary && summary.length > 5 && summary !== 'Session completed.' && !isMeaninglessCandidate(summary)) {\n    const cleanSummary = summary\n      .replace(/[^\\w\\s-]/g, ' ')\n      .trim()\n      .split(/\\s+/)\n      .slice(0, 5)\n      .join(' ');\n    if (cleanSummary.length > 3 && !isMeaninglessCandidate(cleanSummary)) {\n      return sanitizeForFilename(cleanSummary);\n    }\n  }\n\n  return '';\n}\n\n/**\n * Rename a session note with a meaningful name.\n * Always uses \"NNNN - YYYY-MM-DD - Description.md\" format.\n * Returns the new path, or original path if rename fails.\n */\nexport function renameSessionNote(notePath: string, meaningfulName: string): string {\n  if (!meaningfulName || !existsSync(notePath)) return notePath;\n\n  const dir = join(notePath, '..');\n  const oldFilename = basename(notePath);\n\n  const correctMatch = oldFilename.match(/^(\\d{3,4}) - (\\d{4}-\\d{2}-\\d{2}) - .*\\.md$/);\n  const legacyMatch = oldFilename.match(/^(\\d{3,4})_(\\d{4}-\\d{2}-\\d{2})_.*\\.md$/);\n  const match = correctMatch || legacyMatch;\n  if (!match) return notePath;\n\n  const [, noteNumber, date] = match;\n\n  const titleCaseName = meaningfulName\n    .split(/[\\s_-]+/)\n    .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n    .join(' ')\n    .trim();\n\n  const paddedNumber = noteNumber.padStart(4, '0');\n  const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;\n  const newPath = join(dir, newFilename);\n\n  if (newFilename === oldFilename) return notePath;\n\n  try {\n    renameSync(notePath, newPath);\n    console.error(`Renamed note: ${oldFilename} \u2192 ${newFilename}`);\n    return newPath;\n  } catch (error) {\n    console.error(`Could not rename note: ${error}`);\n    return notePath;\n  }\n}\n\n/** Update the session note's H1 title and rename the file. */\nexport function updateSessionNoteTitle(notePath: string, newTitle: string): void {\n  if (!existsSync(notePath)) {\n    console.error(`Note file not found: ${notePath}`);\n    return;\n  }\n\n  let content = readFileSync(notePath, 'utf-8');\n  content = content.replace(/^# Session \\d+:.*$/m, (match) => {\n    const sessionNum = match.match(/Session (\\d+)/)?.[1] || '';\n    return `# Session ${sessionNum}: ${newTitle}`;\n  });\n  writeFileSync(notePath, content);\n  renameSessionNote(notePath, sanitizeForFilename(newTitle));\n}\n\n/**\n * Finalize session note \u2014 mark as complete, add summary, rename with meaningful name.\n * IDEMPOTENT: subsequent calls are no-ops if already finalized.\n * Returns the final path (may be renamed).\n */\nexport function finalizeSessionNote(notePath: string, summary: string): string {\n  if (!existsSync(notePath)) {\n    console.error(`Note file not found: ${notePath}`);\n    return notePath;\n  }\n\n  let content = readFileSync(notePath, 'utf-8');\n\n  if (content.includes('**Status:** Completed')) {\n    console.error(`Note already finalized: ${basename(notePath)}`);\n    return notePath;\n  }\n\n  content = content.replace('**Status:** In Progress', '**Status:** Completed');\n\n  if (!content.includes('**Completed:**')) {\n    const completionTime = new Date().toISOString();\n    content = content.replace(\n      '---\\n\\n## Work Done',\n      `**Completed:** ${completionTime}\\n\\n---\\n\\n## Work Done`\n    );\n  }\n\n  const nextStepsMatch = content.match(/## Next Steps\\n\\n(<!-- .*? -->)/);\n  if (nextStepsMatch) {\n    content = content.replace(\n      nextStepsMatch[0],\n      `## Next Steps\\n\\n${summary || 'Session completed.'}`\n    );\n  }\n\n  writeFileSync(notePath, content);\n  console.error(`Session note finalized: ${basename(notePath)}`);\n\n  const meaningfulName = extractMeaningfulName(content, summary);\n  if (meaningfulName) {\n    return renameSessionNote(notePath, meaningfulName);\n  }\n\n  return notePath;\n}\n"],
  "mappings": "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",
  "names": ["existsSync", "readFileSync", "join", "basename", "dirname", "resolve", "homedir", "existsSync", "readdirSync", "readFileSync", "statSync", "join", "homedir", "join", "homedir", "existsSync", "readFileSync", "join", "existsSync", "homedir", "readdirSync", "statSync", "readFileSync", "homedir", "join", "existsSync", "readFileSync", "homedir", "existsSync", "readFileSync", "join", "existsSync", "join", "existsSync", "mkdirSync", "readdirSync", "copyFileSync", "join", "basename", "join", "basename", "existsSync", "join", "existsSync", "mkdirSync", "readdirSync", "join", "copyFileSync", "join", "existsSync", "join", "existsSync", "existsSync", "readFileSync", "join", "homedir", "resolve", "existsSync", "mkdirSync", "readdirSync", "readFileSync", "renameSync", "join", "basename", "existsSync", "readFileSync", "join", "homedir", "resolve", "basename", "dirname", "mkdirSync", "writeFileSync"]
}
