{"version":3,"file":"checkpoint-block-DKYxCkBL.mjs","names":[],"sources":["../src/registry/moved.ts","../src/cli/commands/registry/utils.ts","../src/cli/commands/registry/scan.ts","../src/daemon/templates/triple-extraction-prompt.ts","../src/memory/kg-extraction.ts","../src/session/checkpoint-block.ts"],"sourcesContent":["/**\n * moved.ts — reconnect a project to transcripts that moved out from under it.\n *\n * The registry records an `encoded_dir`: the name Claude Code gave the folder\n * holding a project's transcripts. It is written once, when the project is\n * added, and nothing updates it when the project moves. So a project that has\n * been relocated points at a directory that is empty or gone, and every lookup\n * — checkpoints, handovers, session digests — quietly returns nothing.\n *\n * `resolveTranscriptDir` already re-derives the name from the project's current\n * root path, which fixes the case where the encoding is stale but the path is\n * right. It cannot fix the case where the ENCODING RULE itself does not\n * reproduce the folder name: iCloud paths with `~` in them, emoji segments, or\n * a folder Claude Code created under a path the project no longer has.\n *\n * This module answers that case by asking the transcripts instead of guessing.\n * Every transcript entry records the `cwd` it was written in, so the mapping\n * from a project root to its transcript folder is a fact sitting on disk rather\n * than something to be inferred from a naming convention. Reading it is slower\n * than a string transform and it is right, which is the correct trade for a\n * repair that runs on demand.\n */\n\nimport { existsSync, readdirSync, statSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n/** Where Claude Code keeps per-project transcript folders. */\nexport function claudeProjectsDir(): string {\n  return join(homedir(), \".claude\", \"projects\");\n}\n\n/**\n * Every transcript file for one project folder — live and archived.\n *\n * The archived half matters: `session-stop` moves all but the newest transcript\n * into `sessions/`, so a folder whose work is finished has an empty top level\n * and everything underneath. Counting only the top level reports a busy project\n * as unused, which is how an earlier audit of this same problem overstated the\n * breakage by a factor of three.\n */\nexport function transcriptFiles(projectDir: string): string[] {\n  const out: string[] = [];\n  for (const dir of [projectDir, join(projectDir, \"sessions\")]) {\n    if (!existsSync(dir)) continue;\n    try {\n      for (const entry of readdirSync(dir)) {\n        if (entry.endsWith(\".jsonl\")) out.push(join(dir, entry));\n      }\n    } catch {\n      /* unreadable — treat as empty rather than failing the scan */\n    }\n  }\n  return out;\n}\n\n/**\n * The working directory a transcript was recorded in.\n *\n * Reads only the head of the file. `cwd` is present on the first entry and does\n * not change within a session, so scanning further would cost time to learn\n * nothing. Returns null rather than throwing: a truncated or half-written\n * transcript is normal for a session that is still running.\n */\nexport function cwdOfTranscript(file: string, maxBytes = 64 * 1024): string | null {\n  let head: string;\n  try {\n    const buf = readFileSync(file);\n    head = buf.subarray(0, maxBytes).toString(\"utf-8\");\n  } catch {\n    return null;\n  }\n\n  for (const line of head.split(\"\\n\")) {\n    if (!line.trim()) continue;\n    try {\n      const cwd = (JSON.parse(line) as { cwd?: string }).cwd;\n      if (cwd) return cwd;\n    } catch {\n      // A partial final line is expected when the head is cut mid-entry.\n      continue;\n    }\n  }\n  return null;\n}\n\nexport interface TranscriptFolder {\n  /** Folder name under ~/.claude/projects. */\n  name: string;\n  /** Number of transcripts, live plus archived. */\n  count: number;\n  /** Newest transcript mtime, for choosing between candidates. */\n  newest: number;\n  /**\n   * How many sampled transcripts in this folder began in the cwd being looked\n   * up, and how many began somewhere else.\n   *\n   * One folder routinely holds transcripts for more than one directory: a\n   * session started in a subdirectory can land in the parent's folder. So a\n   * project appearing in a folder is not evidence the folder is ITS folder.\n   * Measured 2026-08-02 — `apps/youdrill` held 5 transcripts for itself and 3\n   * for `apps/youdrill/app`, and reconnecting `app` there would have attached a\n   * subdirectory to its parent's history.\n   */\n  matching: number;\n  total: number;\n}\n\n/**\n * Map every working directory seen on disk to the folders that hold its\n * transcripts.\n *\n * A directory can legitimately map to more than one folder — Claude Code's\n * encoding is lossy, so two different paths can collide, and a project moved\n * and moved back leaves both. Callers pick; this reports.\n */\nexport function scanTranscriptFolders(\n  projectsDir = claudeProjectsDir(),\n  sampleFilesPerFolder = 8\n): Map<string, TranscriptFolder[]> {\n  const byCwd = new Map<string, TranscriptFolder[]>();\n  if (!existsSync(projectsDir)) return byCwd;\n\n  let entries: string[];\n  try {\n    entries = readdirSync(projectsDir);\n  } catch {\n    return byCwd;\n  }\n\n  for (const name of entries) {\n    const dir = join(projectsDir, name);\n    try {\n      if (!statSync(dir).isDirectory()) continue;\n    } catch {\n      continue;\n    }\n\n    const files = transcriptFiles(dir);\n    if (files.length === 0) continue;\n\n    // Newest first: a moved project's most recent transcripts carry its current\n    // path, and older ones may still carry the old one.\n    const mtimes = new Map<string, number>();\n    for (const f of files) {\n      try {\n        mtimes.set(f, statSync(f).mtimeMs);\n      } catch {\n        mtimes.set(f, 0);\n      }\n    }\n    const ordered = files.sort((a, b) => (mtimes.get(b) ?? 0) - (mtimes.get(a) ?? 0));\n    const newest = mtimes.get(ordered[0]) ?? 0;\n\n    // Count how many sampled transcripts began in each cwd, so a caller can\n    // tell \"this folder is that project's\" from \"that project appears in this\n    // folder\" — which are very different claims.\n    const tally = new Map<string, number>();\n    let sampled = 0;\n    for (const f of ordered.slice(0, sampleFilesPerFolder)) {\n      const cwd = cwdOfTranscript(f);\n      if (!cwd) continue;\n      sampled++;\n      tally.set(cwd, (tally.get(cwd) ?? 0) + 1);\n    }\n\n    for (const [cwd, matching] of tally) {\n      const record: TranscriptFolder = {\n        name,\n        count: files.length,\n        newest,\n        matching,\n        total: sampled,\n      };\n      const list = byCwd.get(cwd);\n      if (list) list.push(record);\n      else byCwd.set(cwd, [record]);\n    }\n  }\n\n  return byCwd;\n}\n\nexport interface MovedProject {\n  id: number;\n  slug: string;\n  rootPath: string;\n  /** What the registry currently claims. Null when it has never been set. */\n  storedDir: string | null;\n  /** The folder that actually holds this project's transcripts. */\n  correctDir: string;\n  transcripts: number;\n  /** Sessions the registry believes exist — how much is currently unreachable. */\n  sessions: number;\n}\n\nexport interface RegistryProjectRow {\n  id: number;\n  slug: string;\n  root_path: string;\n  encoded_dir: string | null;\n  sessions: number;\n}\n\n/**\n * Which projects point at the wrong transcript folder, and where they belong.\n *\n * A project is only reported when its stored folder yields nothing AND its root\n * path is recorded as a `cwd` somewhere else. Both halves matter: without the\n * first this would rewrite entries that work, and without the second it would\n * have nothing better to offer than the value already there.\n *\n * `resolvesNow` is supplied by the caller so this module does not have to\n * duplicate the resolver's fallback logic — a project the shipped resolver\n * already handles is not broken and must not be \"repaired\".\n */\nexport function findMovedProjects(\n  rows: RegistryProjectRow[],\n  byCwd: Map<string, TranscriptFolder[]>,\n  resolvesNow: (row: RegistryProjectRow) => boolean\n): MovedProject[] {\n  const out: MovedProject[] = [];\n\n  for (const row of rows) {\n    if (resolvesNow(row)) continue;\n\n    const candidates = byCwd.get(row.root_path);\n    if (!candidates || candidates.length === 0) continue;\n\n    // Only a folder this project DOMINATES. A folder where most transcripts\n    // began somewhere else belongs to that somewhere else, and attaching a\n    // subdirectory to its parent's folder would hand it a history that is\n    // mostly not its own — then the no-session-id fallback, which takes the\n    // newest transcript in the folder, would read a sibling's work as this\n    // project's. A missed repair costs nothing; a wrong one is silent and\n    // wrong in the direction of confidently reporting someone else's data.\n    const owned = candidates.filter((c) => c.matching * 2 > c.total);\n    if (owned.length === 0) continue;\n\n    // Most transcripts wins, newest breaks a tie: the folder with the most\n    // history is the one worth reconnecting to, and recency separates a live\n    // folder from an abandoned duplicate.\n    const best = [...owned].sort((a, b) => b.count - a.count || b.newest - a.newest)[0];\n\n    if (best.name === row.encoded_dir) continue;\n\n    out.push({\n      id: row.id,\n      slug: row.slug,\n      rootPath: row.root_path,\n      storedDir: row.encoded_dir,\n      correctDir: best.name,\n      transcripts: best.count,\n      sessions: row.sessions,\n    });\n  }\n\n  // Most sessions first: those are the projects where the breakage costs most.\n  return out.sort((a, b) => b.sessions - a.sessions || b.transcripts - a.transcripts);\n}\n","/** Shared database helpers for registry command operations. */\n\nimport type { Database } from \"better-sqlite3\";\nimport { now } from \"../../utils.js\";\nimport { basename, join } from \"node:path\";\nimport { transcriptFiles, claudeProjectsDir } from \"../../../registry/moved.js\";\n\n/**\n * Would writing this encoded_dir replace a working one with a broken one?\n *\n * `encodeDir` is lossy: `~` and emoji do not survive it, so a value derived\n * from a project's path can name a folder Claude Code never created. Every\n * caller here derives its value that way, and this function is the single\n * point they all pass through.\n *\n * `pai registry reconnect` repairs those by reading the cwd recorded inside the\n * transcripts. Without this check the next scan silently reverts the repair —\n * and it reverts it in the most confusing possible way, because a read taken\n * straight after the repair still shows it. Observed twice on 2026-08-02:\n * three projects reconnected, verified, and rediscovered as broken within the\n * hour, with the repair command reporting success both times.\n *\n * Permits the write when the new value resolves to transcripts, or when the\n * existing one resolves to nothing — i.e. whenever it cannot make things worse.\n */\nfunction worthWriting(db: Database, projectId: number, encodedDir: string): boolean {\n  if (transcriptFiles(join(claudeProjectsDir(), encodedDir)).length > 0) return true;\n\n  const row = db\n    .prepare(\"SELECT encoded_dir FROM projects WHERE id = ?\")\n    .get(projectId) as { encoded_dir: string | null } | undefined;\n\n  const current = row?.encoded_dir;\n  if (!current) return true;\n\n  return transcriptFiles(join(claudeProjectsDir(), current)).length === 0;\n}\n\n/**\n * Upsert a project row. Returns { id, isNew }.\n *\n * Matching priority:\n *  1. root_path  — most reliable; handles slug collisions\n *  2. encoded_dir — Claude project dirs are canonical\n *  3. Insert with suffix-deduplication on slug collision\n *\n * display_name is set to basename(rootPath) on INSERT so that the unified\n * listing always shows a human-readable name rather than the kebab-case slug.\n */\nexport function upsertProject(\n  db: Database,\n  slug: string,\n  rootPath: string,\n  encodedDir: string\n): { id: number; isNew: boolean } {\n  const ts = now();\n\n  const byPath = db\n    .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n    .get(rootPath) as { id: number } | undefined;\n\n  if (byPath) {\n    const encodedOwner = db\n      .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n      .get(encodedDir) as { id: number } | undefined;\n\n    if (\n      (!encodedOwner || encodedOwner.id === byPath.id) &&\n      worthWriting(db, byPath.id, encodedDir)\n    ) {\n      db.prepare(\n        \"UPDATE projects SET encoded_dir = ?, updated_at = ? WHERE id = ?\"\n      ).run(encodedDir, ts, byPath.id);\n    }\n    return { id: byPath.id, isNew: false };\n  }\n\n  const byEncoded = db\n    .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n    .get(encodedDir) as { id: number } | undefined;\n\n  if (byEncoded) {\n    const pathOwner = db\n      .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n      .get(rootPath) as { id: number } | undefined;\n\n    if (!pathOwner || pathOwner.id === byEncoded.id) {\n      db.prepare(\n        \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n      ).run(rootPath, ts, byEncoded.id);\n    }\n    return { id: byEncoded.id, isNew: false };\n  }\n\n  // Insert — deduplicate slug with numeric suffix if needed.\n  let finalSlug = slug;\n  let attempt = 0;\n  while (true) {\n    const conflict = db\n      .prepare(\"SELECT id FROM projects WHERE slug = ?\")\n      .get(finalSlug) as { id: number } | undefined;\n    if (!conflict) break;\n    attempt++;\n    finalSlug = `${slug}-${attempt}`;\n  }\n\n  // Use basename(rootPath) as the human display name. If rootPath is empty or\n  // just \"/\" fall back to the slug so we always have something non-empty.\n  const displayName = basename(rootPath) || finalSlug;\n\n  const result = db\n    .prepare(\n      `INSERT OR IGNORE INTO projects\n         (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n       VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`\n    )\n    .run(finalSlug, displayName, rootPath, encodedDir, ts, ts);\n\n  if (result.changes === 0) {\n    const fallback =\n      (db.prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\").get(encodedDir) as { id: number } | undefined) ??\n      (db.prepare(\"SELECT id FROM projects WHERE root_path = ?\").get(rootPath) as { id: number } | undefined);\n\n    if (fallback) {\n      return { id: fallback.id, isNew: false };\n    }\n\n    throw new Error(\n      `upsertProject: INSERT OR IGNORE was suppressed but no matching row found ` +\n      `for root_path=${rootPath} encoded_dir=${encodedDir}`\n    );\n  }\n\n  return { id: result.lastInsertRowid as number, isNew: true };\n}\n\n/** Upsert a session note. Returns true if newly inserted. */\nexport function upsertSession(\n  db: Database,\n  projectId: number,\n  number: number,\n  date: string,\n  slug: string,\n  title: string,\n  filename: string\n): boolean {\n  const existing = db\n    .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND number = ?\")\n    .get(projectId, number);\n\n  if (existing) return false;\n\n  const ts = now();\n  db.prepare(\n    `INSERT INTO sessions\n       (project_id, number, date, slug, title, filename, status, created_at)\n     VALUES (?, ?, ?, ?, ?, ?, 'completed', ?)`\n  ).run(projectId, number, date, slug, title, filename, ts);\n\n  return true;\n}\n","/** Registry scan command: walk ~/.claude/projects/ and populate the registry. */\n\nimport { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { realpathSync } from \"node:fs\";\nimport { join, basename, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { ok, warn, err, dim, bold } from \"../../utils.js\";\nimport { encodeDir } from \"../../utils.js\";\nimport { decodeEncodedDir, slugify, parseSessionFilename, buildEncodedDirMap } from \"../../../registry/migrate.js\";\nimport { ensurePaiMarker, discoverPaiMarkers } from \"../../../registry/pai-marker.js\";\nimport { transcriptFiles, claudeProjectsDir } from \"../../../registry/moved.js\";\nimport { upsertProject, upsertSession } from \"./utils.js\";\nimport type { Database } from \"better-sqlite3\";\n\n// ---------------------------------------------------------------------------\n// clc session.json fallback map\n// ---------------------------------------------------------------------------\n\n/**\n * Build a reverse map from encoded-dir → real path using the clc session\n * registry (~/.claude/session.json).\n *\n * The clc registry stores { sessions: [{ directory, ... }, ...] }. For each\n * entry with a resolvable directory, we encode the realpath and add it to\n * the map. This lets the scanner recover project paths that are not in\n * Claude's session-registry.json but were registered by clc.\n */\nfunction buildClcDirMap(): Map<string, string> {\n  const map = new Map<string, string>();\n  const sessionFile = join(homedir(), \".claude\", \"session.json\");\n  if (!existsSync(sessionFile)) return map;\n\n  try {\n    const raw = readFileSync(sessionFile, \"utf8\");\n    const parsed = JSON.parse(raw) as { sessions?: Array<{ directory?: string }> };\n    for (const entry of parsed.sessions ?? []) {\n      const dir = entry.directory;\n      if (!dir) continue;\n      try {\n        const real = realpathSync(dir);\n        if (!existsSync(real)) continue;\n        const encoded = encodeDir(real);\n        map.set(encoded, real);\n      } catch {\n        // Unresolvable path — skip\n      }\n    }\n  } catch {\n    // Unparseable — return empty map\n  }\n  return map;\n}\n\n// ---------------------------------------------------------------------------\n// Config helpers\n// ---------------------------------------------------------------------------\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\nconst PAI_CONFIG_DIR = join(homedir(), \".pai\");\nconst PAI_CONFIG_FILE = join(PAI_CONFIG_DIR, \"config.json\");\n\ninterface PaiConfig {\n  scan_dirs: string[];\n}\n\nexport function loadScanConfig(): PaiConfig {\n  if (!existsSync(PAI_CONFIG_FILE)) return { scan_dirs: [] };\n  try {\n    return JSON.parse(readFileSync(PAI_CONFIG_FILE, \"utf8\")) as PaiConfig;\n  } catch {\n    return { scan_dirs: [] };\n  }\n}\n\nexport function saveScanConfig(config: PaiConfig): void {\n  mkdirSync(PAI_CONFIG_DIR, { recursive: true });\n  writeFileSync(PAI_CONFIG_FILE, JSON.stringify(config, null, 2) + \"\\n\", \"utf8\");\n}\n\n/**\n * Resolve a path to its canonical form, falling back to the input.\n *\n * Every path that becomes a `projects.root_path` must go through this. That\n * column is UNIQUE, so two spellings of one directory create two projects and\n * split session history between them. The spellings are not hypothetical: a\n * configured scan_dir of `~/dev/ai` (where `~/dev` is a symlink) makes every\n * project under it register under the symlinked path, while a session started\n * from the resolved path registers a rival row.\n */\nexport function canonicalPath(p: string): string {\n  try {\n    return realpathSync(p);\n  } catch {\n    return p;\n  }\n}\n\nexport function resolveHome(p: string): string {\n  if (p.startsWith(\"~/\")) return join(homedir(), p.slice(2));\n  return resolve(p);\n}\n\n// ---------------------------------------------------------------------------\n// File discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively find all .md files in a directory, including YYYY/MM subdirectories.\n * Returns filenames (basename only).\n */\nexport function findNoteFiles(dir: string): string[] {\n  const results: string[] = [];\n  if (!existsSync(dir)) return results;\n\n  for (const entry of readdirSync(dir, { withFileTypes: true })) {\n    if (entry.isFile() && entry.name.endsWith(\".md\")) {\n      results.push(entry.name);\n    } else if (entry.isDirectory() && /^\\d{4}$/.test(entry.name)) {\n      const yearDir = join(dir, entry.name);\n      for (const monthEntry of readdirSync(yearDir, { withFileTypes: true })) {\n        if (monthEntry.isDirectory() && /^\\d{2}$/.test(monthEntry.name)) {\n          const monthDir = join(yearDir, monthEntry.name);\n          for (const noteEntry of readdirSync(monthDir, { withFileTypes: true })) {\n            if (noteEntry.isFile() && noteEntry.name.endsWith(\".md\")) {\n              results.push(noteEntry.name);\n            }\n          }\n        }\n      }\n    }\n  }\n  return results;\n}\n\n// ---------------------------------------------------------------------------\n// Scan result type\n// ---------------------------------------------------------------------------\n\nexport interface ScanResult {\n  projectsScanned: number;\n  projectsNew: number;\n  projectsUpdated: number;\n  sessionsScanned: number;\n  sessionsNew: number;\n  skipped: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Core scan logic\n// ---------------------------------------------------------------------------\n\nexport function performScan(db: Database): ScanResult {\n  const result: ScanResult = {\n    projectsScanned: 0,\n    projectsNew: 0,\n    projectsUpdated: 0,\n    sessionsScanned: 0,\n    sessionsNew: 0,\n    skipped: [],\n  };\n\n  if (!existsSync(CLAUDE_PROJECTS_DIR)) {\n    throw new Error(`Claude projects directory not found: ${CLAUDE_PROJECTS_DIR}`);\n  }\n\n  const entries = readdirSync(CLAUDE_PROJECTS_DIR).filter((name) => {\n    const full = join(CLAUDE_PROJECTS_DIR, name);\n    return statSync(full).isDirectory();\n  });\n\n  const lookupMap = buildEncodedDirMap();\n  const clcMap = buildClcDirMap();\n\n  for (const encodedDir of entries) {\n    let rootPath = decodeEncodedDir(encodedDir, lookupMap);\n\n    // If the decoded path doesn't exist, try the clc session.json fallback map.\n    // The clc map is keyed by encodeDir(realpathSync(directory)), so paths that\n    // were registered by clc but not in session-registry.json can be recovered.\n    if (!existsSync(rootPath) && clcMap.has(encodedDir)) {\n      const clcPath = clcMap.get(encodedDir)!;\n      if (existsSync(clcPath)) {\n        rootPath = clcPath;\n      }\n    }\n\n    if (!existsSync(rootPath)) {\n      result.skipped.push(`${encodedDir} (decoded: ${rootPath} — path not found on disk)`);\n      result.projectsScanned++;\n      continue;\n    }\n\n    // Canonicalize before upserting.\n    //\n    // ~/.claude/projects/ holds one encoded entry per path *spelling*, so a\n    // directory reachable through a symlinked prefix (~/dev -> Cloud/Development)\n    // appears twice. Decoding each to its literal path registers two projects\n    // for one directory, and session history then splits between them —\n    // whichever spelling the shell used gets the session.\n    //\n    // upsertProject looks up by root_path first, so resolving here makes the\n    // second spelling land on the row the first one created instead of\n    // creating a rival. Both encoded_dir values stay valid for locating\n    // central notes; only the project identity is unified.\n    rootPath = canonicalPath(rootPath);\n\n    const slug = slugify(basename(rootPath) || encodedDir);\n    const { id, isNew } = upsertProject(db, slug, rootPath, encodedDir);\n\n    result.projectsScanned++;\n    if (isNew) result.projectsNew++;\n    else result.projectsUpdated++;\n\n    try {\n      ensurePaiMarker(rootPath, slug);\n    } catch {\n      // Non-fatal\n    }\n\n    const claudeNotesDir = join(CLAUDE_PROJECTS_DIR, encodedDir, \"Notes\");\n\n    if (existsSync(claudeNotesDir)) {\n      const rootNotesDir = join(rootPath, \"Notes\");\n      if (claudeNotesDir !== rootNotesDir) {\n        db.prepare(\n          \"UPDATE projects SET claude_notes_dir = ?, updated_at = ? WHERE id = ?\"\n        ).run(claudeNotesDir, Date.now(), id);\n      }\n    }\n\n    if (!existsSync(claudeNotesDir)) continue;\n\n    const noteFiles = findNoteFiles(claudeNotesDir);\n\n    for (const filename of noteFiles) {\n      const parsed = parseSessionFilename(filename);\n      if (!parsed) continue;\n\n      result.sessionsScanned++;\n      const isNewSession = upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename);\n      if (isNewSession) result.sessionsNew++;\n    }\n  }\n\n  // Phase 2: Scan project-root Notes/ for all registered active projects\n  {\n    const activeProjects = db\n      .prepare(\"SELECT id, slug, root_path FROM projects WHERE status = 'active'\")\n      .all() as { id: number; slug: string; root_path: string }[];\n\n    for (const project of activeProjects) {\n      const notesDir = join(project.root_path, \"Notes\");\n      if (!existsSync(notesDir)) continue;\n\n      let files: string[];\n      try {\n        files = findNoteFiles(notesDir);\n      } catch {\n        continue;\n      }\n\n      for (const filename of files) {\n        const parsed = parseSessionFilename(filename);\n        if (!parsed) continue;\n\n        result.sessionsScanned++;\n        const isNewSession = upsertSession(db, project.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename);\n        if (isNewSession) result.sessionsNew++;\n      }\n    }\n  }\n\n  // Phase 3: Scan extra directories from config\n  const config = loadScanConfig();\n  if (config.scan_dirs.length) {\n    for (const rawDir of config.scan_dirs) {\n      const scanDir = resolveHome(rawDir);\n      if (!existsSync(scanDir)) {\n        result.skipped.push(`${rawDir} (configured scan_dir not found)`);\n        continue;\n      }\n\n      const children = readdirSync(scanDir).filter((name) => {\n        if (name.startsWith(\".\")) return false;\n        const full = join(scanDir, name);\n        try { return statSync(full).isDirectory(); } catch { return false; }\n      });\n\n      for (const child of children) {\n        // Canonicalize: a scan_dir of \"~/dev/ai\" (symlinked prefix) would\n        // otherwise register every project under the symlinked spelling and\n        // duplicate anything already registered under the resolved one.\n        const childPath = canonicalPath(join(scanDir, child));\n        const childSlug = slugify(child);\n        const childEncoded = encodeDir(childPath);\n\n        const existing = db\n          .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n          .get(childPath) as { id: number } | undefined;\n\n        if (existing) {\n          result.projectsScanned++;\n          result.projectsUpdated++;\n\n          try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n          const notesDir = join(childPath, \"Notes\");\n          if (existsSync(notesDir)) {\n            const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n            for (const filename of noteFiles) {\n              const parsed = parseSessionFilename(filename);\n              if (!parsed) continue;\n              result.sessionsScanned++;\n              if (upsertSession(db, existing.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n                result.sessionsNew++;\n              }\n            }\n          }\n          continue;\n        }\n\n        const { id, isNew } = upsertProject(db, childSlug, childPath, childEncoded);\n        result.projectsScanned++;\n        if (isNew) result.projectsNew++;\n        else result.projectsUpdated++;\n\n        try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n        const notesDir = join(childPath, \"Notes\");\n        if (existsSync(notesDir)) {\n          const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n          for (const filename of noteFiles) {\n            const parsed = parseSessionFilename(filename);\n            if (!parsed) continue;\n            result.sessionsScanned++;\n            if (upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n              result.sessionsNew++;\n            }\n          }\n        }\n      }\n    }\n  }\n\n  // Phase 4: Discover PAI.md markers in scan_dirs\n  if (config.scan_dirs.length) {\n    const resolvedScanDirs = config.scan_dirs.map(resolveHome).filter(existsSync);\n    const markers = discoverPaiMarkers(resolvedScanDirs);\n\n    for (const marker of markers) {\n      const registeredRow = db\n        .prepare(\"SELECT id, root_path, slug, encoded_dir FROM projects WHERE slug = ?\")\n        .get(marker.slug) as\n        | { id: number; root_path: string; slug: string; encoded_dir: string | null }\n        | undefined;\n\n      if (!registeredRow) continue;\n\n      // The marker was found by walking a scan_dir, so its projectRoot carries\n      // whatever spelling that dir used. Canonicalize before comparing —\n      // otherwise this \"repair\" step rewrites a correct root_path back to the\n      // symlinked form on every scan, undoing any deduplication.\n      const markerRoot = canonicalPath(marker.projectRoot);\n\n      if (registeredRow.root_path !== markerRoot) {\n        const newEncoded = encodeDir(markerRoot);\n        const now4 = Date.now();\n\n        const encodedOwner = db\n          .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n          .get(newEncoded) as { id: number } | undefined;\n        const pathOwner = db\n          .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n          .get(markerRoot) as { id: number } | undefined;\n\n        const encodedSafe = !encodedOwner || encodedOwner.id === registeredRow.id;\n        const pathSafe = !pathOwner || pathOwner.id === registeredRow.id;\n\n        // Never trade a working encoded_dir for one that resolves to nothing.\n        //\n        // encodeDir is lossy — `~` and emoji do not survive it — so re-deriving\n        // from the path can produce a folder name Claude Code never created.\n        // `pai registry reconnect` repairs exactly those by reading the cwd out\n        // of the transcripts, and this scan runs every 30 minutes from the\n        // daemon: without this check it silently reverts that repair, and the\n        // repair looks like it worked because a fresh read right afterwards\n        // still shows it. Observed on 2026-08-02 — three projects reconnected,\n        // reverted, and rediscovered as broken within the hour.\n        const derivedResolves = transcriptFiles(join(claudeProjectsDir(), newEncoded)).length > 0;\n        const currentResolves =\n          Boolean(registeredRow.encoded_dir) &&\n          transcriptFiles(join(claudeProjectsDir(), registeredRow.encoded_dir!)).length > 0;\n        const encodedWorthWriting = derivedResolves || !currentResolves;\n\n        if (encodedSafe && pathSafe && encodedWorthWriting) {\n          db.prepare(\n            \"UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?\"\n          ).run(markerRoot, newEncoded, now4, registeredRow.id);\n        } else if (pathSafe) {\n          db.prepare(\n            \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n          ).run(markerRoot, now4, registeredRow.id);\n        }\n      }\n    }\n  }\n\n  // Phase 5: Backfill display_name for rows where it still equals the slug.\n  // This converts legacy entries created before the basename-based naming was\n  // introduced: \"jobs-beta\" → \"Jobs Beta\" (from basename of root_path).\n  {\n    const stale = db\n      .prepare(\"SELECT id, slug, root_path FROM projects WHERE display_name = slug AND root_path IS NOT NULL AND root_path != ''\")\n      .all() as { id: number; slug: string; root_path: string }[];\n\n    for (const row of stale) {\n      const name = basename(row.root_path);\n      if (name && name !== row.slug) {\n        db.prepare(\"UPDATE projects SET display_name = ?, updated_at = ? WHERE id = ?\")\n          .run(name, Date.now(), row.id);\n      }\n    }\n  }\n\n  return result;\n}\n\n// ---------------------------------------------------------------------------\n// cmdScan\n// ---------------------------------------------------------------------------\n\n/**\n * Run the registry scan CLI command.\n *\n * @param opts.quick  When true, skips verbose output (same scan, less noise).\n *                    The underlying scan is always incremental via upsert —\n *                    this flag exists for hook/daemon-triggered invocations that\n *                    want minimal log output.\n */\nexport function cmdScan(db: Database, opts: { quick?: boolean } = {}): void {\n  const config = loadScanConfig();\n  if (!opts.quick) {\n    console.log(dim(\"Scanning ~/.claude/projects/ ...\"));\n    if (config.scan_dirs.length) {\n      console.log(dim(`Scanning ${config.scan_dirs.length} extra dir(s): ${config.scan_dirs.join(\", \")}`));\n    }\n    console.log(dim(\"Scanning project-root Notes/ directories ...\"));\n  }\n\n  let result: ScanResult;\n  try {\n    result = performScan(db);\n  } catch (e) {\n    console.error(err(String(e)));\n    process.exitCode = 1;\n    return;\n  }\n\n  if (!opts.quick) {\n    console.log(\n      ok(`Scanned ${bold(String(result.projectsScanned))} projects, ${bold(String(result.sessionsScanned))} session notes.`)\n    );\n    console.log(dim(`  Projects: ${result.projectsNew} new, ${result.projectsUpdated} updated`));\n    console.log(dim(`  Sessions: ${result.sessionsNew} new`));\n\n    if (result.skipped.length) {\n      console.log();\n      console.log(warn(`  ${result.skipped.length} project(s) skipped (path not found on disk):`));\n      for (const s of result.skipped.slice(0, 10)) {\n        console.log(dim(`    ${s}`));\n      }\n      if (result.skipped.length > 10) {\n        console.log(dim(`    ... and ${result.skipped.length - 10} more`));\n      }\n    }\n  } else {\n    // Quick mode: single compact line (stderr so it doesn't pollute pipe output)\n    process.stderr.write(\n      `[registry-scan] ${result.projectsScanned} projects, ${result.sessionsScanned} sessions ` +\n      `(${result.projectsNew}+${result.sessionsNew} new).\\n`\n    );\n  }\n}\n","/**\n * triple-extraction-prompt.ts — Prompt template for KG triple extraction.\n *\n * Used by the session-summary-worker to extract structured facts from\n * a completed session summary and store them in the temporal knowledge graph.\n */\n\nexport function buildTripleExtractionPrompt(params: {\n  sessionContent: string;\n  projectSlug: string;\n  gitLog: string;\n}): string {\n  return `Extract structured entities and relations from this coding session.\n\nOutput a single JSON object with two arrays: \"entities\" and \"relations\".\n\nEntity types: project | person | concept | tool | file | version | decision | technology | organization\n\nRules:\n- Be SPECIFIC: entity names must be concrete (e.g., \"FSRS\", \"Glidr\", \"the owner\")\n- Use snake_case relation verb phrases (e.g., \"uses_algorithm\", \"decided_to\", \"shipped_version\")\n- Skip opinions, speculation, and \"we should\" statements\n- Skip entities obvious from project metadata unless they have a meaningful relation\n- Maximum 15 relations per session — pick the most important\n- Each entity should have a brief description (1 sentence, what it is in this context)\n- Each relation must reference entity names that appear in the entities array\n\nExample output:\n{\n  \"entities\": [\n    {\"name\": \"Glidr\", \"type\": \"project\", \"description\": \"Flashcard app using FSRS spaced repetition\"},\n    {\"name\": \"FSRS\", \"type\": \"concept\", \"description\": \"Free Spaced Repetition Scheduler algorithm\"},\n    {\"name\": \"the owner\", \"type\": \"person\", \"description\": \"Developer of Glidr and Quassl\"},\n    {\"name\": \"Quassl\", \"type\": \"project\", \"description\": \"iOS app being rewritten in Flutter\"},\n    {\"name\": \"Flutter\", \"type\": \"technology\", \"description\": \"Cross-platform mobile framework\"}\n  ],\n  \"relations\": [\n    {\"source\": \"Glidr\", \"relation\": \"uses_algorithm\", \"target\": \"FSRS\"},\n    {\"source\": \"Glidr\", \"relation\": \"shipped_version\", \"target\": \"1.0.5\"},\n    {\"source\": \"the owner\", \"relation\": \"decided_to_rewrite\", \"target\": \"Quassl\"},\n    {\"source\": \"Quassl\", \"relation\": \"migrating_to\", \"target\": \"Flutter\"}\n  ]\n}\n\nPROJECT: ${params.projectSlug}\n\nSESSION CONTENT:\n${params.sessionContent}\n\nGIT COMMITS:\n${params.gitLog}\n\nJSON object (entities + relations):`;\n}\n","/**\n * kg-extraction.ts — Shared KG triple extraction logic.\n *\n * Extracted from session-summary-worker.ts so both the worker and the\n * CLI backfill (`pai kg backfill`) can use the same code path.\n *\n * Provides:\n *   - findClaudeBinary()       — locate the claude CLI\n *   - spawnClaude()            — generic prompt -> response runner (strips ANTHROPIC_API_KEY)\n *   - extractAndStoreTriples() — run the extractor prompt and persist triples to Postgres\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { Pool } from \"pg\";\nimport type { Database } from \"better-sqlite3\";\n\nimport { buildTripleExtractionPrompt } from \"../daemon/templates/triple-extraction-prompt.js\";\nimport { kgAdd, kgQuery, kgInvalidate } from \"./kg.js\";\nimport { upsertKgEntity } from \"./kg-entity.js\";\n\n// ---------------------------------------------------------------------------\n// Claude CLI binary discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `claude` CLI binary. Checks common installation locations first\n * (launchd PATH is minimal so bare \"claude\" often won't resolve).\n */\nexport function findClaudeBinary(): string | null {\n  const candidates = [\n    join(homedir(), \".local\", \"bin\", \"claude\"),\n    join(homedir(), \".claude\", \"local\", \"claude\"),\n    \"/usr/local/bin/claude\",\n    \"/opt/homebrew/bin/claude\",\n  ];\n\n  for (const candidate of candidates) {\n    try {\n      if (existsSync(candidate)) return candidate;\n    } catch { /* skip */ }\n  }\n  return \"claude\";\n}\n\nconst CLAUDE_TIMEOUT_MS: Record<string, number> = {\n  haiku: 60_000,\n  sonnet: 120_000,\n  opus: 300_000,\n};\n\n/**\n * Spawn the claude CLI with a prompt on stdin and return stdout.\n *\n * IMPORTANT: ANTHROPIC_API_KEY is stripped from the spawned environment so\n * the CLI uses the user's Max plan (free) instead of billing the API key.\n */\nexport async function spawnClaude(\n  prompt: string,\n  model: \"haiku\" | \"sonnet\" | \"opus\" = \"sonnet\"\n): Promise<string | null> {\n  const claudeBin = findClaudeBinary();\n  if (!claudeBin) {\n    process.stderr.write(\"[kg-extraction] claude CLI not found.\\n\");\n    return null;\n  }\n\n  const { spawn } = await import(\"node:child_process\");\n\n  return new Promise((resolve) => {\n    let timer: ReturnType<typeof setTimeout> | null = null;\n\n    const { ANTHROPIC_API_KEY: _drop, ...envWithoutApiKey } = process.env;\n    const child = spawn(\n      claudeBin,\n      [\"--model\", model, \"-p\", \"--no-session-persistence\"],\n      { env: envWithoutApiKey, stdio: [\"pipe\", \"pipe\", \"pipe\"] }\n    );\n\n    let stdout = \"\";\n    let stderr = \"\";\n\n    child.stdout.on(\"data\", (chunk: Buffer) => { stdout += chunk.toString(); });\n    child.stderr.on(\"data\", (chunk: Buffer) => { stderr += chunk.toString(); });\n\n    child.on(\"error\", (err: Error) => {\n      if (timer) { clearTimeout(timer); timer = null; }\n      process.stderr.write(`[kg-extraction] ${model} spawn error: ${err.message}\\n`);\n      resolve(null);\n    });\n\n    child.on(\"close\", (code: number | null) => {\n      if (timer) { clearTimeout(timer); timer = null; }\n      if (code !== 0) {\n        process.stderr.write(\n          `[kg-extraction] ${model} exited ${code}: ${stderr.slice(0, 300)}\\n`\n        );\n        resolve(null);\n      } else {\n        resolve(stdout.trim() || null);\n      }\n    });\n\n    timer = setTimeout(() => {\n      process.stderr.write(`[kg-extraction] ${model} timed out — killing process.\\n`);\n      child.kill(\"SIGTERM\");\n      resolve(null);\n    }, CLAUDE_TIMEOUT_MS[model] ?? 120_000);\n\n    child.stdin.write(prompt);\n    child.stdin.end();\n  });\n}\n\n// ---------------------------------------------------------------------------\n// Triple extraction\n// ---------------------------------------------------------------------------\n\nexport interface ExtractTriplesParams {\n  summaryText: string;\n  projectSlug: string;\n  projectId: number | null;\n  sessionId: string;\n  gitLog?: string;\n  model?: \"haiku\" | \"sonnet\" | \"opus\";\n  /** Optional federation SQLite db — when provided, entities are upserted into kg_entities (QW1) */\n  federationDb?: Database;\n  /** Tenant ID for multi-tenant entity scoping (default: \"default\") */\n  tenantId?: string;\n}\n\nexport interface ExtractTriplesResult {\n  extracted: number;\n  added: number;\n  superseded: number;\n}\n\n/**\n * Extract structured KG triples from a session summary and store them in\n * Postgres. Idempotent: if a (subject, predicate) pair already has the same\n * object, no new row is added; if the object differs, the old triple is\n * invalidated (valid_to = NOW()) and a new one is inserted.\n *\n * Best-effort: per-triple errors are caught and logged but never thrown.\n * Returns a small stats object so callers can report progress.\n */\nexport async function extractAndStoreTriples(\n  pool: Pool,\n  params: ExtractTriplesParams\n): Promise<ExtractTriplesResult> {\n  const stats: ExtractTriplesResult = { extracted: 0, added: 0, superseded: 0 };\n\n  const prompt = buildTripleExtractionPrompt({\n    sessionContent: params.summaryText,\n    projectSlug: params.projectSlug,\n    gitLog: params.gitLog ?? \"\",\n  });\n\n  const jsonOutput = await spawnClaude(prompt, params.model ?? \"sonnet\");\n  if (!jsonOutput) return stats;\n\n  // Strip markdown code fences if Claude wrapped the JSON\n  const cleaned = jsonOutput\n    .replace(/^```json\\s*/m, \"\")\n    .replace(/^```\\s*/m, \"\")\n    .replace(/\\s*```$/m, \"\")\n    .trim();\n\n  // Support both legacy array format and new structured format\n  type LegacyTriple = { subject: string; predicate: string; object: string };\n  type NewRelation = { source: string; relation: string; target: string };\n  type NewEntity = { name: string; type: string; description: string };\n  type NewFormat = { entities: NewEntity[]; relations: NewRelation[] };\n\n  let triples: Array<LegacyTriple>;\n  try {\n    const parsed = JSON.parse(cleaned);\n\n    if (Array.isArray(parsed)) {\n      // Legacy format: [{subject, predicate, object}]\n      triples = parsed;\n    } else if (parsed && typeof parsed === \"object\" && Array.isArray(parsed.relations)) {\n      // New structured format: {entities: [...], relations: [...]}\n      const newFmt = parsed as NewFormat;\n\n      // QW1: Upsert entities into federation SQLite kg_entities table when db is available\n      if (params.federationDb && Array.isArray(newFmt.entities)) {\n        const tenantId = params.tenantId ?? \"default\";\n        for (const entity of newFmt.entities) {\n          if (!entity.name) continue;\n          try {\n            upsertKgEntity(params.federationDb, {\n              name: entity.name,\n              type: entity.type ?? \"unknown\",\n              description: entity.description,\n              tenantId,\n            });\n          } catch (entityErr) {\n            process.stderr.write(`[kg-extraction] entity upsert error (${entity.name}): ${entityErr}\\n`);\n          }\n        }\n      }\n\n      triples = newFmt.relations.map((r: NewRelation) => ({\n        subject: r.source,\n        predicate: r.relation,\n        object: r.target,\n      }));\n    } else {\n      process.stderr.write(`[kg-extraction] Unexpected JSON shape — neither array nor {entities,relations}\\n`);\n      return stats;\n    }\n  } catch (e) {\n    process.stderr.write(`[kg-extraction] JSON parse failed: ${e}\\n`);\n    return stats;\n  }\n\n  if (!Array.isArray(triples)) return stats;\n  stats.extracted = triples.length;\n\n  for (const t of triples) {\n    if (!t.subject || !t.predicate || !t.object) continue;\n\n    try {\n      const existing = await kgQuery(pool, {\n        subject: t.subject,\n        predicate: t.predicate,\n        project_id: params.projectId ?? undefined,\n      });\n\n      // If an identical (subject, predicate, object) is already valid, skip — idempotent\n      const alreadyValid = existing.find((e) => e.object === t.object && !e.valid_to);\n      if (alreadyValid) continue;\n\n      // Invalidate any superseded triple (same subject+predicate, different object)\n      const supersedes = existing.find((e) => e.object !== t.object && !e.valid_to);\n      if (supersedes) {\n        await kgInvalidate(pool, supersedes.id);\n        stats.superseded++;\n      }\n\n      await kgAdd(pool, {\n        subject: t.subject,\n        predicate: t.predicate,\n        object: t.object,\n        project_id: params.projectId ?? undefined,\n        source_session: params.sessionId,\n        confidence: \"EXTRACTED\",\n      });\n      stats.added++;\n    } catch (tripleErr) {\n      process.stderr.write(`[kg-extraction] store error (${t.subject}): ${tripleErr}\\n`);\n    }\n  }\n\n  return stats;\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 — 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 — 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 — 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 — 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 — 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 — 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) — 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 `(?!#)` — `###` 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 — 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 — 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 — 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 — 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 — 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 — 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 — 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 — it fires from the session-stop and pre-compact\n * hooks — 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* — via `session\n *      slug --apply` and `session cleanup --execute` — 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 — TODO.md would\n *      otherwise keep pointing at the wrong session — 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 — quite\n *      possibly as a hand-rolled workaround for the very clobbering this fixes —\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 * — 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 — 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 — 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 ? ` — 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 — 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 — 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 — 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 — 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 — `Working directory: …`, 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 — 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 — 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 — 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 —\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 — 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 — 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/** PAI_DIR — mirrors pai-paths.ts resolution. */\nfunction getPaiDir(): string {\n  const envDir = 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 — 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 — ${ts}`;\n\n  let existing: string;\n  try {\n    existing = readFileSync(notePath, \"utf8\");\n  } catch (err) {\n    return { appended: false, error: String(err) };\n  }\n\n  if (existing.includes(heading)) return { appended: false };\n\n  const block = `\\n\\n---\\n\\n${heading}\\n\\n${body.trim()}\\n`;\n  const tmpPath = `${notePath}.checkpoint.tmp`;\n\n  try {\n    writeFileSync(tmpPath, existing.trimEnd() + block, \"utf8\");\n    renameSync(tmpPath, notePath);\n  } catch (err) {\n    try {\n      if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);\n    } catch {\n      /* ignore */\n    }\n    return { appended: false, error: String(err) };\n  }\n\n  return { appended: true };\n}\n\n// ---------------------------------------------------------------------------\n// Body loading\n// ---------------------------------------------------------------------------\n\n/** Read a checkpoint body from a file, or from stdin when path is \"-\". */\nexport function readBodyFile(path: string): string {\n  if (path === \"-\") {\n    return readFileSync(0, \"utf8\");\n  }\n  return readFileSync(path, \"utf8\");\n}\n"],"mappings":"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"}