@tekmidian/pai 0.36.1 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{auto-route-Byf8ENXj.mjs → auto-route-DM7GhJ8y.mjs} +2 -2
- package/dist/{auto-route-Byf8ENXj.mjs.map → auto-route-DM7GhJ8y.mjs.map} +1 -1
- package/dist/cli/index.mjs +5 -4
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.d.mts.map +1 -1
- package/dist/cli/program.mjs +5 -4
- package/dist/{clusters-wZgTCYCB.mjs → clusters-Do4tEGyc.mjs} +1 -1
- package/dist/{clusters-wZgTCYCB.mjs.map → clusters-Do4tEGyc.mjs.map} +1 -1
- package/dist/{context-handover-cache-PtNvj_8D.mjs → context-handover-cache-BpUojjsi.mjs} +2 -2
- package/dist/{context-handover-cache-PtNvj_8D.mjs.map → context-handover-cache-BpUojjsi.mjs.map} +1 -1
- package/dist/daemon/index.mjs +8 -8
- package/dist/{daemon-BZ93KRBo.mjs → daemon-CGg1VCbA.mjs} +25 -25
- package/dist/{daemon-BZ93KRBo.mjs.map → daemon-CGg1VCbA.mjs.map} +1 -1
- package/dist/daemon-DsGGiIJM.mjs +20 -0
- package/dist/daemon-mcp/index.mjs +428 -1
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{detector--Gg5JRN5.mjs → detector-CMap-9vw.mjs} +1 -1
- package/dist/{detector--Gg5JRN5.mjs.map → detector-CMap-9vw.mjs.map} +1 -1
- package/dist/detector-DtLExmHN.mjs +5 -0
- package/dist/factory-BXzqRYVZ.mjs +3 -0
- package/dist/{factory-Bsp7xOpO.mjs → factory-DD2T33C9.mjs} +5 -5
- package/dist/{factory-Bsp7xOpO.mjs.map → factory-DD2T33C9.mjs.map} +1 -1
- package/dist/hooks/context-compression-hook.mjs +164 -14
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/initialize-session.mjs +6 -0
- package/dist/hooks/initialize-session.mjs.map +3 -3
- package/dist/hooks/load-core-context.mjs +6 -0
- package/dist/hooks/load-core-context.mjs.map +3 -3
- package/dist/hooks/load-project-context.mjs +6 -0
- package/dist/hooks/load-project-context.mjs.map +3 -3
- package/dist/hooks/post-compact-inject.mjs +48 -7
- package/dist/hooks/post-compact-inject.mjs.map +4 -4
- package/dist/hooks/route-agents-to-worker.mjs +456 -0
- package/dist/hooks/route-agents-to-worker.mjs.map +7 -0
- package/dist/hooks/status-line.mjs +400 -0
- package/dist/hooks/status-line.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs +6 -0
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/hooks/subagent-stop-hook.mjs +6 -0
- package/dist/hooks/subagent-stop-hook.mjs.map +3 -3
- package/dist/hooks/sync-todo-to-md.mjs +6 -0
- package/dist/hooks/sync-todo-to-md.mjs.map +3 -3
- package/dist/hooks/update-tab-on-action.mjs +6 -0
- package/dist/hooks/update-tab-on-action.mjs.map +3 -3
- package/dist/hooks/update-tab-titles.mjs +6 -0
- package/dist/hooks/update-tab-titles.mjs.map +3 -3
- package/dist/hooks/worker-proxy.mjs +796 -0
- package/dist/hooks/worker-proxy.mjs.map +7 -0
- package/dist/hooks/worker-status-line.mjs +483 -0
- package/dist/hooks/worker-status-line.mjs.map +7 -0
- package/dist/{indexer-backend-nQZuEx6N.mjs → indexer-backend-Cox9BCo-.mjs} +1 -1
- package/dist/{indexer-backend-nQZuEx6N.mjs.map → indexer-backend-Cox9BCo-.mjs.map} +1 -1
- package/dist/{kg-entity-DbOMPdF9.mjs → kg-entity-r8duqhi9.mjs} +1 -1
- package/dist/{kg-entity-DbOMPdF9.mjs.map → kg-entity-r8duqhi9.mjs.map} +1 -1
- package/dist/{latent-ideas-Bn6A5-5P.mjs → latent-ideas-BC1oINZ-.mjs} +2 -2
- package/dist/{latent-ideas-Bn6A5-5P.mjs.map → latent-ideas-BC1oINZ-.mjs.map} +1 -1
- package/dist/{link-boost-fYjUnxCN.mjs → link-boost-QFLrJwD6.mjs} +1 -1
- package/dist/{link-boost-fYjUnxCN.mjs.map → link-boost-QFLrJwD6.mjs.map} +1 -1
- package/dist/{main-resolver-BAbhKpeX.mjs → main-resolver-DlaLOFBA.mjs} +1 -1
- package/dist/{main-resolver-BAbhKpeX.mjs.map → main-resolver-DlaLOFBA.mjs.map} +1 -1
- package/dist/{main-resolver-Dxh444GO.mjs → main-resolver-IhZo4pI0.mjs} +1 -1
- package/dist/{neighborhood-DpaEM991.mjs → neighborhood-D9MJ1c8f.mjs} +1 -1
- package/dist/{neighborhood-DpaEM991.mjs.map → neighborhood-D9MJ1c8f.mjs.map} +1 -1
- package/dist/{note-context-DrcY4cWm.mjs → note-context-d1wT_-GA.mjs} +1 -1
- package/dist/{note-context-DrcY4cWm.mjs.map → note-context-d1wT_-GA.mjs.map} +1 -1
- package/dist/{postgres-BVme6qX0.mjs → postgres--BjPtLa0.mjs} +1 -1
- package/dist/{postgres-BVme6qX0.mjs.map → postgres--BjPtLa0.mjs.map} +1 -1
- package/dist/{program-C-fUghPv.mjs → program-CEIHn_Ma.mjs} +693 -101
- package/dist/program-CEIHn_Ma.mjs.map +1 -0
- package/dist/providers-sXcK5bDZ.mjs +3067 -0
- package/dist/providers-sXcK5bDZ.mjs.map +1 -0
- package/dist/{query-feedback-C1T6kS18.mjs → query-feedback-BUJxgw5B.mjs} +1 -1
- package/dist/{query-feedback-C1T6kS18.mjs.map → query-feedback-BUJxgw5B.mjs.map} +1 -1
- package/dist/query-feedback-DhyLOe5S.mjs +3 -0
- package/dist/router-1zi8jiNF.mjs +3 -0
- package/dist/{router-CsDm7HvK.mjs → router-DK_sLsUL.mjs} +1 -1
- package/dist/{router-CsDm7HvK.mjs.map → router-DK_sLsUL.mjs.map} +1 -1
- package/dist/skills/Worker/SKILL.md +41 -0
- package/dist/{sources-D8ZdNfvK.mjs → sources-Bi7--33T.mjs} +1 -1
- package/dist/{sources-D8ZdNfvK.mjs.map → sources-Bi7--33T.mjs.map} +1 -1
- package/dist/{sqlite-D1IaR8Am.mjs → sqlite-DtaL1glm.mjs} +1 -1
- package/dist/{sqlite-D1IaR8Am.mjs.map → sqlite-DtaL1glm.mjs.map} +1 -1
- package/dist/{state-qtmrBWCm.mjs → state-8Hm9E4tW.mjs} +1 -1
- package/dist/{state-WaXhLr6R.mjs → state-BY2L6-vX.mjs} +1 -1
- package/dist/{state-WaXhLr6R.mjs.map → state-BY2L6-vX.mjs.map} +1 -1
- package/dist/{themes-XPkj_bfP.mjs → themes-BN0a2duq.mjs} +1 -1
- package/dist/{themes-XPkj_bfP.mjs.map → themes-BN0a2duq.mjs.map} +1 -1
- package/dist/{tools-DEt6YPfc.mjs → tools-CGPqpU3A.mjs} +1 -1
- package/dist/{tools-ceiy7ANX.mjs → tools-y2bJpKom.mjs} +14 -14
- package/dist/{tools-ceiy7ANX.mjs.map → tools-y2bJpKom.mjs.map} +1 -1
- package/dist/{trace-DfyGmMG_.mjs → trace-h23JCcFD.mjs} +1 -1
- package/dist/{trace-DfyGmMG_.mjs.map → trace-h23JCcFD.mjs.map} +1 -1
- package/dist/{vault-indexer-CFvlPUMB.mjs → vault-indexer-DgsPjMgs.mjs} +1 -1
- package/dist/{vault-indexer-CFvlPUMB.mjs.map → vault-indexer-DgsPjMgs.mjs.map} +1 -1
- package/dist/{work-queue-worker-B8W8_3Rn.mjs → work-queue-worker-Dva_v_pI.mjs} +4 -4
- package/dist/{work-queue-worker-B8W8_3Rn.mjs.map → work-queue-worker-Dva_v_pI.mjs.map} +1 -1
- package/dist/{work-queue-worker-HN2Ufg-L.mjs → work-queue-worker-gsKd2LJa.mjs} +4 -4
- package/dist/{zettelkasten-CvjmMghT.mjs → zettelkasten-vo7psPdT.mjs} +3 -3
- package/dist/{zettelkasten-CvjmMghT.mjs.map → zettelkasten-vo7psPdT.mjs.map} +1 -1
- package/docs/commands/README.md +28 -0
- package/docs/commands/backup.md +1 -1
- package/docs/commands/clear-names.md +1 -1
- package/docs/commands/daemon.md +1 -1
- package/docs/commands/db.md +1 -1
- package/docs/commands/end.md +1 -1
- package/docs/commands/help.md +1 -1
- package/docs/commands/identity.md +1 -1
- package/docs/commands/kg.md +1 -1
- package/docs/commands/mcp.md +1 -1
- package/docs/commands/memory.md +1 -1
- package/docs/commands/notify.md +1 -1
- package/docs/commands/observation.md +1 -1
- package/docs/commands/obsidian.md +1 -1
- package/docs/commands/pause.md +1 -1
- package/docs/commands/project.md +1 -1
- package/docs/commands/projects.md +1 -1
- package/docs/commands/registry.md +1 -1
- package/docs/commands/restore.md +1 -1
- package/docs/commands/session.md +1 -1
- package/docs/commands/sessions.md +1 -1
- package/docs/commands/setup.md +1 -1
- package/docs/commands/shell-init.md +1 -1
- package/docs/commands/skill.md +1 -1
- package/docs/commands/task.md +1 -1
- package/docs/commands/topic.md +1 -1
- package/docs/commands/update.md +1 -1
- package/docs/commands/worker.md +361 -0
- package/docs/commands/zettel.md +1 -1
- package/docs/worker.md +303 -0
- package/package.json +1 -1
- package/scripts/build-hooks.mjs +46 -1
- package/src/hooks/pre-compact.sh +1 -0
- package/src/hooks/session-autosave.sh +1 -0
- package/src/hooks/session-stop.sh +1 -0
- package/src/hooks/ts/lib/context-fill.test.ts +104 -0
- package/src/hooks/ts/lib/context-fill.ts +73 -0
- package/src/hooks/ts/lib/handover-evidence.ts +227 -0
- package/src/hooks/ts/lib/worker-session.test.ts +24 -0
- package/src/hooks/ts/lib/worker-session.ts +23 -0
- package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +2 -0
- package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +2 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +17 -0
- package/src/hooks/ts/pre-tool-use/route-agents-to-worker.ts +128 -0
- package/src/hooks/ts/session-start/initialize-session.ts +2 -0
- package/src/hooks/ts/session-start/load-core-context.ts +2 -0
- package/src/hooks/ts/session-start/load-project-context.ts +2 -0
- package/src/hooks/ts/session-start/post-compact-inject.ts +13 -0
- package/src/hooks/ts/stop/stop-hook.ts +2 -0
- package/src/hooks/ts/subagent-stop/subagent-stop-hook.ts +2 -0
- package/src/hooks/ts/user-prompt/update-tab-titles.ts +2 -0
- package/statusline-command.sh +17 -0
- package/dist/daemon-DRdoA489.mjs +0 -20
- package/dist/detector-DGAk1iBR.mjs +0 -5
- package/dist/factory-CrokPMk2.mjs +0 -3
- package/dist/program-C-fUghPv.mjs.map +0 -1
- package/dist/query-feedback-BBMBp96K.mjs +0 -3
- package/dist/router-BMkOb62X.mjs +0 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../../src/hooks/ts/pre-compact/context-compression-hook.ts", "../../../src/hooks/ts/lib/project-utils/paths.ts", "../../../src/hooks/ts/lib/pai-paths.ts", "../../../src/hooks/ts/lib/project-utils/notify.ts", "../../../src/hooks/ts/lib/project-utils/session-notes.ts", "../../../src/hooks/ts/lib/project-utils/todo.ts", "../../../src/session/checkpoint-block.ts", "../../../src/hooks/ts/lib/context-fill.ts", "../../../src/hooks/ts/lib/transcript-text.ts", "../../../src/hooks/ts/lib/context-handover-cache.ts"],
|
|
4
|
-
"sourcesContent": ["#!/usr/bin/env node\n/**\n * PreCompact Hook - Triggered before context compression\n *\n * Three critical jobs:\n * 1. Save rich checkpoint to session note \u2014 work items, state, meaningful rename\n * 2. Update TODO.md with a proper ## Continue section for the next session\n * 3. Save session state to temp file for post-compact injection via SessionStart(compact)\n *\n * Uses a CUMULATIVE state file (.compact-state.json) that persists across\n * compactions. This ensures that even after multiple compactions (where the\n * transcript becomes thin), we still have rich data for titles, summaries,\n * and work items from earlier in the session.\n */\n\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { basename, dirname, join } from 'path';\nimport { tmpdir } from 'os';\nimport { connect } from 'net';\nimport { randomUUID } from 'crypto';\nimport {\n sendNtfyNotification,\n getCurrentNotePath,\n createSessionNote,\n appendCheckpoint,\n addWorkToSessionNote,\n isMeaningfulTitle,\n findNotesDir,\n renameSessionNote,\n updateTodoContinue,\n isProbeSession,\n WorkItem,\n} from '../lib/project-utils';\nimport { getContextFill, formatContextFill } from '../lib/context-fill.js';\nimport { contentToText, isNoiseFilePath, preferCwdFiles } from '../lib/transcript-text.js';\nimport { readContextHandoverCache } from '../lib/context-handover-cache.js';\n\ninterface HookInput {\n session_id: string;\n transcript_path: string;\n cwd?: string;\n hook_event_name: string;\n compact_type?: string;\n trigger?: string;\n /** Session's scratchpad directory, when the harness provides one \u2014 used to\n * exclude scratch files from the \"Files modified\" list (see\n * isNoiseFilePath). */\n scratchpad_dir?: string;\n}\n\nconst DAEMON_SOCKET = process.env.PAI_SOCKET ?? '/tmp/pai.sock';\nconst DAEMON_TIMEOUT_MS = 3_000;\n\n/** Structured data extracted from a transcript in a single pass. */\ninterface TranscriptData {\n userMessages: string[];\n summaries: string[];\n captures: string[];\n lastCompleted: string;\n filesModified: string[];\n /** Count of modified-file paths dropped as noise (tmp/jobs/scratchpad). */\n filesExcluded: number;\n workItems: WorkItem[];\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n//\n// contentToText, isNoiseFilePath, and preferCwdFiles live in\n// ../lib/transcript-text.ts, not here \u2014 this file calls `main()` and\n// `process.exit()` at import time, which makes it unsafe to import from a\n// test. Splitting the pure, bug-fixed logic out is what lets those two bugs\n// (the \"[object Object]\" content bug and the tmp/jobs noise-crowding bug)\n// carry a real regression test instead of only a manual before/after digest.\n\nfunction getTranscriptStats(transcriptPath: string): { messageCount: number; isLarge: boolean } {\n try {\n const content = readFileSync(transcriptPath, 'utf-8');\n const lines = content.trim().split('\\n');\n let userMessages = 0;\n let assistantMessages = 0;\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const entry = JSON.parse(line);\n if (entry.type === 'user') userMessages++;\n else if (entry.type === 'assistant') assistantMessages++;\n } catch { /* skip */ }\n }\n const totalMessages = userMessages + assistantMessages;\n return { messageCount: totalMessages, isLarge: totalMessages > 50 };\n } catch {\n return { messageCount: 0, isLarge: false };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Unified transcript parser \u2014 single pass extracts everything\n// ---------------------------------------------------------------------------\n\nfunction parseTranscript(transcriptPath: string, scratchpadDir?: string): TranscriptData {\n const data: TranscriptData = {\n userMessages: [],\n summaries: [],\n captures: [],\n lastCompleted: '',\n filesModified: [],\n filesExcluded: 0,\n workItems: [],\n };\n\n try {\n const raw = readFileSync(transcriptPath, 'utf-8');\n const lines = raw.trim().split('\\n');\n const seenSummaries = new Set<string>();\n\n for (const line of lines) {\n if (!line.trim()) continue;\n let entry: any;\n try { entry = JSON.parse(line); } catch { continue; }\n\n // --- User messages ---\n if (entry.type === 'user' && entry.message?.content) {\n const text = contentToText(entry.message.content).slice(0, 300);\n if (text) data.userMessages.push(text);\n }\n\n // --- Assistant content ---\n if (entry.type === 'assistant' && entry.message?.content) {\n const text = contentToText(entry.message.content);\n\n // Summaries \u2192 also create work items\n const summaryMatch = text.match(/SUMMARY:\\s*(.+?)(?:\\n|$)/i);\n if (summaryMatch) {\n const s = summaryMatch[1].trim();\n if (s.length > 5 && !data.summaries.includes(s)) {\n data.summaries.push(s);\n if (!seenSummaries.has(s)) {\n seenSummaries.add(s);\n const details: string[] = [];\n const actionsMatch = text.match(/ACTIONS:\\s*(.+?)(?=\\n[A-Z]+:|$)/is);\n if (actionsMatch) {\n const actionLines = actionsMatch[1].split('\\n')\n .map(l => l.replace(/^[-*\u2022]\\s*/, '').replace(/^\\d+\\.\\s*/, '').trim())\n .filter(l => l.length > 3 && l.length < 100);\n details.push(...actionLines.slice(0, 3));\n }\n data.workItems.push({ title: s, details: details.length > 0 ? details : undefined, completed: true });\n }\n }\n }\n\n // Captures\n const captureMatch = text.match(/CAPTURE:\\s*(.+?)(?:\\n|$)/i);\n if (captureMatch) {\n const c = captureMatch[1].trim();\n if (c.length > 5 && !data.captures.includes(c)) data.captures.push(c);\n }\n\n // Completed\n const completedMatch = text.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n if (completedMatch) {\n data.lastCompleted = completedMatch[1].trim().replace(/\\*+/g, '');\n if (data.workItems.length === 0 && !seenSummaries.has(data.lastCompleted) && data.lastCompleted.length > 5) {\n seenSummaries.add(data.lastCompleted);\n data.workItems.push({ title: data.lastCompleted, completed: true });\n }\n }\n\n // File modifications (from tool_use blocks)\n if (Array.isArray(entry.message.content)) {\n for (const block of entry.message.content) {\n if (block.type === 'tool_use') {\n const tool = block.name;\n if ((tool === 'Edit' || tool === 'Write') && block.input?.file_path) {\n const fp = block.input.file_path;\n if (isNoiseFilePath(fp, scratchpadDir)) {\n data.filesExcluded++;\n } else if (!data.filesModified.includes(fp)) {\n data.filesModified.push(fp);\n }\n }\n }\n }\n }\n }\n }\n } catch (err) {\n console.error(`parseTranscript error: ${err}`);\n }\n\n return data;\n}\n\n// ---------------------------------------------------------------------------\n// Format session state as human-readable string\n// ---------------------------------------------------------------------------\n\nfunction formatSessionState(data: TranscriptData, cwd?: string): string | null {\n const parts: string[] = [];\n\n if (cwd) parts.push(`Working directory: ${cwd}`);\n\n const recentUser = data.userMessages.slice(-3);\n if (recentUser.length > 0) {\n parts.push('\\nRecent user requests:');\n for (const msg of recentUser) {\n parts.push(`- ${msg.split('\\n')[0].slice(0, 200)}`);\n }\n }\n\n const recentSummaries = data.summaries.slice(-3);\n if (recentSummaries.length > 0) {\n parts.push('\\nWork summaries:');\n for (const s of recentSummaries) parts.push(`- ${s.slice(0, 150)}`);\n }\n\n const recentCaptures = data.captures.slice(-5);\n if (recentCaptures.length > 0) {\n parts.push('\\nCaptured context:');\n for (const c of recentCaptures) parts.push(`- ${c.slice(0, 150)}`);\n }\n\n // Noise (tmp/jobs/scratchpad) is already excluded at collection time \u2014 see\n // isNoiseFilePath. Here, prefer files inside the session's own working\n // directory when there are more than fit in the slice, so an unrelated\n // repo touched in passing doesn't crowd out the files that matter.\n const files = preferCwdFiles(data.filesModified, cwd).slice(-10);\n if (files.length > 0) {\n parts.push('\\nFiles modified this session:');\n for (const f of files) parts.push(`- ${f}`);\n } else if (data.filesExcluded > 0) {\n parts.push(\n `\\nFiles modified this session: none in the working directory ` +\n `(excluded ${data.filesExcluded} noise path(s) under tmp/jobs/scratchpad)`\n );\n }\n\n if (data.lastCompleted) {\n parts.push(`\\nLast completed: ${data.lastCompleted.slice(0, 150)}`);\n }\n\n const result = parts.join('\\n');\n return result.length > 50 ? result : null;\n}\n\n// ---------------------------------------------------------------------------\n// Derive a meaningful title for the session note\n// ---------------------------------------------------------------------------\n\nfunction deriveTitle(data: TranscriptData): string {\n // Collect candidates in priority order, then pick the first meaningful one.\n const candidates: string[] = [];\n\n // 1. Work item titles (most descriptive of what was accomplished)\n for (let i = data.workItems.length - 1; i >= 0; i--) {\n candidates.push(data.workItems[i].title);\n }\n\n // 2. Summaries\n for (let i = data.summaries.length - 1; i >= 0; i--) {\n candidates.push(data.summaries[i]);\n }\n\n // 3. Last completed marker\n if (data.lastCompleted && data.lastCompleted.length > 5) {\n candidates.push(data.lastCompleted);\n }\n\n // 4. User messages (FIRST meaningful one, not last \u2014 first is more likely\n // to describe the session's purpose; last is often system noise)\n for (const msg of data.userMessages) {\n const line = msg.split('\\n')[0].trim();\n if (line.length > 10 && line.length < 80 &&\n !line.toLowerCase().startsWith('yes') &&\n !line.toLowerCase().startsWith('ok')) {\n candidates.push(line);\n }\n }\n\n // 5. Derive from files modified (fallback)\n if (data.filesModified.length > 0) {\n const basenames = data.filesModified.slice(-5).map(f => {\n const b = basename(f);\n return b.replace(/\\.[^.]+$/, '');\n });\n const unique = [...new Set(basenames)];\n candidates.push(\n unique.length <= 3\n ? `Updated ${unique.join(', ')}`\n : `Modified ${data.filesModified.length} files`\n );\n }\n\n // Pick the first candidate that passes the meaningfulness filter\n for (const raw of candidates) {\n const cleaned = raw\n .replace(/[^\\w\\s-]/g, ' ') // Remove special chars\n .replace(/\\s+/g, ' ') // Normalize whitespace\n .trim()\n .substring(0, 60);\n if (cleaned.length >= 5 && isMeaningfulTitle(cleaned)) {\n return cleaned;\n }\n }\n\n // All candidates were garbage \u2014 return empty (caller will not rename)\n return '';\n}\n\n// ---------------------------------------------------------------------------\n// Cumulative state \u2014 persists across compactions in .compact-state.json\n// ---------------------------------------------------------------------------\n\nconst CUMULATIVE_STATE_FILE = '.compact-state.json';\n\nfunction loadCumulativeState(notesDir: string): TranscriptData | null {\n try {\n const filePath = join(notesDir, CUMULATIVE_STATE_FILE);\n if (!existsSync(filePath)) return null;\n const raw = JSON.parse(readFileSync(filePath, 'utf-8'));\n return {\n userMessages: raw.userMessages || [],\n summaries: raw.summaries || [],\n captures: raw.captures || [],\n lastCompleted: raw.lastCompleted || '',\n filesModified: raw.filesModified || [],\n filesExcluded: raw.filesExcluded || 0,\n workItems: raw.workItems || [],\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Peek at `.compact-state.json`'s `lastUpdated` \u2014 the timestamp of THIS\n * session's previous compaction \u2014 without pulling it into TranscriptData's\n * contract. Read before `saveCumulativeState` overwrites the file for the\n * current compaction, so callers can tell \"a cached handover generated\n * since the last compaction\" from \"a cached handover left over from before\n * it, now stale\". Returns null when there is no previous compaction (the\n * first one this session) \u2014 treated as \"any cached handover is fresh\".\n */\nfunction readPreviousCompactionTimestamp(notesDir: string): string | null {\n try {\n const filePath = join(notesDir, CUMULATIVE_STATE_FILE);\n if (!existsSync(filePath)) return null;\n const raw = JSON.parse(readFileSync(filePath, 'utf-8'));\n return typeof raw.lastUpdated === 'string' ? raw.lastUpdated : null;\n } catch {\n return null;\n }\n}\n\nfunction mergeTranscriptData(accumulated: TranscriptData | null, current: TranscriptData): TranscriptData {\n if (!accumulated) return current;\n\n const mergeArrays = (a: string[], b: string[]): string[] => {\n const seen = new Set(a);\n return [...a, ...b.filter(x => !seen.has(x))];\n };\n\n const seenTitles = new Set(accumulated.workItems.map(w => w.title));\n const newWorkItems = current.workItems.filter(w => !seenTitles.has(w.title));\n\n return {\n userMessages: mergeArrays(accumulated.userMessages, current.userMessages).slice(-20),\n summaries: mergeArrays(accumulated.summaries, current.summaries),\n captures: mergeArrays(accumulated.captures, current.captures),\n lastCompleted: current.lastCompleted || accumulated.lastCompleted,\n filesModified: mergeArrays(accumulated.filesModified, current.filesModified),\n filesExcluded: accumulated.filesExcluded + current.filesExcluded,\n workItems: [...accumulated.workItems, ...newWorkItems],\n };\n}\n\nfunction saveCumulativeState(notesDir: string, data: TranscriptData, notePath: string | null): void {\n try {\n const filePath = join(notesDir, CUMULATIVE_STATE_FILE);\n writeFileSync(filePath, JSON.stringify({\n ...data,\n notePath,\n lastUpdated: new Date().toISOString(),\n }, null, 2));\n console.error(`Cumulative state saved (${data.workItems.length} work items, ${data.filesModified.length} files)`);\n } catch (err) {\n console.error(`Failed to save cumulative state: ${err}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Daemon IPC \u2014 enqueue registry-scan work item\n// ---------------------------------------------------------------------------\n\n/**\n * Enqueue a registry-scan work item with the daemon.\n * Fire-and-forget \u2014 never throws, never blocks the pre-compact hook.\n */\nfunction enqueueRegistryScan(): Promise<void> {\n return new Promise((resolve) => {\n let done = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(): void {\n if (done) return;\n done = true;\n if (timer !== null) { clearTimeout(timer); timer = null; }\n try { client.destroy(); } catch { /* ignore */ }\n resolve();\n }\n\n const client = connect(DAEMON_SOCKET, () => {\n const msg = JSON.stringify({\n id: randomUUID(),\n method: 'work_queue_enqueue',\n params: {\n type: 'registry-scan',\n priority: 5,\n payload: {},\n },\n }) + '\\n';\n client.write(msg);\n });\n\n client.on('data', () => finish());\n client.on('error', () => finish());\n client.on('end', () => finish());\n timer = setTimeout(() => finish(), DAEMON_TIMEOUT_MS);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Daemon IPC \u2014 enqueue session-summary work item\n// ---------------------------------------------------------------------------\n\n/**\n * Send a session-summary work item to the daemon via IPC.\n * Returns true on success, false if the daemon is unreachable.\n * Times out after DAEMON_TIMEOUT_MS to avoid blocking the hook.\n */\nfunction enqueueSessionSummary(payload: {\n cwd: string;\n sessionId?: string;\n transcriptPath?: string;\n}): Promise<boolean> {\n return new Promise((resolve) => {\n let done = false;\n let buffer = '';\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(ok: boolean): void {\n if (done) return;\n done = true;\n if (timer !== null) { clearTimeout(timer); timer = null; }\n try { client.destroy(); } catch { /* ignore */ }\n resolve(ok);\n }\n\n const client = connect(DAEMON_SOCKET, () => {\n const msg = JSON.stringify({\n id: randomUUID(),\n method: 'work_queue_enqueue',\n params: {\n type: 'session-summary',\n priority: 4, // lower priority than session-end (2)\n payload: {\n cwd: payload.cwd,\n sessionId: payload.sessionId,\n transcriptPath: payload.transcriptPath,\n },\n },\n }) + '\\n';\n client.write(msg);\n });\n\n client.on('data', (chunk: Buffer) => {\n buffer += chunk.toString();\n const nl = buffer.indexOf('\\n');\n if (nl === -1) return;\n const line = buffer.slice(0, nl);\n try {\n const response = JSON.parse(line) as { ok: boolean; error?: string; result?: { id: string } };\n if (response.ok) {\n console.error(`PRE-COMPACT: Session summary enqueued with daemon (id=${response.result?.id}).`);\n finish(true);\n } else {\n console.error(`PRE-COMPACT: Daemon rejected session-summary: ${response.error}`);\n finish(false);\n }\n } catch {\n finish(false);\n }\n });\n\n client.on('error', (e: NodeJS.ErrnoException) => {\n if (e.code === 'ENOENT' || e.code === 'ECONNREFUSED') {\n console.error('PRE-COMPACT: Daemon not running \u2014 skipping session summary.');\n } else {\n console.error(`PRE-COMPACT: Daemon socket error: ${e.message}`);\n }\n finish(false);\n });\n\n timer = setTimeout(() => {\n console.error('PRE-COMPACT: Daemon IPC timed out \u2014 skipping session summary.');\n finish(false);\n }, DAEMON_TIMEOUT_MS);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\nasync function main() {\n // Skip probe/health-check sessions (e.g. CodexBar ClaudeProbe)\n if (isProbeSession()) {\n process.exit(0);\n }\n\n let hookInput: HookInput | null = null;\n\n try {\n const decoder = new TextDecoder();\n let input = '';\n const timeoutPromise = new Promise<void>((resolve) => { setTimeout(resolve, 500); });\n const readPromise = (async () => {\n for await (const chunk of process.stdin) {\n input += decoder.decode(chunk, { stream: true });\n }\n })();\n await Promise.race([readPromise, timeoutPromise]);\n if (input.trim()) {\n hookInput = JSON.parse(input) as HookInput;\n }\n } catch {\n // Silently handle input errors\n }\n\n const compactType = hookInput?.compact_type || hookInput?.trigger || 'auto';\n let ntfyTokenDisplay: string | null = null;\n\n if (hookInput?.transcript_path) {\n const stats = getTranscriptStats(hookInput.transcript_path);\n\n // BUG (real headers pulled from this machine's history): \"~995073k\n // tokens\", \"~2135399k tokens\" \u2014 995 million and 2.1 billion. The old\n // source, calculateSessionTokens, sums usage across EVERY line in the\n // transcript, which is a lifetime token-spend counter, not a context\n // fill reading \u2014 it necessarily exceeds the window on any long session\n // and is not what \"context compression triggered at ~X tokens\" means.\n // getContextFill reads the most recent usage entry only (or the\n // statusline's own reading), which is the actual current fill and can\n // never legitimately exceed the window. formatContextFill still\n // clamps-and-flags rather than trust that, and reports \"unknown\"\n // instead of guessing when neither source has an answer.\n const fill = getContextFill({\n sessionId: hookInput.session_id,\n transcriptPath: hookInput.transcript_path,\n });\n const tokenDisplay = formatContextFill(fill).text;\n ntfyTokenDisplay = tokenDisplay;\n\n // -----------------------------------------------------------------\n // Single-pass transcript parsing + cumulative state merge\n // -----------------------------------------------------------------\n const data = parseTranscript(hookInput.transcript_path, hookInput.scratchpad_dir);\n\n // Find notes directory early \u2014 needed for cumulative state\n let notesInfo: { path: string; isLocal: boolean };\n try {\n notesInfo = hookInput.cwd\n ? findNotesDir(hookInput.cwd)\n : { path: join(dirname(hookInput.transcript_path), 'Notes'), isLocal: false };\n } catch {\n notesInfo = { path: join(dirname(hookInput.transcript_path), 'Notes'), isLocal: false };\n }\n\n // Load accumulated state from previous compactions and merge\n const accumulated = loadCumulativeState(notesInfo.path);\n // Read BEFORE saveCumulativeState (below) overwrites this file for the\n // CURRENT compaction \u2014 this is the previous one's timestamp, the\n // freshness line a cached handover has to clear (see the injection step).\n const previousCompactionAt = readPreviousCompactionTimestamp(notesInfo.path);\n const merged = mergeTranscriptData(accumulated, data);\n const state = formatSessionState(merged, hookInput.cwd);\n\n if (accumulated) {\n console.error(`Loaded cumulative state: ${accumulated.workItems.length} work items, ${accumulated.filesModified.length} files from previous compaction(s)`);\n }\n\n // -----------------------------------------------------------------\n // Persist session state to numbered session note.\n // RULE: ONE note per session. NEVER create a new note during compaction.\n // If the latest note is completed, reopen it (remove completed status)\n // rather than creating a duplicate.\n // -----------------------------------------------------------------\n let notePath: string | null = null;\n\n try {\n notePath = getCurrentNotePath(notesInfo.path);\n\n if (!notePath) {\n // Truly no note exists at all \u2014 create one (first compaction of a session\n // that started before PAI was installed, or corrupted notes dir)\n console.error('No session note found \u2014 creating one for checkpoint');\n notePath = createSessionNote(notesInfo.path, 'Untitled Session');\n } else {\n // If the latest note is completed, reopen it for this session's checkpoints\n // instead of creating a duplicate. This handles the case where session-stop\n // finalized a note but the session continued (e.g., user said \"end session\"\n // but kept working).\n try {\n let noteContent = readFileSync(notePath, 'utf-8');\n if (noteContent.includes('**Status:** Completed')) {\n noteContent = noteContent.replace('**Status:** Completed', '**Status:** In Progress');\n writeFileSync(notePath, noteContent);\n console.error(`Reopened completed note for continued session: ${basename(notePath)}`);\n }\n } catch { /* proceed with existing note */ }\n }\n\n // 1. Write rich checkpoint with full session state\n const checkpointBody = state\n ? `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.\\n\\n${state}`\n : `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.`;\n appendCheckpoint(notePath, checkpointBody);\n\n // 2. Write work items to \"Work Done\" section (uses merged cumulative data)\n if (merged.workItems.length > 0) {\n addWorkToSessionNote(notePath, merged.workItems, `Pre-Compact (~${tokenDisplay} tokens)`);\n console.error(`Added ${merged.workItems.length} work item(s) to session note`);\n }\n\n // 3. Rename session note with a meaningful title (uses merged data for richer titles)\n const title = deriveTitle(merged);\n if (title) {\n const newPath = renameSessionNote(notePath, title);\n if (newPath !== notePath) {\n // Update H1 title inside the note to match\n try {\n let noteContent = readFileSync(newPath, 'utf-8');\n noteContent = noteContent.replace(\n /^(# Session \\d+:)\\s*.*$/m,\n `$1 ${title}`\n );\n writeFileSync(newPath, noteContent);\n console.error(`Updated note H1 to match rename`);\n } catch { /* ignore */ }\n notePath = newPath;\n }\n }\n\n console.error(`Rich checkpoint saved: ${basename(notePath)}`);\n } catch (noteError) {\n console.error(`Could not save checkpoint: ${noteError}`);\n }\n\n // Save cumulative state for next compaction\n saveCumulativeState(notesInfo.path, merged, notePath);\n\n // -----------------------------------------------------------------\n // Update TODO.md with proper ## Continue section (like \"pause session\")\n // -----------------------------------------------------------------\n if (hookInput.cwd && notePath) {\n try {\n const noteFilename = basename(notePath);\n updateTodoContinue(hookInput.cwd, noteFilename, state, tokenDisplay);\n console.error('TODO.md ## Continue section updated');\n } catch (todoError) {\n console.error(`Could not update TODO.md: ${todoError}`);\n }\n }\n\n // -----------------------------------------------------------------------\n // Save session state to temp file for post-compact injection.\n //\n // PreCompact hooks have NO stdout support (Claude Code ignores it).\n // Instead, we write the injection payload to a temp file keyed by\n // session_id. The SessionStart(compact) hook reads it and outputs\n // to stdout, which IS injected into the post-compaction context.\n //\n // Always fires (even with thin state) \u2014 includes note path so the AI\n // can enrich the session note post-compaction using its own context.\n // -----------------------------------------------------------------------\n if (hookInput.session_id) {\n const stateText = state || `Working directory: ${hookInput.cwd || 'unknown'}`;\n const noteInfo = notePath\n ? `\\nSESSION NOTE: ${notePath}\\nIf this note still has a generic title (e.g. \"New Session\", \"Context Compression\"),\\nrename it based on actual work done and add a rich summary.`\n : '';\n\n // -------------------------------------------------------------------\n // ADDITIVE: a model-written handover (decisions/reasoning/open threads\n // \u2014 see the threshold-triggered context-handover-worker) sits ALONGSIDE\n // the mechanical scrape above, never replaces it. The scrape is the\n // floor and stays intact on every path, so this is never worse than\n // the pre-existing behaviour \u2014 only sometimes better. \"Fresh\" means\n // generated after this session's PREVIOUS compaction: a handover\n // cached before that point describes state the last compaction\n // already accounted for, not what has happened since.\n //\n // BUG (live): a production digest was found with NO \"HANDOVER SOURCE:\"\n // line at all \u2014 its cause was never pinned down for certain (the\n // deployed hook is a compiled dist/ artifact reached through\n // ${PAI_DIR}/Hooks/..., a separate build step from editing this\n // source, which is itself a plausible way for \"works when I run the\n // .ts source\" and \"missing in production\" to diverge). Regardless of\n // cause: sourceLabel and handoverBlock are now computed in their own\n // try/catch, so ANY failure in the cache read or the freshness\n // comparison degrades to an explicit error label rather than being\n // capable of preventing the line from existing at all.\n // -------------------------------------------------------------------\n let sourceLabel: string;\n let handoverBlock: string;\n try {\n const cachedHandover = readContextHandoverCache(hookInput.session_id);\n const handoverIsFresh =\n cachedHandover !== null &&\n (!previousCompactionAt || new Date(cachedHandover.generatedAt) > new Date(previousCompactionAt));\n\n sourceLabel = handoverIsFresh\n ? `model-written handover (${cachedHandover!.model}, generated ${cachedHandover!.generatedAt}, ` +\n `threshold=${cachedHandover!.threshold}) + mechanical scrape`\n : 'mechanical scrape only (no fresh model-written handover was available)';\n\n handoverBlock = handoverIsFresh\n ? [\n '',\n '--- MODEL-WRITTEN HANDOVER (decisions, reasoning, open threads \u2014 not in the scrape above) ---',\n cachedHandover!.summary,\n '--- end model-written handover ---',\n ].join('\\n')\n : '';\n } catch (err) {\n console.error(`Failed to resolve handover cache \u2014 falling back to scrape-only: ${err}`);\n sourceLabel = `mechanical scrape only (error resolving handover cache: ${err})`;\n handoverBlock = '';\n }\n\n const injection = [\n '<system-reminder>',\n `SESSION STATE RECOVERED AFTER COMPACTION (${compactType}, ~${tokenDisplay} tokens)`,\n `HANDOVER SOURCE: ${sourceLabel}`,\n '',\n stateText,\n noteInfo,\n handoverBlock,\n '',\n 'IMPORTANT: This session state was captured before context compaction.',\n 'Use it to maintain continuity. Continue the conversation from where',\n 'it left off without asking the user to repeat themselves.',\n 'Continue with the last task that you were asked to work on.',\n '</system-reminder>',\n ].join('\\n');\n\n try {\n const stateFile = join(tmpdir(), `pai-compact-state-${hookInput.session_id}.txt`);\n writeFileSync(stateFile, injection, 'utf-8');\n console.error(`Session state saved to ${stateFile} (${injection.length} chars)`);\n } catch (err) {\n console.error(`Failed to save state file: ${err}`);\n }\n }\n }\n\n // -----------------------------------------------------------------------\n // Enqueue session-summary work item with daemon for AI-powered note generation\n // -----------------------------------------------------------------------\n if (hookInput?.cwd) {\n try {\n await enqueueSessionSummary({\n cwd: hookInput.cwd,\n sessionId: hookInput.session_id,\n transcriptPath: hookInput.transcript_path,\n });\n } catch (err) {\n console.error(`Could not enqueue session-summary: ${err}`);\n }\n }\n\n // -----------------------------------------------------------------------\n // Enqueue registry-scan so pai session recent stays fresh after compaction\n // -----------------------------------------------------------------------\n try {\n await enqueueRegistryScan();\n } catch { /* non-fatal */ }\n\n // Send ntfy.sh notification\n const ntfyMessage = ntfyTokenDisplay && ntfyTokenDisplay !== 'unknown'\n ? `Auto-pause: ~${ntfyTokenDisplay} tokens`\n : 'Context compressing';\n await sendNtfyNotification(ntfyMessage);\n\n process.exit(0);\n}\n\nmain().catch(() => {\n process.exit(0);\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 { PAI_DIR } from '../pai-paths.js';\n\n// Re-export PAI_DIR for consumers\nexport { PAI_DIR };\nexport const PROJECTS_DIR = join(PAI_DIR, 'projects');\n\n/**\n * Directories known to be automated health-check / probe sessions.\n * Hooks should exit early for these to avoid registry clutter and wasted work.\n */\nconst PROBE_CWD_PATTERNS = [\n '/CodexBar/ClaudeProbe',\n '/ClaudeProbe',\n];\n\n/**\n * Check if the current working directory belongs to a probe/health-check session.\n * Returns true if hooks should skip this session entirely.\n */\nexport function isProbeSession(cwd?: string): boolean {\n const dir = cwd || process.cwd();\n return PROBE_CWD_PATTERNS.some(pattern => dir.includes(pattern));\n}\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with -\n * - Replace space with -\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-')\n .replace(/\\./g, '-')\n .replace(/ /g, '-');\n}\n\n/** Get the project directory for a given working directory. */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/** Get the Notes directory for a project (central location). */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory \u2014 checks local first, falls back to central.\n * Does NOT create the directory.\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n const localPaths = [\n join(cwd, 'Notes'),\n join(cwd, 'notes'),\n join(cwd, '.claude', 'Notes'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return { path, isLocal: true };\n }\n }\n\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/** Get the sessions/ directory for a project (stores .jsonl transcripts). */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/** Get the sessions/ directory from a project directory path. */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n// ---------------------------------------------------------------------------\n// Directory creation helpers\n// ---------------------------------------------------------------------------\n\n/** Ensure the Notes directory exists for a project. @deprecated Use ensureNotesDirSmart() */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\n return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists \u2192 use it (don't create anything new)\n * - If no local Notes/ \u2192 ensure central exists and use that\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n if (found.isLocal) return found;\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n return found;\n}\n\n/** Ensure the sessions/ directory exists for a project. */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/** Ensure the sessions/ directory exists (from project dir path). */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/**\n * Publish every project-root .jsonl into sessions/ as well, WITHOUT removing it.\n *\n * This used to renameSync, and that is how PAI destroyed its users' sessions.\n *\n * `claude --resume <uuid>` finds a transcript only at the project root. Move it\n * into sessions/ and the session becomes permanently unresumable \u2014 measured\n * 2026-08-04: `claude --resume b3462801` (867 KB of real work, sessions/ only)\n * answers \"No conversation found with session ID\", while a top-level id is found\n * fine. Nothing warned; the id still looked valid everywhere PAI displayed it.\n *\n * The damage was not occasional. This ran from a UserPromptSubmit hook excluding\n * only the CURRENT session, so every prompt anyone typed unresumed every other\n * session in the project. One PAI project measured 1 transcript at top level\n * against 52 underneath. Among the casualties was 046bb712 \u2014 the exact id PAI's\n * own handover tells the user to resume.\n *\n * A hardlink satisfies both sides, which is why this is a two-line fix rather\n * than a redesign: the archive genuinely has consumers that read sessions/\n * (session-summary-worker, registry/moved, session/autosave), and `--resume`\n * needs the root path. One inode, two names, no copy, no window where the file\n * is missing from either place.\n *\n * Never unlink the source. Tidying up another tool's store was the whole\n * mistake; a stale duplicate is free, a lost session is not. Every caller of\n * `transcriptFiles()` was checked before choosing this \u2014 they all test emptiness\n * (`.length > 0`), never count, so the duplicate cannot skew a project's stats.\n *\n * `excludeFile` keeps a hook from archiving the transcript it is itself watching\n * being written. It is not a \"finished sessions only\" guard and must not be read\n * as one: it excludes exactly one file, the caller's own, so with two sessions\n * live in one project each still archives the other mid-turn. A consumer that\n * needs \"finished only\" has to enforce that itself \u2014 this function cannot know\n * what is live.\n *\n * Returns the number of files newly archived.\n */\nexport function archiveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) return 0;\n\n const files = readdirSync(projectDir);\n let archivedCount = 0;\n\n for (const file of files) {\n if (!file.endsWith('.jsonl') || file === excludeFile) continue;\n\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n\n // Already archived \u2014 including by an earlier rename, before this was a\n // hardlink. Those are the sessions that need restoring to the root, which is\n // a separate repair and not this function's job.\n if (existsSync(destPath)) continue;\n\n try {\n linkSync(sourcePath, destPath);\n if (!silent) console.error(`Archived ${file} \u2192 sessions/ (still resumable)`);\n archivedCount++;\n } catch (error) {\n // Cross-device (EXDEV) is the realistic failure: sessions/ on another\n // volume. Copy instead, and still leave the original alone.\n try {\n copyFileSync(sourcePath, destPath);\n if (!silent) console.error(`Copied ${file} \u2192 sessions/ (hardlink unavailable)`);\n archivedCount++;\n } catch {\n if (!silent) console.error(`Could not archive ${file}: ${error}`);\n }\n }\n }\n\n return archivedCount;\n}\n\n/**\n * @deprecated Renamed to `archiveSessionFilesToSessionsDir`, which is what it\n * now does. Kept so an out-of-tree caller fails loudly at the type level rather\n * than silently keeping the old destructive name for a non-destructive action.\n */\nexport const moveSessionFilesToSessionsDir = archiveSessionFilesToSessionsDir;\n\n// ---------------------------------------------------------------------------\n// CLAUDE.md / TODO.md discovery\n// ---------------------------------------------------------------------------\n\n/** Find TODO.md \u2014 check local first, fallback to central. */\nexport function findTodoPath(cwd: string): string {\n const localPaths = [\n join(cwd, 'TODO.md'),\n join(cwd, 'notes', 'TODO.md'),\n join(cwd, 'Notes', 'TODO.md'),\n join(cwd, '.claude', 'TODO.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) return path;\n }\n\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/** Find CLAUDE.md \u2014 returns the FIRST found path. */\nexport function findClaudeMdPath(cwd: string): string | null {\n const paths = findAllClaudeMdPaths(cwd);\n return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations in priority order.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n const localPaths = [\n join(cwd, '.claude', 'CLAUDE.md'),\n join(cwd, 'CLAUDE.md'),\n join(cwd, 'Notes', 'CLAUDE.md'),\n join(cwd, 'notes', 'CLAUDE.md'),\n join(cwd, 'Prompts', 'CLAUDE.md'),\n join(cwd, 'prompts', 'CLAUDE.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) foundPaths.push(path);\n }\n\n return foundPaths;\n}\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(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\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_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(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_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 PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_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 * 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(HISTORY_DIR, subdir, `${year}-${month}`, filename);\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", "/**\n * TODO.md management \u2014 creation, task updates, checkpoints, and Continue section.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { findTodoPath } from './paths.js';\nimport { applyContinue } from '../../../../session/checkpoint-block.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Task item for TODO.md. */\nexport interface TodoItem {\n content: string;\n completed: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Ensure TODO.md exists. Creates it with default structure if missing.\n * Returns the path to the TODO.md file.\n */\nexport function ensureTodoMd(cwd: string): string {\n const todoPath = findTodoPath(cwd);\n\n if (!existsSync(todoPath)) {\n const parentDir = join(todoPath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n\n const content = `# TODO\n\n## Current Session\n\n- [ ] (Tasks will be tracked here)\n\n## Backlog\n\n- [ ] (Future tasks)\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Created TODO.md: ${todoPath}`);\n }\n\n return todoPath;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Update TODO.md with current session tasks.\n * Preserves the Backlog section and ensures exactly ONE timestamp at the end.\n */\nexport function updateTodoMd(cwd: string, tasks: TodoItem[], sessionSummary?: string): void {\n const todoPath = ensureTodoMd(cwd);\n const content = readFileSync(todoPath, 'utf-8');\n\n const backlogMatch = content.match(/## Backlog[\\s\\S]*?(?=\\n---|\\n\\*Last updated|$)/);\n const backlogSection = backlogMatch\n ? backlogMatch[0].trim()\n : '## Backlog\\n\\n- [ ] (Future tasks)';\n\n const taskLines = tasks.length > 0\n ? tasks.map(t => `- [${t.completed ? 'x' : ' '}] ${t.content}`).join('\\n')\n : '- [ ] (No active tasks)';\n\n const newContent = `# TODO\n\n## Current Session\n\n${taskLines}\n\n${sessionSummary ? `**Session Summary:** ${sessionSummary}\\n\\n` : ''}${backlogSection}\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, newContent);\n console.error(`Updated TODO.md: ${todoPath}`);\n}\n\n/**\n * Add a checkpoint entry to TODO.md (without replacing tasks).\n * Ensures exactly ONE timestamp line at the end.\n */\nexport function addTodoCheckpoint(cwd: string, checkpoint: string): void {\n const todoPath = ensureTodoMd(cwd);\n let content = readFileSync(todoPath, 'utf-8');\n\n // Remove ALL existing timestamp lines and trailing separators\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n\n const checkpointText = `\\n**Checkpoint (${new Date().toISOString()}):** ${checkpoint}\\n\\n`;\n\n const backlogIndex = content.indexOf('## Backlog');\n if (backlogIndex !== -1) {\n content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);\n } else {\n const continueIndex = content.indexOf('## Continue');\n if (continueIndex !== -1) {\n const afterContinue = content.indexOf('\\n---', continueIndex);\n if (afterContinue !== -1) {\n const insertAt = afterContinue + 4;\n content = content.substring(0, insertAt) + '\\n' + checkpointText + content.substring(insertAt);\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n }\n\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${new Date().toISOString()}*\\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Checkpoint added to TODO.md`);\n}\n\n/**\n * The Claude Code session UUID, read off the transcript path.\n *\n * Claude Code names a transcript `<session-uuid>.jsonl`, so the hooks that\n * receive a transcript path are holding the session's identity without knowing\n * it. Both session-end writers had one available and neither used it.\n *\n * Shape-checked rather than trusted: only a UUID is returned, so a renamed,\n * archived or otherwise unexpected filename yields undefined and the caller\n * falls back to the session line rather than writing a `session-id` that\n * identifies nothing. A wrong id is worse than none \u2014 it would make two\n * different sessions compare equal.\n */\nexport function sessionIdFromTranscript(transcriptPath: string | undefined): string | undefined {\n if (!transcriptPath) return undefined;\n const base = basename(transcriptPath).replace(/\\.jsonl$/, '');\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(base)\n ? base\n : undefined;\n}\n\n/**\n * Update the ## Continue section at the top of TODO.md.\n *\n * This is the unattended writer: the pre-compact hook and the daemon's\n * work-queue worker both come through here. It used to build its own block and\n * strip the existing section with `/## Continue\\n[\\s\\S]*?\\n---\\n+/` \u2014 a\n * non-greedy match to the first `---`, which cuts a rich checkpoint in half at\n * the first horizontal rule inside its body and leaves the remainder orphaned\n * in the document.\n *\n * More importantly it was the writer that actually destroyed model-authored\n * checkpoints: `pai pause` wrote one, then this ran seconds later on session\n * end and replaced it with `Working directory: \u2026 Check the latest session note\n * for details.`\n *\n * It now delegates to the shared checkpoint module in \"auto\" mode, which means\n * it inherits the preservation rules rather than reimplementing them. Guarding\n * here rather than at each of the three call sites is deliberate: any future\n * caller inherits the behaviour without knowing it exists.\n */\nexport function updateTodoContinue(\n cwd: string,\n noteFilename: string,\n state: string | null,\n tokenDisplay: string,\n /**\n * The Claude Code session UUID, when the caller knows it.\n *\n * Optional only because a caller may genuinely not have it; supply it\n * whenever you can. Without it `isSameSession` falls back to comparing the\n * note FILENAME, and pausing renames the note \u2014 so a session stops\n * recognising its own checkpoint seconds after writing it, and an automated\n * write is then free to replace it as though it belonged to a predecessor.\n * That is how a live session lost a model-authored handover on 2026-08-04.\n */\n sessionId?: string\n): void {\n // Ensure a TODO.md exists so applyContinue writes to the same file the rest\n // of the hooks lib uses.\n ensureTodoMd(cwd);\n\n const result = applyContinue({\n rootPath: cwd,\n authored: 'auto',\n // The hooks identify a session by its note filename; `pai pause` resolves\n // the same string from the registry (and falls back to this filename when\n // the registry has no session row). They must agree, because this string\n // is the key that decides whether an authored checkpoint belongs to the\n // current session and must be preserved.\n sessionLine: noteFilename.replace(/\\.md$/, ''),\n // The UUID is authoritative when present; the line above is the fallback\n // for callers that cannot supply one, and it is mutable by design.\n sessionId,\n cwd,\n body: state?.trim() || undefined,\n });\n\n if (result.action === 'preserved') {\n console.error(\n 'TODO.md ## Continue left intact \u2014 authored checkpoint for this session'\n );\n return;\n }\n\n if (result.action === 'failed') {\n console.error(`TODO.md ## Continue update failed: ${result.error}`);\n return;\n }\n\n // Refresh the trailing \"Last updated\" stamp without disturbing the block.\n try {\n const todoPath = result.path!;\n const now = new Date().toISOString();\n let content = readFileSync(todoPath, 'utf-8');\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${now}*\\n`;\n writeFileSync(todoPath, content);\n } catch {\n // Non-fatal \u2014 the checkpoint itself is already written.\n }\n\n console.error(\n result.carriedForward\n ? 'TODO.md ## Continue section updated (previous content carried forward)'\n : 'TODO.md ## Continue section updated'\n );\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/** PAI_DIR \u2014 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 \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 * context-fill.ts \u2014 \"how full is this session's context window, right now?\"\n *\n * No hook payload carries this number directly (verified: PreCompact,\n * UserPromptSubmit and PostToolUse stdin never include `context_window`).\n * Two sources can reconstruct it, in order of preference:\n *\n * 1. STATUSLINE STATE FILE \u2014 statusline-command.sh receives the exact\n * figure from Claude Code (`.context_window.used_percentage` /\n * `.context_window.context_window_size`) on every render and persists\n * it to `${TMPDIR}/pai-context-<session_id>.json`. This is authoritative\n * but depends on the status line having rendered recently \u2014 a session\n * whose terminal isn't drawing a status line (headless, backgrounded)\n * leaves this file missing or stale.\n *\n * 2. TRANSCRIPT USAGE \u2014 every `message.usage` entry in the session's own\n * .jsonl transcript already reports the token accounting for that one\n * API call: `input_tokens + cache_read_input_tokens +\n * cache_creation_input_tokens` on the MOST RECENT such entry *is* the\n * current context fill, because each request resends the full context.\n * This is why it must be the last entry's fields summed, never a sum\n * across entries \u2014 summing across entries is a running token-spend\n * total, not a fill reading (see calculateSessionTokens, a different\n * metric answering a different question).\n *\n * When neither source is available, the answer is UNKNOWN, not zero. A zero\n * reads as \"plenty of room\" and would silently suppress every downstream\n * decision that depends on this number (the compaction handover chief among\n * them) for the entire session.\n */\n\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/** Fallback window size when nothing on hand reports one. Matches the\n * default statusline-command.sh already falls back to. */\nexport const DEFAULT_CONTEXT_WINDOW = 200_000;\n\n/** How many trailing lines of the transcript to scan for a usage entry.\n * A single turn is rarely more than a handful of JSONL lines (assistant\n * text + tool_use/tool_result pairs), so 40 comfortably covers one turn\n * even a busy one, without reading the whole file on every check. */\nconst TRANSCRIPT_TAIL_LINES = 40;\n\n/** A statusline reading older than this is treated as not there at all \u2014\n * a stale number is worse than none, because it looks confident. */\nconst STATUSLINE_STALE_MS = 5 * 60 * 1000; // 5 minutes\n\nexport type ContextFillSource = \"statusline\" | \"transcript\" | \"unknown\";\n\nexport interface ContextFillReading {\n status: \"ok\" | \"unknown\";\n /** Tokens currently occupying the context window, or null when unknown. */\n usedTokens: number | null;\n windowSize: number;\n /** usedTokens / windowSize, or null when unknown. Not clamped to [0,1] \u2014\n * callers that need a clamped display value do that themselves so the\n * raw reading (which can legitimately exceed 1 for a moment) is never\n * silently rewritten here. */\n fraction: number | null;\n source: ContextFillSource;\n}\n\nfunction unknownReading(windowSize: number): ContextFillReading {\n return { status: \"unknown\", usedTokens: null, windowSize, fraction: null, source: \"unknown\" };\n}\n\n// ---------------------------------------------------------------------------\n// Source 1 \u2014 statusline state file\n// ---------------------------------------------------------------------------\n\nexport function statuslineStateFilePath(sessionId: string): string {\n return join(tmpdir(), `pai-context-${sessionId}.json`);\n}\n\ninterface StatuslineState {\n used_percentage?: number;\n context_window_size?: number;\n session_id?: string;\n timestamp?: number;\n}\n\n/**\n * Read the state file statusline-command.sh writes on every render.\n * Returns null when the file is missing, unparsable, or older than\n * STATUSLINE_STALE_MS \u2014 all three mean \"not a source right now\".\n */\nexport function readStatuslineFill(sessionId: string, now = Date.now()): ContextFillReading | null {\n if (!sessionId) return null;\n const path = statuslineStateFilePath(sessionId);\n if (!existsSync(path)) return null;\n\n let raw: StatuslineState;\n try {\n raw = JSON.parse(readFileSync(path, \"utf-8\"));\n } catch {\n return null;\n }\n\n if (typeof raw.timestamp !== \"number\" || now - raw.timestamp > STATUSLINE_STALE_MS) {\n return null; // stale \u2014 fall through to the transcript source\n }\n if (typeof raw.used_percentage !== \"number\" || typeof raw.context_window_size !== \"number\") {\n return null;\n }\n\n const windowSize = raw.context_window_size;\n const usedTokens = Math.round((raw.used_percentage / 100) * windowSize);\n return {\n status: \"ok\",\n usedTokens,\n windowSize,\n fraction: raw.used_percentage / 100,\n source: \"statusline\",\n };\n}\n\n// ---------------------------------------------------------------------------\n// Source 2 \u2014 transcript usage (fallback)\n// ---------------------------------------------------------------------------\n\ninterface UsageEntry {\n input_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n}\n\n/**\n * Sum the three context-carrying fields of ONE usage object \u2014 never across\n * usage objects. A single `message.usage` already reports what that one API\n * call sent as context; adding another entry's numbers to it produces a\n * cumulative spend figure, not a fill reading, and can exceed the window\n * size many times over on a long session (the exact defect this helper\n * exists to not repeat \u2014 see the PreCompact header fix in the same change).\n */\nfunction usageTotal(usage: UsageEntry): number {\n return (\n (usage.input_tokens || 0) +\n (usage.cache_read_input_tokens || 0) +\n (usage.cache_creation_input_tokens || 0)\n );\n}\n\n/**\n * Derive context fill from the tail of a .jsonl transcript: the most recent\n * `message.usage` entry, read from the end backwards so a trailing line with\n * no usage field (a plain text turn, a tool_result) doesn't hide one just\n * before it.\n */\nexport function contextFillFromTranscript(\n transcriptPath: string,\n windowSize = DEFAULT_CONTEXT_WINDOW\n): ContextFillReading {\n if (!transcriptPath || !existsSync(transcriptPath)) return unknownReading(windowSize);\n\n let raw: string;\n try {\n raw = readFileSync(transcriptPath, \"utf-8\");\n } catch {\n return unknownReading(windowSize);\n }\n\n const lines = raw.trim().split(\"\\n\").filter((l) => l.trim());\n const tail = lines.slice(-TRANSCRIPT_TAIL_LINES);\n\n for (let i = tail.length - 1; i >= 0; i--) {\n let entry: { message?: { usage?: UsageEntry } };\n try {\n entry = JSON.parse(tail[i]);\n } catch {\n continue;\n }\n const usage = entry?.message?.usage;\n if (usage && typeof usage === \"object\") {\n const usedTokens = usageTotal(usage);\n return {\n status: \"ok\",\n usedTokens,\n windowSize,\n fraction: usedTokens / windowSize,\n source: \"transcript\",\n };\n }\n }\n\n return unknownReading(windowSize);\n}\n\n// ---------------------------------------------------------------------------\n// Precedence \u2014 statusline (fresh) > transcript > unknown\n// ---------------------------------------------------------------------------\n\nexport function getContextFill(\n input: { sessionId?: string; transcriptPath?: string; windowSize?: number },\n now = Date.now()\n): ContextFillReading {\n const windowSize = input.windowSize ?? DEFAULT_CONTEXT_WINDOW;\n\n if (input.sessionId) {\n const fromStatusline = readStatuslineFill(input.sessionId, now);\n if (fromStatusline) return fromStatusline;\n }\n\n if (input.transcriptPath) {\n return contextFillFromTranscript(input.transcriptPath, windowSize);\n }\n\n return unknownReading(windowSize);\n}\n\n// ---------------------------------------------------------------------------\n// Display helper \u2014 clamp-and-flag rather than ever print an absurd number\n// ---------------------------------------------------------------------------\n\nexport interface FillDisplay {\n /** e.g. \"63k\" or \"unknown\". Never a number larger than the window. */\n text: string;\n /** True when the raw reading exceeded the window size and had to be\n * clamped \u2014 that reading was a bug, not a fill, and callers may want to\n * log it even though the displayed text is already safe. */\n flagged: boolean;\n}\n\nexport function formatContextFill(reading: ContextFillReading): FillDisplay {\n if (reading.status !== \"ok\" || reading.usedTokens === null) {\n return { text: \"unknown\", flagged: false };\n }\n\n let used = reading.usedTokens;\n let flagged = false;\n if (used > reading.windowSize) {\n flagged = true;\n used = reading.windowSize;\n }\n\n const text = used > 1000 ? `${Math.round(used / 1000)}k` : String(used);\n return { text: flagged ? `${text}+ (clamped \u2014 reading exceeded window)` : text, flagged };\n}\n\n// ---------------------------------------------------------------------------\n// Threshold-triggered handover \u2014 WHEN to warm up / refresh the model-written\n// summary ahead of a compaction.\n// ---------------------------------------------------------------------------\n//\n// THE CONFIGURED VALUE DOES NOT PREDICT THE TRIGGER. CLAUDE_AUTOCOMPACT_\n// PCT_OVERRIDE was 80 throughout, on this machine, across BOTH of the\n// following regimes \u2014 the same configured number, two different truths:\n//\n// 2026-08-16 .. 2026-09-10 preTokens ~ 993,096 \u2013 1,002,500 (~100% of a 1M window)\n// 2026-09-12 .. 2026-09-15 preTokens ~ 782,981 \u2013 791,995 (~78-79% of a 1M window)\n//\n// A threshold derived only from the configured override was wrong for the\n// second regime and would be wrong again the next time it moves \u2014 the\n// config is not ground truth, it is what Claude Code claims it will honor.\n//\n// GROUND TRUTH IS ON DISK ALREADY: every real compaction writes a\n// `compact_boundary` system event to the transcript with\n// `compactMetadata.preTokens` \u2014 the platform's own count of context tokens\n// immediately before IT compacted. `measureCompactionTrigger` scans a\n// project's own transcripts for these and takes the MINIMUM of the most\n// recent three (minimum, not mean: warm-up must fire before the earliest\n// plausible boundary, not the average one). A project that has compacted\n// before learns its own trigger and needs no code change when the regime\n// moves again \u2014 the same derivation produced 998,508 pre-09-12 and ~784k\n// after, from one unchanged formula.\n//\n// The configured chain \u2014 env override, then a default \u2014 is the fallback,\n// used ONLY when a project has no compaction history yet:\n//\n// effectiveTrigger = measured ?? (windowSize * (override ?? 80) / 100)\n//\n// DEFAULT IS 80, NOT 100, WHEN NOTHING IS KNOWN. 100 was considered \u2014 it\n// matches the FIRST regime above \u2014 and rejected: the costs are asymmetric.\n// Warming up too early wastes one summary; cheap, invisible. Warming up too\n// late produces exactly the degraded successor session this feature exists\n// to prevent. 80 is the conservative default until a project has its own\n// measured history to correct it.\n//\n// Margins below the effective trigger are absolute tokens, sized against the\n// largest single-turn context jump measured: 62,383 tokens (517,952 \u2192\n// 580,335, about 27 seconds). warmup sits 100k below the trigger \u2014\n// comfortably more than that jump, so a session cannot leap clean over\n// warmup straight into a compaction in one turn; refresh at 40k below\n// catches a session that sat above warmup for a while; fireNow at 15k below\n// is \"no time left for a clean crossing\" and fires immediately rather than\n// waiting for one.\nexport const DEFAULT_AUTOCOMPACT_PCT = 80;\n\n/** How many of the most recent compact_boundary events to consider, and to\n * take the minimum of. */\nconst MEASURED_TRIGGER_SAMPLE_SIZE = 3;\n\nexport const THRESHOLD_MARGIN_TOKENS = {\n warmup: 100_000,\n refresh: 40_000,\n immediate: 15_000,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Measured trigger \u2014 scan a project's own transcript history for the\n// platform's own compact_boundary events, ground truth over configuration.\n// ---------------------------------------------------------------------------\n//\n// Deliberately re-implements the tiny bit of path encoding it needs (below)\n// rather than importing project-utils/paths.ts's encodePath: that module\n// chain ends in pai-paths.ts, which calls process.exit(1) if PAI_DIR does\n// not resolve to an existing directory. context-fill.ts is specifically the\n// module a hook falls back on when its environment is unreliable \u2014 pulling\n// in a dependency that can kill the process on import would defeat that.\n// Claude Code's transcript directory is a fixed, home-relative convention\n// (~/.claude/projects/<encoded-cwd>/), not something PAI_DIR governs, so\n// resolving it directly here is also just the more correct dependency, not\n// only the safer one.\n\n/** Real default \u2014 overridable per-call so tests never touch the user's\n * actual ~/.claude/projects/ (a live directory this very session writes to). */\nexport const CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\nfunction encodeProjectPath(cwd: string): string {\n return cwd.replace(/[/\\s.-]/g, \"-\");\n}\n\nexport interface CompactBoundarySample {\n preTokens: number;\n timestampMs: number;\n /** ISO string, kept alongside timestampMs so callers can print it. */\n timestamp: string;\n /** The event's own uuid when present \u2014 the dedup key. Claude Code's\n * `sessions/` archive directory mirrors the live project transcript, so\n * the SAME compact_boundary event can legitimately appear in two files;\n * without deduping by identity, \"most recent three\" can silently become\n * three copies of one event, which is a stale reading wearing a\n * plausible-looking sample size. */\n uuid?: string;\n}\n\n/** Every `.jsonl` transcript belonging to a project \u2014 top-level (the live\n * file) and `sessions/` (Claude Code's archive, which mirrors it). No file\n * is excluded and no ordering is applied here: ordering by EVENT\n * timestamp, not by file mtime, is the whole point (see\n * readCompactBoundarySamples) \u2014 a file's mtime does not reliably track\n * which events inside it are recent, and pre-filtering by mtime is exactly\n * what caused this function to return a stale, pre-regime-change trigger\n * on real data. */\nfunction listProjectTranscripts(cwd: string, projectsDir: string): string[] {\n const projectDir = join(projectsDir, encodeProjectPath(cwd));\n if (!existsSync(projectDir)) return [];\n\n const paths: string[] = [];\n const collect = (dir: string): void => {\n if (!existsSync(dir)) return;\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return;\n }\n for (const entry of entries) {\n if (entry.endsWith(\".jsonl\")) paths.push(join(dir, entry));\n }\n };\n collect(projectDir);\n collect(join(projectDir, \"sessions\"));\n return paths;\n}\n\n/**\n * Every DISTINCT compact_boundary sample found across ALL of a project's\n * transcripts (live + archived), newest first by the event's OWN timestamp\n * \u2014 never by which file it came from or that file's mtime. Deduplicated by\n * the event's uuid (falling back to a timestamp+preTokens key for the rare\n * line with no uuid) so an event mirrored into `sessions/` is counted once.\n */\nfunction readCompactBoundarySamples(cwd: string, projectsDir: string): CompactBoundarySample[] {\n const byKey = new Map<string, CompactBoundarySample>();\n\n for (const path of listProjectTranscripts(cwd, projectsDir)) {\n let raw: string;\n try {\n raw = readFileSync(path, \"utf-8\");\n } catch {\n continue;\n }\n\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n let entry: {\n type?: string;\n subtype?: string;\n timestamp?: string;\n uuid?: string;\n compactMetadata?: { preTokens?: number };\n };\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n if (entry.type !== \"system\" || entry.subtype !== \"compact_boundary\") continue;\n const preTokens = entry.compactMetadata?.preTokens;\n if (typeof preTokens !== \"number\" || !Number.isFinite(preTokens)) continue;\n const timestampMs = entry.timestamp ? Date.parse(entry.timestamp) : NaN;\n if (!Number.isFinite(timestampMs)) continue;\n\n const key = entry.uuid ?? `${timestampMs}:${preTokens}`;\n if (!byKey.has(key)) {\n byKey.set(key, { preTokens, timestampMs, timestamp: entry.timestamp!, uuid: entry.uuid });\n }\n }\n }\n\n return [...byKey.values()].sort((a, b) => b.timestampMs - a.timestampMs);\n}\n\n/**\n * The most recent MEASURED_TRIGGER_SAMPLE_SIZE distinct compact_boundary\n * events for a project, newest first \u2014 exposed on its own (not just the\n * derived minimum) so the number `measureCompactionTrigger` returns is\n * checkable: print these and the timestamps prove which three events\n * produced it, rather than asking for trust.\n */\nexport function selectedCompactionSamples(\n cwd: string,\n projectsDir: string = CLAUDE_PROJECTS_DIR\n): CompactBoundarySample[] {\n if (!cwd) return [];\n return readCompactBoundarySamples(cwd, projectsDir).slice(0, MEASURED_TRIGGER_SAMPLE_SIZE);\n}\n\n/**\n * The measured compaction trigger for a project, or null when it has no\n * compaction history yet (a brand-new project, or one whose transcripts\n * this process cannot read). Minimum of the most recent\n * MEASURED_TRIGGER_SAMPLE_SIZE DISTINCT compact_boundary events, ordered by\n * the events' own timestamps across every transcript the project has\n * (live and archived) \u2014 see the module comment above for why minimum, not\n * mean, and readCompactBoundarySamples for why \"distinct\" and \"own\n * timestamp\" both matter (a file-mtime-ordered, non-deduplicated version of\n * this returned a stale pre-regime-change trigger on real project data).\n */\nexport function measureCompactionTrigger(\n cwd: string,\n projectsDir: string = CLAUDE_PROJECTS_DIR\n): number | null {\n const samples = selectedCompactionSamples(cwd, projectsDir);\n if (samples.length === 0) return null;\n const trigger = Math.min(...samples.map((s) => s.preTokens));\n console.error(\n `[context-fill] measured trigger for ${cwd}: ${trigger} (minimum of ` +\n samples.map((s) => `${s.preTokens}@${s.timestamp}`).join(\", \") + \")\"\n );\n return trigger;\n}\n\n/**\n * Read CLAUDE_AUTOCOMPACT_PCT_OVERRIDE from the environment. Absent \u2192\n * DEFAULT_AUTOCOMPACT_PCT. Present but not a finite number in (0, 100] \u2192\n * also DEFAULT_AUTOCOMPACT_PCT, logged, so a typo in the override degrades\n * to the documented default instead of silently producing nonsense\n * thresholds (0, negative, or a fraction so large no session ever reaches\n * it).\n */\nexport function resolveAutocompactPct(env: NodeJS.ProcessEnv = process.env): number {\n const raw = env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE;\n if (raw === undefined || raw === \"\") return DEFAULT_AUTOCOMPACT_PCT;\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 100) {\n console.error(\n `[context-fill] CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=\"${raw}\" is not a usable percentage \u2014 ` +\n `falling back to the default ${DEFAULT_AUTOCOMPACT_PCT}.`\n );\n return DEFAULT_AUTOCOMPACT_PCT;\n }\n return parsed;\n}\n\n/**\n * The two thresholds that actually fire a handover job. \"immediate\" is not a\n * third job \u2014 see `isImmediate` below \u2014 it only changes how urgently a\n * crossing of these two is treated.\n */\nexport type ThresholdName = \"warmup\" | \"refresh\";\n\n/** Which basis actually produced effectiveTriggerTokens \u2014 reported so the\n * number is checkable rather than trusted. */\nexport type TriggerSource = \"measured\" | \"configured\" | \"measured-clamped\";\n\nexport interface ContextFillThresholds {\n warmupTokens: number;\n refreshTokens: number;\n immediateTokens: number;\n /** The derived compaction trigger these were measured back from \u2014\n * min(measured, configured) when a measured value exists. */\n effectiveTriggerTokens: number;\n /** The raw measured value from this project's own compact_boundary\n * history, before any clamping \u2014 null when the project has no history.\n * Kept alongside effectiveTriggerTokens so a clamp is visible rather\n * than silent: a caller can see both what was measured and what was\n * actually used. */\n measuredTriggerTokens: number | null;\n /** The raw configured-chain value (env override or default, as a\n * fraction of the window) \u2014 always computed and reported, even when it\n * wasn't what ended up being used. */\n configuredTriggerTokens: number;\n /** The autocompact percentage from the configured chain. */\n autocompactPct: number;\n /** \"measured\": a measured value existed and was <= configured, so it was\n * used directly \u2014 the better estimate, since it reflects reality the\n * configured percentage cannot know.\n * \"measured-clamped\": a measured value existed but was HIGHER than\n * configured \u2014 it reflects a regime that may no longer apply (this\n * project's last compaction predates a since-changed trigger), so the\n * lower, safer configured value was used instead.\n * \"configured\": no measured value exists yet (no compaction history for\n * this project) \u2014 the configured chain is all there is. */\n triggerSource: TriggerSource;\n /** True when these were computed against a window size Claude Code itself\n * reported (the statusline source). False when the window is only an\n * assumed default (the transcript-fallback source never learns the real\n * window size) \u2014 callers should log this: a threshold silently computed\n * against the wrong basis is the same class of fault as printing a token\n * count larger than the window. */\n windowConfirmed: boolean;\n}\n\nexport interface ContextFillThresholdOpts {\n /** The session's project directory, used to look up its own compaction\n * history for the measured trigger. Omit when unknown \u2014 falls back to\n * the configured chain, same as a project with no history yet. */\n cwd?: string;\n /** Override the measured-trigger lookup instead of scanning transcripts \u2014\n * primarily for tests. `null` forces the configured fallback even when\n * `cwd` is given; `undefined` (the default) does the real lookup. */\n measuredTrigger?: number | null;\n}\n\n/**\n * Derive warmup/refresh/immediate thresholds from a fill reading.\n *\n * effectiveTrigger = min(measured, configuredChainValue) when a measured\n * value exists \u2014 NOT the measured value outright. A project whose newest\n * compaction predates a regime change measures a stale-HIGH trigger: a real\n * case on this machine measured 998,267 for a project whose ACTUAL current\n * boundary (from a different project's fresher history, cross-checked\n * independently) is ~784,000 \u2014 using 998,267 directly would compute a\n * warm-up of 898,267, above the real boundary, so the handover would never\n * fire there. The measurement is honest; it is just old.\n *\n * The minimum is correct in both directions: the measured value is the\n * better estimate when it is LOWER than configured (it reflects reality the\n * configured percentage cannot know \u2014 see the module comment above, the\n * 100%-to-78% regime change this project itself lived through); it is\n * unsafe when it is HIGHER (it reflects a regime that no longer applies).\n * Taking the minimum costs nothing in the safe direction \u2014 one wasted\n * summary if the project's regime actually did move up \u2014 and prevents the\n * unsafe direction, where a stale-high measurement suppresses the handover\n * past the real boundary. That asymmetry is the same one the 80-not-100\n * default was chosen for.\n *\n * Margins below effectiveTrigger are clamped at 0 (and logged) for a window\n * small enough that a margin would otherwise go negative \u2014 a pathological\n * input should degrade to \"fire immediately\", never to a threshold below\n * zero.\n */\nexport function contextFillThresholds(\n reading: ContextFillReading,\n env: NodeJS.ProcessEnv = process.env,\n opts: ContextFillThresholdOpts = {}\n): ContextFillThresholds {\n const windowConfirmed = reading.source === \"statusline\";\n const autocompactPct = resolveAutocompactPct(env);\n const configuredTriggerTokens = Math.round(reading.windowSize * (autocompactPct / 100));\n\n const measuredTriggerTokens = opts.measuredTrigger !== undefined\n ? opts.measuredTrigger\n : opts.cwd\n ? measureCompactionTrigger(opts.cwd)\n : null;\n\n let effectiveTriggerTokens: number;\n let triggerSource: TriggerSource;\n\n if (measuredTriggerTokens === null) {\n effectiveTriggerTokens = configuredTriggerTokens;\n triggerSource = \"configured\";\n console.error(\n `[context-fill] trigger source: CONFIGURED \u2014 no compaction history for this project yet. ` +\n `measured=none, configured=${configuredTriggerTokens} (${autocompactPct}% of a ${reading.windowSize}-token ` +\n `window) -> using ${effectiveTriggerTokens}.`\n );\n } else if (measuredTriggerTokens <= configuredTriggerTokens) {\n effectiveTriggerTokens = measuredTriggerTokens;\n triggerSource = \"measured\";\n console.error(\n `[context-fill] trigger source: MEASURED \u2014 measured=${measuredTriggerTokens} ` +\n `(minimum of the most recent ${MEASURED_TRIGGER_SAMPLE_SIZE} compact_boundary events), ` +\n `configured=${configuredTriggerTokens} -> using ${effectiveTriggerTokens} (measured, \u2264 configured).`\n );\n } else {\n effectiveTriggerTokens = configuredTriggerTokens;\n triggerSource = \"measured-clamped\";\n console.error(\n `[context-fill] trigger source: MEASURED-CLAMPED \u2014 measured=${measuredTriggerTokens} is HIGHER than ` +\n `configured=${configuredTriggerTokens} (a stale regime this project's history predates) -> ` +\n `using ${effectiveTriggerTokens} (configured, the safer bound).`\n );\n }\n\n const clamp = (name: string, value: number): number => {\n if (value >= 0) return value;\n console.error(\n `[context-fill] ${name} threshold went negative (${value}) for a ` +\n `${reading.windowSize}-token window \u2014 clamping to 0.`\n );\n return 0;\n };\n\n return {\n warmupTokens: clamp(\"warmup\", effectiveTriggerTokens - THRESHOLD_MARGIN_TOKENS.warmup),\n refreshTokens: clamp(\"refresh\", effectiveTriggerTokens - THRESHOLD_MARGIN_TOKENS.refresh),\n immediateTokens: clamp(\"immediate\", effectiveTriggerTokens - THRESHOLD_MARGIN_TOKENS.immediate),\n effectiveTriggerTokens,\n measuredTriggerTokens,\n configuredTriggerTokens,\n autocompactPct,\n triggerSource,\n windowConfirmed,\n };\n}\n\n/**\n * Which named thresholds `usedTokens` has newly crossed, given the ones that\n * have already fired this session. Ascending order (warmup before refresh).\n *\n * A single check can return more than one name \u2014 e.g. a session first\n * observed at 99% of its window crosses warmup and refresh in the same tick,\n * because there was no earlier clean crossing to catch it at. That is the\n * \"fire immediately\" case: the caller enqueues one handover and marks every\n * newly-crossed name fired, rather than waiting for a crossing that already\n * happened.\n */\nexport function crossedThresholds(\n usedTokens: number,\n thresholds: ContextFillThresholds,\n alreadyFired: ThresholdName[]\n): ThresholdName[] {\n const fired = new Set(alreadyFired);\n const ordered: Array<[ThresholdName, number]> = [\n [\"warmup\", thresholds.warmupTokens],\n [\"refresh\", thresholds.refreshTokens],\n ];\n return ordered.filter(([name, tokens]) => !fired.has(name) && usedTokens >= tokens).map(([name]) => name);\n}\n\n/**\n * True once a session is at or above the \"no time left for a clean crossing\"\n * floor (0.985 of the window). Purely informational for callers \u2014 it never\n * gates whether `crossedThresholds` fires (a poll-based check already fires\n * any unfired threshold the moment `usedTokens` reaches it, on whatever tick\n * observes it) \u2014 but it distinguishes \"this crossed on schedule\" from \"this\n * was first observed already almost out of room\", which is worth a different\n * log line and, for a caller that queues work, a higher priority.\n */\nexport function isImmediate(usedTokens: number, thresholds: ContextFillThresholds): boolean {\n return usedTokens >= thresholds.immediateTokens;\n}\n", "/**\n * transcript-text.ts \u2014 small, pure pieces of transcript parsing that are\n * worth testing in isolation.\n *\n * Split out of context-compression-hook.ts (a hook entrypoint that calls\n * `main()` and `process.exit()` at import time, which makes it unsafe to\n * import directly from a test) so the two bug fixes that live here can be\n * covered by a real regression test rather than only a manual before/after\n * digest.\n */\n\n/**\n * Turn Claude content (string or content block array) into plain text.\n *\n * BUG (found in ~1 in 4 injected digests on this machine): a \"user\" turn is\n * not always something the user typed \u2014 a tool_result is also delivered as a\n * user-role message, and its `content` block looks like\n * `{ type: \"tool_result\", content: [...] }` where the inner `content` is\n * itself an array of blocks, not a string. The old code fell back to\n * `String(c.content)` for anything without a `.text` field, and `String()`\n * on an array of objects produces literal `\"[object Object],[object\n * Object]\"` \u2014 which is exactly what showed up under \"Recent user requests\".\n * Only ever take the `.text` of a `type: \"text\"` block; everything else\n * (tool_use, tool_result, image, and any nested content) is ignored rather\n * than stringified.\n */\nexport function contentToText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === 'string') return c;\n if (c && typeof c === 'object' && c.type === 'text' && typeof c.text === 'string') {\n return c.text;\n }\n return '';\n })\n .join(' ')\n .trim();\n }\n return '';\n}\n\n/**\n * True for a modified-file path that is noise for a handover, not signal.\n *\n * BUG: a real digest showed 8 of 10 \"Files modified\" slots filled with\n * throwaway paths under a jobs/<id>/tmp/ directory (msg8.txt, msg9.txt \u2026),\n * crowding out the two real source files also edited that session. Anything\n * under a `tmp/`, `jobs/`, or `scratchpad/` path segment is excluded \u2014 those\n * are working scratch space, not the codebase the next session needs to\n * remember it touched.\n */\nexport function isNoiseFilePath(filePath: string, scratchpadDir?: string): boolean {\n if (scratchpadDir && filePath.startsWith(scratchpadDir)) return true;\n const segments = filePath.split('/');\n return segments.includes('tmp') || segments.includes('jobs') || segments.includes('scratchpad');\n}\n\n/**\n * Order a modified-file list so entries inside `cwd` are preferred when a\n * caller has to truncate to a fixed slot count \u2014 an unrelated repo touched\n * in passing should not crowd out the files that actually matter for this\n * session's working directory.\n */\nexport function preferCwdFiles(files: string[], cwd?: string): string[] {\n if (!cwd) return files;\n const inCwd = files.filter((f) => f.startsWith(cwd));\n const outCwd = files.filter((f) => !f.startsWith(cwd));\n return [...inCwd, ...outCwd];\n}\n", "/**\n * context-handover-cache.ts \u2014 the well-known file where the threshold-\n * triggered pre-compaction handover (see ../../../daemon/context-handover-\n * worker.ts) is written, and where context-compression-hook.ts looks for it\n * at compaction time.\n *\n * Deliberately dependency-light (fs/os/path only). The daemon worker that\n * WRITES this file needs the heavier machinery in session-summary-worker.ts\n * (spawning Claude, git log, etc.), but the PreCompact hook that READS it\n * runs as a short-lived process on every compaction and must not drag in\n * daemon state, database pools, or anything else that module graph carries \u2014\n * this file is the shared seam so neither side has to.\n */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport type HandoverThreshold = \"warmup\" | \"refresh\";\n\nexport interface ContextHandoverCache {\n sessionId: string;\n cwd: string;\n threshold: HandoverThreshold;\n generatedAt: string; // ISO timestamp\n model: string;\n summary: string;\n}\n\nexport function contextHandoverCachePath(sessionId: string): string {\n return join(tmpdir(), `pai-context-handover-${sessionId}.json`);\n}\n\nexport function readContextHandoverCache(sessionId: string): ContextHandoverCache | null {\n const path = contextHandoverCachePath(sessionId);\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf-8\")) as ContextHandoverCache;\n } catch {\n return null;\n }\n}\n\nexport function writeContextHandoverCache(cache: ContextHandoverCache): void {\n writeFileSync(contextHandoverCachePath(cache.sessionId), JSON.stringify(cache, null, 2), \"utf-8\");\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["existsSync", "readFileSync", "writeFileSync", "basename", "join", "tmpdir", "existsSync", "join", "join", "existsSync", "join", "existsSync", "existsSync", "readFileSync", "join", "homedir", "resolve", "existsSync", "mkdirSync", "readdirSync", "readFileSync", "join", "basename", "content", "existsSync", "join", "basename", "existsSync", "mkdirSync", "readFileSync", "writeFileSync", "join", "basename", "existsSync", "readFileSync", "writeFileSync", "readdirSync", "renameSync", "mkdirSync", "join", "join", "existsSync", "readFileSync", "mkdirSync", "writeFileSync", "renameSync", "existsSync", "existsSync", "join", "mkdirSync", "writeFileSync", "readFileSync", "writeFileSync", "existsSync", "readdirSync", "readFileSync", "homedir", "join", "join", "homedir", "existsSync", "readFileSync", "writeFileSync", "tmpdir", "join", "readFileSync", "basename", "join", "existsSync", "writeFileSync", "resolve", "tmpdir"]
|
|
3
|
+
"sources": ["../../../src/hooks/ts/lib/worker-session.ts", "../../../src/hooks/ts/pre-compact/context-compression-hook.ts", "../../../src/hooks/ts/lib/project-utils/paths.ts", "../../../src/hooks/ts/lib/pai-paths.ts", "../../../src/hooks/ts/lib/project-utils/notify.ts", "../../../src/hooks/ts/lib/project-utils/session-notes.ts", "../../../src/hooks/ts/lib/project-utils/todo.ts", "../../../src/session/checkpoint-block.ts", "../../../src/hooks/ts/lib/context-fill.ts", "../../../src/hooks/ts/lib/transcript-text.ts", "../../../src/hooks/ts/lib/context-handover-cache.ts", "../../../src/hooks/ts/lib/handover-evidence.ts"],
|
|
4
|
+
"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 * PreCompact Hook - Triggered before context compression\n *\n * Three critical jobs:\n * 1. Save rich checkpoint to session note \u2014 work items, state, meaningful rename\n * 2. Update TODO.md with a proper ## Continue section for the next session\n * 3. Save session state to temp file for post-compact injection via SessionStart(compact)\n *\n * Uses a CUMULATIVE state file (.compact-state.json) that persists across\n * compactions. This ensures that even after multiple compactions (where the\n * transcript becomes thin), we still have rich data for titles, summaries,\n * and work items from earlier in the session.\n */\n\nimport { isWorkerSession } from \"../lib/worker-session.js\";\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { basename, dirname, join } from 'path';\nimport { tmpdir } from 'os';\nimport { connect } from 'net';\nimport { randomUUID } from 'crypto';\nimport {\n sendNtfyNotification,\n getCurrentNotePath,\n createSessionNote,\n appendCheckpoint,\n addWorkToSessionNote,\n isMeaningfulTitle,\n findNotesDir,\n renameSessionNote,\n updateTodoContinue,\n isProbeSession,\n WorkItem,\n} from '../lib/project-utils';\nimport { getContextFill, formatContextFill } from '../lib/context-fill.js';\nimport { contentToText, isNoiseFilePath, preferCwdFiles } from '../lib/transcript-text.js';\nimport { readContextHandoverCache } from '../lib/context-handover-cache.js';\nimport { bindHandoverEvidence } from '../lib/handover-evidence.js';\n\ninterface HookInput {\n session_id: string;\n transcript_path: string;\n cwd?: string;\n hook_event_name: string;\n compact_type?: string;\n trigger?: string;\n /** Session's scratchpad directory, when the harness provides one \u2014 used to\n * exclude scratch files from the \"Files modified\" list (see\n * isNoiseFilePath). */\n scratchpad_dir?: string;\n}\n\nconst DAEMON_SOCKET = process.env.PAI_SOCKET ?? '/tmp/pai.sock';\nconst DAEMON_TIMEOUT_MS = 3_000;\n\n/** Structured data extracted from a transcript in a single pass. */\ninterface TranscriptData {\n userMessages: string[];\n summaries: string[];\n captures: string[];\n lastCompleted: string;\n filesModified: string[];\n /** Count of modified-file paths dropped as noise (tmp/jobs/scratchpad). */\n filesExcluded: number;\n workItems: WorkItem[];\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n//\n// contentToText, isNoiseFilePath, and preferCwdFiles live in\n// ../lib/transcript-text.ts, not here \u2014 this file calls `main()` and\n// `process.exit()` at import time, which makes it unsafe to import from a\n// test. Splitting the pure, bug-fixed logic out is what lets those two bugs\n// (the \"[object Object]\" content bug and the tmp/jobs noise-crowding bug)\n// carry a real regression test instead of only a manual before/after digest.\n\nfunction getTranscriptStats(transcriptPath: string): { messageCount: number; isLarge: boolean } {\n try {\n const content = readFileSync(transcriptPath, 'utf-8');\n const lines = content.trim().split('\\n');\n let userMessages = 0;\n let assistantMessages = 0;\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const entry = JSON.parse(line);\n if (entry.type === 'user') userMessages++;\n else if (entry.type === 'assistant') assistantMessages++;\n } catch { /* skip */ }\n }\n const totalMessages = userMessages + assistantMessages;\n return { messageCount: totalMessages, isLarge: totalMessages > 50 };\n } catch {\n return { messageCount: 0, isLarge: false };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Unified transcript parser \u2014 single pass extracts everything\n// ---------------------------------------------------------------------------\n\nfunction parseTranscript(transcriptPath: string, scratchpadDir?: string): TranscriptData {\n const data: TranscriptData = {\n userMessages: [],\n summaries: [],\n captures: [],\n lastCompleted: '',\n filesModified: [],\n filesExcluded: 0,\n workItems: [],\n };\n\n try {\n const raw = readFileSync(transcriptPath, 'utf-8');\n const lines = raw.trim().split('\\n');\n const seenSummaries = new Set<string>();\n\n for (const line of lines) {\n if (!line.trim()) continue;\n let entry: any;\n try { entry = JSON.parse(line); } catch { continue; }\n\n // --- User messages ---\n if (entry.type === 'user' && entry.message?.content) {\n const text = contentToText(entry.message.content).slice(0, 300);\n if (text) data.userMessages.push(text);\n }\n\n // --- Assistant content ---\n if (entry.type === 'assistant' && entry.message?.content) {\n const text = contentToText(entry.message.content);\n\n // Summaries \u2192 also create work items\n const summaryMatch = text.match(/SUMMARY:\\s*(.+?)(?:\\n|$)/i);\n if (summaryMatch) {\n const s = summaryMatch[1].trim();\n if (s.length > 5 && !data.summaries.includes(s)) {\n data.summaries.push(s);\n if (!seenSummaries.has(s)) {\n seenSummaries.add(s);\n const details: string[] = [];\n const actionsMatch = text.match(/ACTIONS:\\s*(.+?)(?=\\n[A-Z]+:|$)/is);\n if (actionsMatch) {\n const actionLines = actionsMatch[1].split('\\n')\n .map(l => l.replace(/^[-*\u2022]\\s*/, '').replace(/^\\d+\\.\\s*/, '').trim())\n .filter(l => l.length > 3 && l.length < 100);\n details.push(...actionLines.slice(0, 3));\n }\n data.workItems.push({ title: s, details: details.length > 0 ? details : undefined, completed: true });\n }\n }\n }\n\n // Captures\n const captureMatch = text.match(/CAPTURE:\\s*(.+?)(?:\\n|$)/i);\n if (captureMatch) {\n const c = captureMatch[1].trim();\n if (c.length > 5 && !data.captures.includes(c)) data.captures.push(c);\n }\n\n // Completed\n const completedMatch = text.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n if (completedMatch) {\n data.lastCompleted = completedMatch[1].trim().replace(/\\*+/g, '');\n if (data.workItems.length === 0 && !seenSummaries.has(data.lastCompleted) && data.lastCompleted.length > 5) {\n seenSummaries.add(data.lastCompleted);\n data.workItems.push({ title: data.lastCompleted, completed: true });\n }\n }\n\n // File modifications (from tool_use blocks)\n if (Array.isArray(entry.message.content)) {\n for (const block of entry.message.content) {\n if (block.type === 'tool_use') {\n const tool = block.name;\n if ((tool === 'Edit' || tool === 'Write') && block.input?.file_path) {\n const fp = block.input.file_path;\n if (isNoiseFilePath(fp, scratchpadDir)) {\n data.filesExcluded++;\n } else if (!data.filesModified.includes(fp)) {\n data.filesModified.push(fp);\n }\n }\n }\n }\n }\n }\n }\n } catch (err) {\n console.error(`parseTranscript error: ${err}`);\n }\n\n return data;\n}\n\n// ---------------------------------------------------------------------------\n// Format session state as human-readable string\n// ---------------------------------------------------------------------------\n\nfunction formatSessionState(data: TranscriptData, cwd?: string): string | null {\n const parts: string[] = [];\n\n if (cwd) parts.push(`Working directory: ${cwd}`);\n\n const recentUser = data.userMessages.slice(-3);\n if (recentUser.length > 0) {\n parts.push('\\nRecent user requests:');\n for (const msg of recentUser) {\n parts.push(`- ${msg.split('\\n')[0].slice(0, 200)}`);\n }\n }\n\n const recentSummaries = data.summaries.slice(-3);\n if (recentSummaries.length > 0) {\n parts.push('\\nWork summaries:');\n for (const s of recentSummaries) parts.push(`- ${s.slice(0, 150)}`);\n }\n\n const recentCaptures = data.captures.slice(-5);\n if (recentCaptures.length > 0) {\n parts.push('\\nCaptured context:');\n for (const c of recentCaptures) parts.push(`- ${c.slice(0, 150)}`);\n }\n\n // Noise (tmp/jobs/scratchpad) is already excluded at collection time \u2014 see\n // isNoiseFilePath. Here, prefer files inside the session's own working\n // directory when there are more than fit in the slice, so an unrelated\n // repo touched in passing doesn't crowd out the files that matter.\n const files = preferCwdFiles(data.filesModified, cwd).slice(-10);\n if (files.length > 0) {\n parts.push('\\nFiles modified this session:');\n for (const f of files) parts.push(`- ${f}`);\n } else if (data.filesExcluded > 0) {\n parts.push(\n `\\nFiles modified this session: none in the working directory ` +\n `(excluded ${data.filesExcluded} noise path(s) under tmp/jobs/scratchpad)`\n );\n }\n\n if (data.lastCompleted) {\n parts.push(`\\nLast completed: ${data.lastCompleted.slice(0, 150)}`);\n }\n\n const result = parts.join('\\n');\n return result.length > 50 ? result : null;\n}\n\n// ---------------------------------------------------------------------------\n// Derive a meaningful title for the session note\n// ---------------------------------------------------------------------------\n\nfunction deriveTitle(data: TranscriptData): string {\n // Collect candidates in priority order, then pick the first meaningful one.\n const candidates: string[] = [];\n\n // 1. Work item titles (most descriptive of what was accomplished)\n for (let i = data.workItems.length - 1; i >= 0; i--) {\n candidates.push(data.workItems[i].title);\n }\n\n // 2. Summaries\n for (let i = data.summaries.length - 1; i >= 0; i--) {\n candidates.push(data.summaries[i]);\n }\n\n // 3. Last completed marker\n if (data.lastCompleted && data.lastCompleted.length > 5) {\n candidates.push(data.lastCompleted);\n }\n\n // 4. User messages (FIRST meaningful one, not last \u2014 first is more likely\n // to describe the session's purpose; last is often system noise)\n for (const msg of data.userMessages) {\n const line = msg.split('\\n')[0].trim();\n if (line.length > 10 && line.length < 80 &&\n !line.toLowerCase().startsWith('yes') &&\n !line.toLowerCase().startsWith('ok')) {\n candidates.push(line);\n }\n }\n\n // 5. Derive from files modified (fallback)\n if (data.filesModified.length > 0) {\n const basenames = data.filesModified.slice(-5).map(f => {\n const b = basename(f);\n return b.replace(/\\.[^.]+$/, '');\n });\n const unique = [...new Set(basenames)];\n candidates.push(\n unique.length <= 3\n ? `Updated ${unique.join(', ')}`\n : `Modified ${data.filesModified.length} files`\n );\n }\n\n // Pick the first candidate that passes the meaningfulness filter\n for (const raw of candidates) {\n const cleaned = raw\n .replace(/[^\\w\\s-]/g, ' ') // Remove special chars\n .replace(/\\s+/g, ' ') // Normalize whitespace\n .trim()\n .substring(0, 60);\n if (cleaned.length >= 5 && isMeaningfulTitle(cleaned)) {\n return cleaned;\n }\n }\n\n // All candidates were garbage \u2014 return empty (caller will not rename)\n return '';\n}\n\n// ---------------------------------------------------------------------------\n// Cumulative state \u2014 persists across compactions in .compact-state.json\n// ---------------------------------------------------------------------------\n\nconst CUMULATIVE_STATE_FILE = '.compact-state.json';\n\nfunction loadCumulativeState(notesDir: string): TranscriptData | null {\n try {\n const filePath = join(notesDir, CUMULATIVE_STATE_FILE);\n if (!existsSync(filePath)) return null;\n const raw = JSON.parse(readFileSync(filePath, 'utf-8'));\n return {\n userMessages: raw.userMessages || [],\n summaries: raw.summaries || [],\n captures: raw.captures || [],\n lastCompleted: raw.lastCompleted || '',\n filesModified: raw.filesModified || [],\n filesExcluded: raw.filesExcluded || 0,\n workItems: raw.workItems || [],\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Peek at `.compact-state.json`'s `lastUpdated` \u2014 the timestamp of THIS\n * session's previous compaction \u2014 without pulling it into TranscriptData's\n * contract. Read before `saveCumulativeState` overwrites the file for the\n * current compaction, so callers can tell \"a cached handover generated\n * since the last compaction\" from \"a cached handover left over from before\n * it, now stale\". Returns null when there is no previous compaction (the\n * first one this session) \u2014 treated as \"any cached handover is fresh\".\n */\nfunction readPreviousCompactionTimestamp(notesDir: string): string | null {\n try {\n const filePath = join(notesDir, CUMULATIVE_STATE_FILE);\n if (!existsSync(filePath)) return null;\n const raw = JSON.parse(readFileSync(filePath, 'utf-8'));\n return typeof raw.lastUpdated === 'string' ? raw.lastUpdated : null;\n } catch {\n return null;\n }\n}\n\nfunction mergeTranscriptData(accumulated: TranscriptData | null, current: TranscriptData): TranscriptData {\n if (!accumulated) return current;\n\n const mergeArrays = (a: string[], b: string[]): string[] => {\n const seen = new Set(a);\n return [...a, ...b.filter(x => !seen.has(x))];\n };\n\n const seenTitles = new Set(accumulated.workItems.map(w => w.title));\n const newWorkItems = current.workItems.filter(w => !seenTitles.has(w.title));\n\n return {\n userMessages: mergeArrays(accumulated.userMessages, current.userMessages).slice(-20),\n summaries: mergeArrays(accumulated.summaries, current.summaries),\n captures: mergeArrays(accumulated.captures, current.captures),\n lastCompleted: current.lastCompleted || accumulated.lastCompleted,\n filesModified: mergeArrays(accumulated.filesModified, current.filesModified),\n filesExcluded: accumulated.filesExcluded + current.filesExcluded,\n workItems: [...accumulated.workItems, ...newWorkItems],\n };\n}\n\nfunction saveCumulativeState(notesDir: string, data: TranscriptData, notePath: string | null): void {\n try {\n const filePath = join(notesDir, CUMULATIVE_STATE_FILE);\n writeFileSync(filePath, JSON.stringify({\n ...data,\n notePath,\n lastUpdated: new Date().toISOString(),\n }, null, 2));\n console.error(`Cumulative state saved (${data.workItems.length} work items, ${data.filesModified.length} files)`);\n } catch (err) {\n console.error(`Failed to save cumulative state: ${err}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Daemon IPC \u2014 enqueue registry-scan work item\n// ---------------------------------------------------------------------------\n\n/**\n * Enqueue a registry-scan work item with the daemon.\n * Fire-and-forget \u2014 never throws, never blocks the pre-compact hook.\n */\nfunction enqueueRegistryScan(): Promise<void> {\n return new Promise((resolve) => {\n let done = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(): void {\n if (done) return;\n done = true;\n if (timer !== null) { clearTimeout(timer); timer = null; }\n try { client.destroy(); } catch { /* ignore */ }\n resolve();\n }\n\n const client = connect(DAEMON_SOCKET, () => {\n const msg = JSON.stringify({\n id: randomUUID(),\n method: 'work_queue_enqueue',\n params: {\n type: 'registry-scan',\n priority: 5,\n payload: {},\n },\n }) + '\\n';\n client.write(msg);\n });\n\n client.on('data', () => finish());\n client.on('error', () => finish());\n client.on('end', () => finish());\n timer = setTimeout(() => finish(), DAEMON_TIMEOUT_MS);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Daemon IPC \u2014 enqueue session-summary work item\n// ---------------------------------------------------------------------------\n\n/**\n * Send a session-summary work item to the daemon via IPC.\n * Returns true on success, false if the daemon is unreachable.\n * Times out after DAEMON_TIMEOUT_MS to avoid blocking the hook.\n */\nfunction enqueueSessionSummary(payload: {\n cwd: string;\n sessionId?: string;\n transcriptPath?: string;\n}): Promise<boolean> {\n return new Promise((resolve) => {\n let done = false;\n let buffer = '';\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(ok: boolean): void {\n if (done) return;\n done = true;\n if (timer !== null) { clearTimeout(timer); timer = null; }\n try { client.destroy(); } catch { /* ignore */ }\n resolve(ok);\n }\n\n const client = connect(DAEMON_SOCKET, () => {\n const msg = JSON.stringify({\n id: randomUUID(),\n method: 'work_queue_enqueue',\n params: {\n type: 'session-summary',\n priority: 4, // lower priority than session-end (2)\n payload: {\n cwd: payload.cwd,\n sessionId: payload.sessionId,\n transcriptPath: payload.transcriptPath,\n },\n },\n }) + '\\n';\n client.write(msg);\n });\n\n client.on('data', (chunk: Buffer) => {\n buffer += chunk.toString();\n const nl = buffer.indexOf('\\n');\n if (nl === -1) return;\n const line = buffer.slice(0, nl);\n try {\n const response = JSON.parse(line) as { ok: boolean; error?: string; result?: { id: string } };\n if (response.ok) {\n console.error(`PRE-COMPACT: Session summary enqueued with daemon (id=${response.result?.id}).`);\n finish(true);\n } else {\n console.error(`PRE-COMPACT: Daemon rejected session-summary: ${response.error}`);\n finish(false);\n }\n } catch {\n finish(false);\n }\n });\n\n client.on('error', (e: NodeJS.ErrnoException) => {\n if (e.code === 'ENOENT' || e.code === 'ECONNREFUSED') {\n console.error('PRE-COMPACT: Daemon not running \u2014 skipping session summary.');\n } else {\n console.error(`PRE-COMPACT: Daemon socket error: ${e.message}`);\n }\n finish(false);\n });\n\n timer = setTimeout(() => {\n console.error('PRE-COMPACT: Daemon IPC timed out \u2014 skipping session summary.');\n finish(false);\n }, DAEMON_TIMEOUT_MS);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\nasync function main() {\n if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping\n // Skip probe/health-check sessions (e.g. CodexBar ClaudeProbe)\n if (isProbeSession()) {\n process.exit(0);\n }\n\n let hookInput: HookInput | null = null;\n\n try {\n const decoder = new TextDecoder();\n let input = '';\n const timeoutPromise = new Promise<void>((resolve) => { setTimeout(resolve, 500); });\n const readPromise = (async () => {\n for await (const chunk of process.stdin) {\n input += decoder.decode(chunk, { stream: true });\n }\n })();\n await Promise.race([readPromise, timeoutPromise]);\n if (input.trim()) {\n hookInput = JSON.parse(input) as HookInput;\n }\n } catch {\n // Silently handle input errors\n }\n\n const compactType = hookInput?.compact_type || hookInput?.trigger || 'auto';\n let ntfyTokenDisplay: string | null = null;\n\n if (hookInput?.transcript_path) {\n const stats = getTranscriptStats(hookInput.transcript_path);\n\n // BUG (real headers pulled from this machine's history): \"~995073k\n // tokens\", \"~2135399k tokens\" \u2014 995 million and 2.1 billion. The old\n // source, calculateSessionTokens, sums usage across EVERY line in the\n // transcript, which is a lifetime token-spend counter, not a context\n // fill reading \u2014 it necessarily exceeds the window on any long session\n // and is not what \"context compression triggered at ~X tokens\" means.\n // getContextFill reads the most recent usage entry only (or the\n // statusline's own reading), which is the actual current fill and can\n // never legitimately exceed the window. formatContextFill still\n // clamps-and-flags rather than trust that, and reports \"unknown\"\n // instead of guessing when neither source has an answer.\n const fill = getContextFill({\n sessionId: hookInput.session_id,\n transcriptPath: hookInput.transcript_path,\n });\n const tokenDisplay = formatContextFill(fill).text;\n ntfyTokenDisplay = tokenDisplay;\n\n // -----------------------------------------------------------------\n // Single-pass transcript parsing + cumulative state merge\n // -----------------------------------------------------------------\n const data = parseTranscript(hookInput.transcript_path, hookInput.scratchpad_dir);\n\n // Find notes directory early \u2014 needed for cumulative state\n let notesInfo: { path: string; isLocal: boolean };\n try {\n notesInfo = hookInput.cwd\n ? findNotesDir(hookInput.cwd)\n : { path: join(dirname(hookInput.transcript_path), 'Notes'), isLocal: false };\n } catch {\n notesInfo = { path: join(dirname(hookInput.transcript_path), 'Notes'), isLocal: false };\n }\n\n // Load accumulated state from previous compactions and merge\n const accumulated = loadCumulativeState(notesInfo.path);\n // Read BEFORE saveCumulativeState (below) overwrites this file for the\n // CURRENT compaction \u2014 this is the previous one's timestamp, the\n // freshness line a cached handover has to clear (see the injection step).\n const previousCompactionAt = readPreviousCompactionTimestamp(notesInfo.path);\n const merged = mergeTranscriptData(accumulated, data);\n const state = formatSessionState(merged, hookInput.cwd);\n\n if (accumulated) {\n console.error(`Loaded cumulative state: ${accumulated.workItems.length} work items, ${accumulated.filesModified.length} files from previous compaction(s)`);\n }\n\n // -----------------------------------------------------------------\n // Persist session state to numbered session note.\n // RULE: ONE note per session. NEVER create a new note during compaction.\n // If the latest note is completed, reopen it (remove completed status)\n // rather than creating a duplicate.\n // -----------------------------------------------------------------\n let notePath: string | null = null;\n\n try {\n notePath = getCurrentNotePath(notesInfo.path);\n\n if (!notePath) {\n // Truly no note exists at all \u2014 create one (first compaction of a session\n // that started before PAI was installed, or corrupted notes dir)\n console.error('No session note found \u2014 creating one for checkpoint');\n notePath = createSessionNote(notesInfo.path, 'Untitled Session');\n } else {\n // If the latest note is completed, reopen it for this session's checkpoints\n // instead of creating a duplicate. This handles the case where session-stop\n // finalized a note but the session continued (e.g., user said \"end session\"\n // but kept working).\n try {\n let noteContent = readFileSync(notePath, 'utf-8');\n if (noteContent.includes('**Status:** Completed')) {\n noteContent = noteContent.replace('**Status:** Completed', '**Status:** In Progress');\n writeFileSync(notePath, noteContent);\n console.error(`Reopened completed note for continued session: ${basename(notePath)}`);\n }\n } catch { /* proceed with existing note */ }\n }\n\n // 1. Write rich checkpoint with full session state\n const checkpointBody = state\n ? `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.\\n\\n${state}`\n : `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.`;\n appendCheckpoint(notePath, checkpointBody);\n\n // 2. Write work items to \"Work Done\" section (uses merged cumulative data)\n if (merged.workItems.length > 0) {\n addWorkToSessionNote(notePath, merged.workItems, `Pre-Compact (~${tokenDisplay} tokens)`);\n console.error(`Added ${merged.workItems.length} work item(s) to session note`);\n }\n\n // 3. Rename session note with a meaningful title (uses merged data for richer titles)\n const title = deriveTitle(merged);\n if (title) {\n const newPath = renameSessionNote(notePath, title);\n if (newPath !== notePath) {\n // Update H1 title inside the note to match\n try {\n let noteContent = readFileSync(newPath, 'utf-8');\n noteContent = noteContent.replace(\n /^(# Session \\d+:)\\s*.*$/m,\n `$1 ${title}`\n );\n writeFileSync(newPath, noteContent);\n console.error(`Updated note H1 to match rename`);\n } catch { /* ignore */ }\n notePath = newPath;\n }\n }\n\n console.error(`Rich checkpoint saved: ${basename(notePath)}`);\n } catch (noteError) {\n console.error(`Could not save checkpoint: ${noteError}`);\n }\n\n // Save cumulative state for next compaction\n saveCumulativeState(notesInfo.path, merged, notePath);\n\n // -----------------------------------------------------------------\n // Update TODO.md with proper ## Continue section (like \"pause session\")\n // -----------------------------------------------------------------\n if (hookInput.cwd && notePath) {\n try {\n const noteFilename = basename(notePath);\n updateTodoContinue(hookInput.cwd, noteFilename, state, tokenDisplay);\n console.error('TODO.md ## Continue section updated');\n } catch (todoError) {\n console.error(`Could not update TODO.md: ${todoError}`);\n }\n }\n\n // -----------------------------------------------------------------------\n // Save session state to temp file for post-compact injection.\n //\n // PreCompact hooks have NO stdout support (Claude Code ignores it).\n // Instead, we write the injection payload to a temp file keyed by\n // session_id. The SessionStart(compact) hook reads it and outputs\n // to stdout, which IS injected into the post-compaction context.\n //\n // Always fires (even with thin state) \u2014 includes note path so the AI\n // can enrich the session note post-compaction using its own context.\n // -----------------------------------------------------------------------\n if (hookInput.session_id) {\n const stateText = state || `Working directory: ${hookInput.cwd || 'unknown'}`;\n const noteInfo = notePath\n ? `\\nSESSION NOTE: ${notePath}\\nIf this note still has a generic title (e.g. \"New Session\", \"Context Compression\"),\\nrename it based on actual work done and add a rich summary.`\n : '';\n\n // -------------------------------------------------------------------\n // ADDITIVE: a model-written handover (decisions/reasoning/open threads\n // \u2014 see the threshold-triggered context-handover-worker) sits ALONGSIDE\n // the mechanical scrape above, never replaces it. The scrape is the\n // floor and stays intact on every path, so this is never worse than\n // the pre-existing behaviour \u2014 only sometimes better. \"Fresh\" means\n // generated after this session's PREVIOUS compaction: a handover\n // cached before that point describes state the last compaction\n // already accounted for, not what has happened since.\n //\n // BUG (live): a production digest was found with NO \"HANDOVER SOURCE:\"\n // line at all \u2014 its cause was never pinned down for certain (the\n // deployed hook is a compiled dist/ artifact reached through\n // ${PAI_DIR}/Hooks/..., a separate build step from editing this\n // source, which is itself a plausible way for \"works when I run the\n // .ts source\" and \"missing in production\" to diverge). Regardless of\n // cause: sourceLabel and handoverBlock are now computed in their own\n // try/catch, so ANY failure in the cache read or the freshness\n // comparison degrades to an explicit error label rather than being\n // capable of preventing the line from existing at all.\n // -------------------------------------------------------------------\n let sourceLabel: string;\n let handoverBlock: string;\n let handoverSummary: string | null = null;\n try {\n const cachedHandover = readContextHandoverCache(hookInput.session_id);\n const handoverIsFresh =\n cachedHandover !== null &&\n (!previousCompactionAt || new Date(cachedHandover.generatedAt) > new Date(previousCompactionAt));\n handoverSummary = handoverIsFresh ? cachedHandover!.summary : null;\n\n sourceLabel = handoverIsFresh\n ? `model-written handover (${cachedHandover!.model}, generated ${cachedHandover!.generatedAt}, ` +\n `threshold=${cachedHandover!.threshold}) + mechanical scrape`\n : 'mechanical scrape only (no fresh model-written handover was available)';\n\n handoverBlock = handoverIsFresh\n ? [\n '',\n '--- MODEL-WRITTEN HANDOVER (decisions, reasoning, open threads \u2014 not in the scrape above) ---',\n cachedHandover!.summary,\n '--- end model-written handover ---',\n ].join('\\n')\n : '';\n } catch (err) {\n console.error(`Failed to resolve handover cache \u2014 falling back to scrape-only: ${err}`);\n sourceLabel = `mechanical scrape only (error resolving handover cache: ${err})`;\n handoverBlock = '';\n }\n\n // Evidence binding: which of the handover's identifiers the transcript\n // actually contains. One footer line, content untouched; its own\n // failure changes nothing else about the injection.\n if (handoverSummary !== null && typeof hookInput.transcript_path === 'string') {\n try {\n const evidence = bindHandoverEvidence(handoverSummary, null, [hookInput.transcript_path]);\n handoverBlock += `\\n${evidence.footer}`;\n } catch (err) {\n console.error(`Evidence check skipped: ${err}`);\n }\n }\n\n const injection = [\n '<system-reminder>',\n `SESSION STATE RECOVERED AFTER COMPACTION (${compactType}, ~${tokenDisplay} tokens)`,\n `HANDOVER SOURCE: ${sourceLabel}`,\n '',\n stateText,\n noteInfo,\n handoverBlock,\n '',\n 'IMPORTANT: This session state was captured before context compaction.',\n 'Use it to maintain continuity. Continue the conversation from where',\n 'it left off without asking the user to repeat themselves.',\n 'Continue with the last task that you were asked to work on.',\n '</system-reminder>',\n ].join('\\n');\n\n try {\n const stateFile = join(tmpdir(), `pai-compact-state-${hookInput.session_id}.txt`);\n writeFileSync(stateFile, injection, 'utf-8');\n console.error(`Session state saved to ${stateFile} (${injection.length} chars)`);\n } catch (err) {\n console.error(`Failed to save state file: ${err}`);\n }\n }\n }\n\n // -----------------------------------------------------------------------\n // Enqueue session-summary work item with daemon for AI-powered note generation\n // -----------------------------------------------------------------------\n if (hookInput?.cwd) {\n try {\n await enqueueSessionSummary({\n cwd: hookInput.cwd,\n sessionId: hookInput.session_id,\n transcriptPath: hookInput.transcript_path,\n });\n } catch (err) {\n console.error(`Could not enqueue session-summary: ${err}`);\n }\n }\n\n // -----------------------------------------------------------------------\n // Enqueue registry-scan so pai session recent stays fresh after compaction\n // -----------------------------------------------------------------------\n try {\n await enqueueRegistryScan();\n } catch { /* non-fatal */ }\n\n // Send ntfy.sh notification\n const ntfyMessage = ntfyTokenDisplay && ntfyTokenDisplay !== 'unknown'\n ? `Auto-pause: ~${ntfyTokenDisplay} tokens`\n : 'Context compressing';\n await sendNtfyNotification(ntfyMessage);\n\n process.exit(0);\n}\n\nmain().catch(() => {\n process.exit(0);\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 { PAI_DIR } from '../pai-paths.js';\n\n// Re-export PAI_DIR for consumers\nexport { PAI_DIR };\nexport const PROJECTS_DIR = join(PAI_DIR, 'projects');\n\n/**\n * Directories known to be automated health-check / probe sessions.\n * Hooks should exit early for these to avoid registry clutter and wasted work.\n */\nconst PROBE_CWD_PATTERNS = [\n '/CodexBar/ClaudeProbe',\n '/ClaudeProbe',\n];\n\n/**\n * Check if the current working directory belongs to a probe/health-check session.\n * Returns true if hooks should skip this session entirely.\n */\nexport function isProbeSession(cwd?: string): boolean {\n const dir = cwd || process.cwd();\n return PROBE_CWD_PATTERNS.some(pattern => dir.includes(pattern));\n}\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with -\n * - Replace space with -\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-')\n .replace(/\\./g, '-')\n .replace(/ /g, '-');\n}\n\n/** Get the project directory for a given working directory. */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/** Get the Notes directory for a project (central location). */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory \u2014 checks local first, falls back to central.\n * Does NOT create the directory.\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n const localPaths = [\n join(cwd, 'Notes'),\n join(cwd, 'notes'),\n join(cwd, '.claude', 'Notes'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return { path, isLocal: true };\n }\n }\n\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/** Get the sessions/ directory for a project (stores .jsonl transcripts). */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/** Get the sessions/ directory from a project directory path. */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n// ---------------------------------------------------------------------------\n// Directory creation helpers\n// ---------------------------------------------------------------------------\n\n/** Ensure the Notes directory exists for a project. @deprecated Use ensureNotesDirSmart() */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\n return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists \u2192 use it (don't create anything new)\n * - If no local Notes/ \u2192 ensure central exists and use that\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n if (found.isLocal) return found;\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n return found;\n}\n\n/** Ensure the sessions/ directory exists for a project. */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/** Ensure the sessions/ directory exists (from project dir path). */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/**\n * Publish every project-root .jsonl into sessions/ as well, WITHOUT removing it.\n *\n * This used to renameSync, and that is how PAI destroyed its users' sessions.\n *\n * `claude --resume <uuid>` finds a transcript only at the project root. Move it\n * into sessions/ and the session becomes permanently unresumable \u2014 measured\n * 2026-08-04: `claude --resume b3462801` (867 KB of real work, sessions/ only)\n * answers \"No conversation found with session ID\", while a top-level id is found\n * fine. Nothing warned; the id still looked valid everywhere PAI displayed it.\n *\n * The damage was not occasional. This ran from a UserPromptSubmit hook excluding\n * only the CURRENT session, so every prompt anyone typed unresumed every other\n * session in the project. One PAI project measured 1 transcript at top level\n * against 52 underneath. Among the casualties was 046bb712 \u2014 the exact id PAI's\n * own handover tells the user to resume.\n *\n * A hardlink satisfies both sides, which is why this is a two-line fix rather\n * than a redesign: the archive genuinely has consumers that read sessions/\n * (session-summary-worker, registry/moved, session/autosave), and `--resume`\n * needs the root path. One inode, two names, no copy, no window where the file\n * is missing from either place.\n *\n * Never unlink the source. Tidying up another tool's store was the whole\n * mistake; a stale duplicate is free, a lost session is not. Every caller of\n * `transcriptFiles()` was checked before choosing this \u2014 they all test emptiness\n * (`.length > 0`), never count, so the duplicate cannot skew a project's stats.\n *\n * `excludeFile` keeps a hook from archiving the transcript it is itself watching\n * being written. It is not a \"finished sessions only\" guard and must not be read\n * as one: it excludes exactly one file, the caller's own, so with two sessions\n * live in one project each still archives the other mid-turn. A consumer that\n * needs \"finished only\" has to enforce that itself \u2014 this function cannot know\n * what is live.\n *\n * Returns the number of files newly archived.\n */\nexport function archiveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) return 0;\n\n const files = readdirSync(projectDir);\n let archivedCount = 0;\n\n for (const file of files) {\n if (!file.endsWith('.jsonl') || file === excludeFile) continue;\n\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n\n // Already archived \u2014 including by an earlier rename, before this was a\n // hardlink. Those are the sessions that need restoring to the root, which is\n // a separate repair and not this function's job.\n if (existsSync(destPath)) continue;\n\n try {\n linkSync(sourcePath, destPath);\n if (!silent) console.error(`Archived ${file} \u2192 sessions/ (still resumable)`);\n archivedCount++;\n } catch (error) {\n // Cross-device (EXDEV) is the realistic failure: sessions/ on another\n // volume. Copy instead, and still leave the original alone.\n try {\n copyFileSync(sourcePath, destPath);\n if (!silent) console.error(`Copied ${file} \u2192 sessions/ (hardlink unavailable)`);\n archivedCount++;\n } catch {\n if (!silent) console.error(`Could not archive ${file}: ${error}`);\n }\n }\n }\n\n return archivedCount;\n}\n\n/**\n * @deprecated Renamed to `archiveSessionFilesToSessionsDir`, which is what it\n * now does. Kept so an out-of-tree caller fails loudly at the type level rather\n * than silently keeping the old destructive name for a non-destructive action.\n */\nexport const moveSessionFilesToSessionsDir = archiveSessionFilesToSessionsDir;\n\n// ---------------------------------------------------------------------------\n// CLAUDE.md / TODO.md discovery\n// ---------------------------------------------------------------------------\n\n/** Find TODO.md \u2014 check local first, fallback to central. */\nexport function findTodoPath(cwd: string): string {\n const localPaths = [\n join(cwd, 'TODO.md'),\n join(cwd, 'notes', 'TODO.md'),\n join(cwd, 'Notes', 'TODO.md'),\n join(cwd, '.claude', 'TODO.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) return path;\n }\n\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/** Find CLAUDE.md \u2014 returns the FIRST found path. */\nexport function findClaudeMdPath(cwd: string): string | null {\n const paths = findAllClaudeMdPaths(cwd);\n return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations in priority order.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n const localPaths = [\n join(cwd, '.claude', 'CLAUDE.md'),\n join(cwd, 'CLAUDE.md'),\n join(cwd, 'Notes', 'CLAUDE.md'),\n join(cwd, 'notes', 'CLAUDE.md'),\n join(cwd, 'Prompts', 'CLAUDE.md'),\n join(cwd, 'prompts', 'CLAUDE.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) foundPaths.push(path);\n }\n\n return foundPaths;\n}\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(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\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_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(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_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 PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_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 * 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(HISTORY_DIR, subdir, `${year}-${month}`, filename);\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", "/**\n * TODO.md management \u2014 creation, task updates, checkpoints, and Continue section.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { findTodoPath } from './paths.js';\nimport { applyContinue } from '../../../../session/checkpoint-block.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Task item for TODO.md. */\nexport interface TodoItem {\n content: string;\n completed: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Ensure TODO.md exists. Creates it with default structure if missing.\n * Returns the path to the TODO.md file.\n */\nexport function ensureTodoMd(cwd: string): string {\n const todoPath = findTodoPath(cwd);\n\n if (!existsSync(todoPath)) {\n const parentDir = join(todoPath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n\n const content = `# TODO\n\n## Current Session\n\n- [ ] (Tasks will be tracked here)\n\n## Backlog\n\n- [ ] (Future tasks)\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Created TODO.md: ${todoPath}`);\n }\n\n return todoPath;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Update TODO.md with current session tasks.\n * Preserves the Backlog section and ensures exactly ONE timestamp at the end.\n */\nexport function updateTodoMd(cwd: string, tasks: TodoItem[], sessionSummary?: string): void {\n const todoPath = ensureTodoMd(cwd);\n const content = readFileSync(todoPath, 'utf-8');\n\n const backlogMatch = content.match(/## Backlog[\\s\\S]*?(?=\\n---|\\n\\*Last updated|$)/);\n const backlogSection = backlogMatch\n ? backlogMatch[0].trim()\n : '## Backlog\\n\\n- [ ] (Future tasks)';\n\n const taskLines = tasks.length > 0\n ? tasks.map(t => `- [${t.completed ? 'x' : ' '}] ${t.content}`).join('\\n')\n : '- [ ] (No active tasks)';\n\n const newContent = `# TODO\n\n## Current Session\n\n${taskLines}\n\n${sessionSummary ? `**Session Summary:** ${sessionSummary}\\n\\n` : ''}${backlogSection}\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, newContent);\n console.error(`Updated TODO.md: ${todoPath}`);\n}\n\n/**\n * Add a checkpoint entry to TODO.md (without replacing tasks).\n * Ensures exactly ONE timestamp line at the end.\n */\nexport function addTodoCheckpoint(cwd: string, checkpoint: string): void {\n const todoPath = ensureTodoMd(cwd);\n let content = readFileSync(todoPath, 'utf-8');\n\n // Remove ALL existing timestamp lines and trailing separators\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n\n const checkpointText = `\\n**Checkpoint (${new Date().toISOString()}):** ${checkpoint}\\n\\n`;\n\n const backlogIndex = content.indexOf('## Backlog');\n if (backlogIndex !== -1) {\n content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);\n } else {\n const continueIndex = content.indexOf('## Continue');\n if (continueIndex !== -1) {\n const afterContinue = content.indexOf('\\n---', continueIndex);\n if (afterContinue !== -1) {\n const insertAt = afterContinue + 4;\n content = content.substring(0, insertAt) + '\\n' + checkpointText + content.substring(insertAt);\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n }\n\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${new Date().toISOString()}*\\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Checkpoint added to TODO.md`);\n}\n\n/**\n * The Claude Code session UUID, read off the transcript path.\n *\n * Claude Code names a transcript `<session-uuid>.jsonl`, so the hooks that\n * receive a transcript path are holding the session's identity without knowing\n * it. Both session-end writers had one available and neither used it.\n *\n * Shape-checked rather than trusted: only a UUID is returned, so a renamed,\n * archived or otherwise unexpected filename yields undefined and the caller\n * falls back to the session line rather than writing a `session-id` that\n * identifies nothing. A wrong id is worse than none \u2014 it would make two\n * different sessions compare equal.\n */\nexport function sessionIdFromTranscript(transcriptPath: string | undefined): string | undefined {\n if (!transcriptPath) return undefined;\n const base = basename(transcriptPath).replace(/\\.jsonl$/, '');\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(base)\n ? base\n : undefined;\n}\n\n/**\n * Update the ## Continue section at the top of TODO.md.\n *\n * This is the unattended writer: the pre-compact hook and the daemon's\n * work-queue worker both come through here. It used to build its own block and\n * strip the existing section with `/## Continue\\n[\\s\\S]*?\\n---\\n+/` \u2014 a\n * non-greedy match to the first `---`, which cuts a rich checkpoint in half at\n * the first horizontal rule inside its body and leaves the remainder orphaned\n * in the document.\n *\n * More importantly it was the writer that actually destroyed model-authored\n * checkpoints: `pai pause` wrote one, then this ran seconds later on session\n * end and replaced it with `Working directory: \u2026 Check the latest session note\n * for details.`\n *\n * It now delegates to the shared checkpoint module in \"auto\" mode, which means\n * it inherits the preservation rules rather than reimplementing them. Guarding\n * here rather than at each of the three call sites is deliberate: any future\n * caller inherits the behaviour without knowing it exists.\n */\nexport function updateTodoContinue(\n cwd: string,\n noteFilename: string,\n state: string | null,\n tokenDisplay: string,\n /**\n * The Claude Code session UUID, when the caller knows it.\n *\n * Optional only because a caller may genuinely not have it; supply it\n * whenever you can. Without it `isSameSession` falls back to comparing the\n * note FILENAME, and pausing renames the note \u2014 so a session stops\n * recognising its own checkpoint seconds after writing it, and an automated\n * write is then free to replace it as though it belonged to a predecessor.\n * That is how a live session lost a model-authored handover on 2026-08-04.\n */\n sessionId?: string\n): void {\n // Ensure a TODO.md exists so applyContinue writes to the same file the rest\n // of the hooks lib uses.\n ensureTodoMd(cwd);\n\n const result = applyContinue({\n rootPath: cwd,\n authored: 'auto',\n // The hooks identify a session by its note filename; `pai pause` resolves\n // the same string from the registry (and falls back to this filename when\n // the registry has no session row). They must agree, because this string\n // is the key that decides whether an authored checkpoint belongs to the\n // current session and must be preserved.\n sessionLine: noteFilename.replace(/\\.md$/, ''),\n // The UUID is authoritative when present; the line above is the fallback\n // for callers that cannot supply one, and it is mutable by design.\n sessionId,\n cwd,\n body: state?.trim() || undefined,\n });\n\n if (result.action === 'preserved') {\n console.error(\n 'TODO.md ## Continue left intact \u2014 authored checkpoint for this session'\n );\n return;\n }\n\n if (result.action === 'failed') {\n console.error(`TODO.md ## Continue update failed: ${result.error}`);\n return;\n }\n\n // Refresh the trailing \"Last updated\" stamp without disturbing the block.\n try {\n const todoPath = result.path!;\n const now = new Date().toISOString();\n let content = readFileSync(todoPath, 'utf-8');\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${now}*\\n`;\n writeFileSync(todoPath, content);\n } catch {\n // Non-fatal \u2014 the checkpoint itself is already written.\n }\n\n console.error(\n result.carriedForward\n ? 'TODO.md ## Continue section updated (previous content carried forward)'\n : 'TODO.md ## Continue section updated'\n );\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/** PAI_DIR \u2014 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 \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 * context-fill.ts \u2014 \"how full is this session's context window, right now?\"\n *\n * No hook payload carries this number directly (verified: PreCompact,\n * UserPromptSubmit and PostToolUse stdin never include `context_window`).\n * Two sources can reconstruct it, in order of preference:\n *\n * 1. STATUSLINE STATE FILE \u2014 statusline-command.sh receives the exact\n * figure from Claude Code (`.context_window.used_percentage` /\n * `.context_window.context_window_size`) on every render and persists\n * it to `${TMPDIR}/pai-context-<session_id>.json`. This is authoritative\n * but depends on the status line having rendered recently \u2014 a session\n * whose terminal isn't drawing a status line (headless, backgrounded)\n * leaves this file missing or stale.\n *\n * 2. TRANSCRIPT USAGE \u2014 every `message.usage` entry in the session's own\n * .jsonl transcript already reports the token accounting for that one\n * API call: `input_tokens + cache_read_input_tokens +\n * cache_creation_input_tokens` on the MOST RECENT such entry *is* the\n * current context fill, because each request resends the full context.\n * This is why it must be the last entry's fields summed, never a sum\n * across entries \u2014 summing across entries is a running token-spend\n * total, not a fill reading (see calculateSessionTokens, a different\n * metric answering a different question).\n *\n * When neither source is available, the answer is UNKNOWN, not zero. A zero\n * reads as \"plenty of room\" and would silently suppress every downstream\n * decision that depends on this number (the compaction handover chief among\n * them) for the entire session.\n */\n\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/** Fallback window size when nothing on hand reports one. Matches the\n * default statusline-command.sh already falls back to. */\nexport const DEFAULT_CONTEXT_WINDOW = 200_000;\n\n/** How many trailing lines of the transcript to scan for a usage entry.\n * A single turn is rarely more than a handful of JSONL lines (assistant\n * text + tool_use/tool_result pairs), so 40 comfortably covers one turn\n * even a busy one, without reading the whole file on every check. */\nconst TRANSCRIPT_TAIL_LINES = 40;\n\n/** A statusline reading older than this is treated as not there at all \u2014\n * a stale number is worse than none, because it looks confident. */\nconst STATUSLINE_STALE_MS = 5 * 60 * 1000; // 5 minutes\n\nexport type ContextFillSource = \"statusline\" | \"transcript\" | \"unknown\";\n\nexport interface ContextFillReading {\n status: \"ok\" | \"unknown\";\n /** Tokens currently occupying the context window, or null when unknown. */\n usedTokens: number | null;\n windowSize: number;\n /** usedTokens / windowSize, or null when unknown. Not clamped to [0,1] \u2014\n * callers that need a clamped display value do that themselves so the\n * raw reading (which can legitimately exceed 1 for a moment) is never\n * silently rewritten here. */\n fraction: number | null;\n source: ContextFillSource;\n}\n\nfunction unknownReading(windowSize: number): ContextFillReading {\n return { status: \"unknown\", usedTokens: null, windowSize, fraction: null, source: \"unknown\" };\n}\n\n// ---------------------------------------------------------------------------\n// Source 1 \u2014 statusline state file\n// ---------------------------------------------------------------------------\n\nexport function statuslineStateFilePath(sessionId: string): string {\n return join(tmpdir(), `pai-context-${sessionId}.json`);\n}\n\ninterface StatuslineState {\n used_percentage?: number;\n context_window_size?: number;\n session_id?: string;\n timestamp?: number;\n}\n\n/**\n * Read the state file statusline-command.sh writes on every render.\n * Returns null when the file is missing, unparsable, or older than\n * STATUSLINE_STALE_MS \u2014 all three mean \"not a source right now\".\n */\nexport function readStatuslineFill(sessionId: string, now = Date.now()): ContextFillReading | null {\n if (!sessionId) return null;\n const path = statuslineStateFilePath(sessionId);\n if (!existsSync(path)) return null;\n\n let raw: StatuslineState;\n try {\n raw = JSON.parse(readFileSync(path, \"utf-8\"));\n } catch {\n return null;\n }\n\n if (typeof raw.timestamp !== \"number\" || now - raw.timestamp > STATUSLINE_STALE_MS) {\n return null; // stale \u2014 fall through to the transcript source\n }\n if (typeof raw.used_percentage !== \"number\" || typeof raw.context_window_size !== \"number\") {\n return null;\n }\n\n const windowSize = raw.context_window_size;\n const usedTokens = Math.round((raw.used_percentage / 100) * windowSize);\n return {\n status: \"ok\",\n usedTokens,\n windowSize,\n fraction: raw.used_percentage / 100,\n source: \"statusline\",\n };\n}\n\n// ---------------------------------------------------------------------------\n// Source 2 \u2014 transcript usage (fallback)\n// ---------------------------------------------------------------------------\n\ninterface UsageEntry {\n input_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n}\n\n/**\n * Sum the three context-carrying fields of ONE usage object \u2014 never across\n * usage objects. A single `message.usage` already reports what that one API\n * call sent as context; adding another entry's numbers to it produces a\n * cumulative spend figure, not a fill reading, and can exceed the window\n * size many times over on a long session (the exact defect this helper\n * exists to not repeat \u2014 see the PreCompact header fix in the same change).\n */\nfunction usageTotal(usage: UsageEntry): number {\n return (\n (usage.input_tokens || 0) +\n (usage.cache_read_input_tokens || 0) +\n (usage.cache_creation_input_tokens || 0)\n );\n}\n\n/**\n * Derive context fill from the tail of a .jsonl transcript: the most recent\n * `message.usage` entry, read from the end backwards so a trailing line with\n * no usage field (a plain text turn, a tool_result) doesn't hide one just\n * before it.\n */\nexport function contextFillFromTranscript(\n transcriptPath: string,\n windowSize = DEFAULT_CONTEXT_WINDOW\n): ContextFillReading {\n if (!transcriptPath || !existsSync(transcriptPath)) return unknownReading(windowSize);\n\n let raw: string;\n try {\n raw = readFileSync(transcriptPath, \"utf-8\");\n } catch {\n return unknownReading(windowSize);\n }\n\n const lines = raw.trim().split(\"\\n\").filter((l) => l.trim());\n const tail = lines.slice(-TRANSCRIPT_TAIL_LINES);\n\n for (let i = tail.length - 1; i >= 0; i--) {\n let entry: { message?: { usage?: UsageEntry } };\n try {\n entry = JSON.parse(tail[i]);\n } catch {\n continue;\n }\n const usage = entry?.message?.usage;\n if (usage && typeof usage === \"object\") {\n const usedTokens = usageTotal(usage);\n return {\n status: \"ok\",\n usedTokens,\n windowSize,\n fraction: usedTokens / windowSize,\n source: \"transcript\",\n };\n }\n }\n\n return unknownReading(windowSize);\n}\n\n// ---------------------------------------------------------------------------\n// Precedence \u2014 statusline (fresh) > transcript > unknown\n// ---------------------------------------------------------------------------\n\nexport function getContextFill(\n input: { sessionId?: string; transcriptPath?: string; windowSize?: number },\n now = Date.now()\n): ContextFillReading {\n const windowSize = input.windowSize ?? DEFAULT_CONTEXT_WINDOW;\n\n if (input.sessionId) {\n const fromStatusline = readStatuslineFill(input.sessionId, now);\n if (fromStatusline) return fromStatusline;\n }\n\n if (input.transcriptPath) {\n return contextFillFromTranscript(input.transcriptPath, windowSize);\n }\n\n return unknownReading(windowSize);\n}\n\n// ---------------------------------------------------------------------------\n// Display helper \u2014 clamp-and-flag rather than ever print an absurd number\n// ---------------------------------------------------------------------------\n\nexport interface FillDisplay {\n /** e.g. \"63k\" or \"unknown\". Never a number larger than the window. */\n text: string;\n /** True when the raw reading exceeded the window size and had to be\n * clamped \u2014 that reading was a bug, not a fill, and callers may want to\n * log it even though the displayed text is already safe. */\n flagged: boolean;\n}\n\nexport function formatContextFill(reading: ContextFillReading): FillDisplay {\n if (reading.status !== \"ok\" || reading.usedTokens === null) {\n return { text: \"unknown\", flagged: false };\n }\n\n let used = reading.usedTokens;\n let flagged = false;\n if (used > reading.windowSize) {\n flagged = true;\n used = reading.windowSize;\n }\n\n const text = used > 1000 ? `${Math.round(used / 1000)}k` : String(used);\n return { text: flagged ? `${text}+ (clamped \u2014 reading exceeded window)` : text, flagged };\n}\n\n// ---------------------------------------------------------------------------\n// Threshold-triggered handover \u2014 WHEN to warm up / refresh the model-written\n// summary ahead of a compaction.\n// ---------------------------------------------------------------------------\n//\n// THE CONFIGURED VALUE DOES NOT PREDICT THE TRIGGER. CLAUDE_AUTOCOMPACT_\n// PCT_OVERRIDE was 80 throughout, on this machine, across BOTH of the\n// following regimes \u2014 the same configured number, two different truths:\n//\n// 2026-08-16 .. 2026-09-10 preTokens ~ 993,096 \u2013 1,002,500 (~100% of a 1M window)\n// 2026-09-12 .. 2026-09-15 preTokens ~ 782,981 \u2013 791,995 (~78-79% of a 1M window)\n//\n// A threshold derived only from the configured override was wrong for the\n// second regime and would be wrong again the next time it moves \u2014 the\n// config is not ground truth, it is what Claude Code claims it will honor.\n//\n// GROUND TRUTH IS ON DISK ALREADY: every real compaction writes a\n// `compact_boundary` system event to the transcript with\n// `compactMetadata.preTokens` \u2014 the platform's own count of context tokens\n// immediately before IT compacted. `measureCompactionTrigger` scans a\n// project's own transcripts for these and takes the MINIMUM of the most\n// recent three (minimum, not mean: warm-up must fire before the earliest\n// plausible boundary, not the average one). A project that has compacted\n// before learns its own trigger and needs no code change when the regime\n// moves again \u2014 the same derivation produced 998,508 pre-09-12 and ~784k\n// after, from one unchanged formula.\n//\n// The configured chain \u2014 env override, then a default \u2014 is the fallback,\n// used ONLY when a project has no compaction history yet:\n//\n// effectiveTrigger = measured ?? (windowSize * (override ?? 80) / 100)\n//\n// DEFAULT IS 80, NOT 100, WHEN NOTHING IS KNOWN. 100 was considered \u2014 it\n// matches the FIRST regime above \u2014 and rejected: the costs are asymmetric.\n// Warming up too early wastes one summary; cheap, invisible. Warming up too\n// late produces exactly the degraded successor session this feature exists\n// to prevent. 80 is the conservative default until a project has its own\n// measured history to correct it.\n//\n// Margins below the effective trigger are absolute tokens, sized against the\n// largest single-turn context jump measured: 62,383 tokens (517,952 \u2192\n// 580,335, about 27 seconds). warmup sits 100k below the trigger \u2014\n// comfortably more than that jump, so a session cannot leap clean over\n// warmup straight into a compaction in one turn; refresh at 40k below\n// catches a session that sat above warmup for a while; fireNow at 15k below\n// is \"no time left for a clean crossing\" and fires immediately rather than\n// waiting for one.\nexport const DEFAULT_AUTOCOMPACT_PCT = 80;\n\n/** How many of the most recent compact_boundary events to consider, and to\n * take the minimum of. */\nconst MEASURED_TRIGGER_SAMPLE_SIZE = 3;\n\nexport const THRESHOLD_MARGIN_TOKENS = {\n warmup: 100_000,\n refresh: 40_000,\n immediate: 15_000,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Measured trigger \u2014 scan a project's own transcript history for the\n// platform's own compact_boundary events, ground truth over configuration.\n// ---------------------------------------------------------------------------\n//\n// Deliberately re-implements the tiny bit of path encoding it needs (below)\n// rather than importing project-utils/paths.ts's encodePath: that module\n// chain ends in pai-paths.ts, which calls process.exit(1) if PAI_DIR does\n// not resolve to an existing directory. context-fill.ts is specifically the\n// module a hook falls back on when its environment is unreliable \u2014 pulling\n// in a dependency that can kill the process on import would defeat that.\n// Claude Code's transcript directory is a fixed, home-relative convention\n// (~/.claude/projects/<encoded-cwd>/), not something PAI_DIR governs, so\n// resolving it directly here is also just the more correct dependency, not\n// only the safer one.\n\n/** Real default \u2014 overridable per-call so tests never touch the user's\n * actual ~/.claude/projects/ (a live directory this very session writes to). */\nexport const CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\nfunction encodeProjectPath(cwd: string): string {\n return cwd.replace(/[/\\s.-]/g, \"-\");\n}\n\nexport interface CompactBoundarySample {\n preTokens: number;\n timestampMs: number;\n /** ISO string, kept alongside timestampMs so callers can print it. */\n timestamp: string;\n /** The event's own uuid when present \u2014 the dedup key. Claude Code's\n * `sessions/` archive directory mirrors the live project transcript, so\n * the SAME compact_boundary event can legitimately appear in two files;\n * without deduping by identity, \"most recent three\" can silently become\n * three copies of one event, which is a stale reading wearing a\n * plausible-looking sample size. */\n uuid?: string;\n}\n\n/** Every `.jsonl` transcript belonging to a project \u2014 top-level (the live\n * file) and `sessions/` (Claude Code's archive, which mirrors it). No file\n * is excluded and no ordering is applied here: ordering by EVENT\n * timestamp, not by file mtime, is the whole point (see\n * readCompactBoundarySamples) \u2014 a file's mtime does not reliably track\n * which events inside it are recent, and pre-filtering by mtime is exactly\n * what caused this function to return a stale, pre-regime-change trigger\n * on real data. */\nfunction listProjectTranscripts(cwd: string, projectsDir: string): string[] {\n const projectDir = join(projectsDir, encodeProjectPath(cwd));\n if (!existsSync(projectDir)) return [];\n\n const paths: string[] = [];\n const collect = (dir: string): void => {\n if (!existsSync(dir)) return;\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return;\n }\n for (const entry of entries) {\n if (entry.endsWith(\".jsonl\")) paths.push(join(dir, entry));\n }\n };\n collect(projectDir);\n collect(join(projectDir, \"sessions\"));\n return paths;\n}\n\n/** Placeholder model id the platform writes on synthetic assistant turns. */\nconst SYNTHETIC_MODEL = \"<synthetic>\";\n\n/**\n * Which family of model wrote a transcript. \"claude\" is the platform's own;\n * \"foreign\" is any other provider routed through the same CLI (a different\n * context window, so its compactions say nothing about ours); \"unknown\" is\n * no usable model field at all.\n */\nexport type TranscriptModelFamily = \"claude\" | \"foreign\" | \"unknown\";\n\nexport function modelFamily(model: string | null | undefined): TranscriptModelFamily {\n if (typeof model !== \"string\" || model === \"\" || model === SYNTHETIC_MODEL) return \"unknown\";\n return model.startsWith(\"claude-\") ? \"claude\" : \"foreign\";\n}\n\n/**\n * The family of the LAST assistant model in a transcript \u2014 scanned from the\n * end so a long transcript costs one read and a few lines of parsing.\n * Unreadable or model-less transcripts are \"unknown\", never \"foreign\":\n * the callers that skip work on \"foreign\" must not skip it on doubt.\n */\nexport function transcriptModelFamily(path: string): TranscriptModelFamily {\n let raw: string;\n try {\n raw = readFileSync(path, \"utf-8\");\n } catch {\n return \"unknown\";\n }\n const lines = raw.split(\"\\n\");\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i];\n if (!line.includes('\"model\"')) continue;\n try {\n const entry = JSON.parse(line) as { type?: string; message?: { model?: unknown } };\n if (entry.type !== \"assistant\") continue;\n const model = entry.message?.model;\n if (typeof model === \"string\" && model !== SYNTHETIC_MODEL) return modelFamily(model);\n } catch {\n continue;\n }\n }\n return \"unknown\";\n}\n\n/**\n * Every DISTINCT compact_boundary sample found across ALL of a project's\n * transcripts (live + archived), newest first by the event's OWN timestamp\n * \u2014 never by which file it came from or that file's mtime. Deduplicated by\n * the event's uuid (falling back to a timestamp+preTokens key for the rare\n * line with no uuid) so an event mirrored into `sessions/` is counted once.\n */\nfunction readCompactBoundarySamples(cwd: string, projectsDir: string): CompactBoundarySample[] {\n const byKey = new Map<string, CompactBoundarySample>();\n\n for (const path of listProjectTranscripts(cwd, projectsDir)) {\n let raw: string;\n try {\n raw = readFileSync(path, \"utf-8\");\n } catch {\n continue;\n }\n\n // The model governing each sample: the most recent assistant\n // `message.model` seen in this file before the compact_boundary. A\n // transcript written by a non-Claude model (a headless worker on another\n // provider, with a different context window) compacts at a different\n // size and must not shape THIS project's trigger \u2014 two such workers\n // compacting at ~151k pulled a real project's trigger from ~784k to\n // ~151k. Samples with no model seen yet are kept: older transcripts\n // may lack the field.\n let lastModel: string | null = null;\n let foreignDiscards = 0;\n\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n let entry: {\n type?: string;\n subtype?: string;\n timestamp?: string;\n uuid?: string;\n compactMetadata?: { preTokens?: number };\n message?: { model?: unknown };\n };\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n if (entry.type === \"assistant\") {\n const model = entry.message?.model;\n if (typeof model === \"string\" && model !== SYNTHETIC_MODEL) lastModel = model;\n continue;\n }\n if (entry.type !== \"system\" || entry.subtype !== \"compact_boundary\") continue;\n const preTokens = entry.compactMetadata?.preTokens;\n if (typeof preTokens !== \"number\" || !Number.isFinite(preTokens)) continue;\n const timestampMs = entry.timestamp ? Date.parse(entry.timestamp) : NaN;\n if (!Number.isFinite(timestampMs)) continue;\n if (modelFamily(lastModel) === \"foreign\") {\n foreignDiscards++;\n continue;\n }\n\n const key = entry.uuid ?? `${timestampMs}:${preTokens}`;\n if (!byKey.has(key)) {\n byKey.set(key, { preTokens, timestampMs, timestamp: entry.timestamp!, uuid: entry.uuid });\n }\n }\n\n if (foreignDiscards > 0) {\n console.error(\n `[context-fill] ignored ${foreignDiscards} compaction sample(s) from a ` +\n `non-Claude transcript (model=${lastModel}): ${path}`\n );\n }\n }\n\n return [...byKey.values()].sort((a, b) => b.timestampMs - a.timestampMs);\n}\n\n/**\n * The most recent MEASURED_TRIGGER_SAMPLE_SIZE distinct compact_boundary\n * events for a project, newest first \u2014 exposed on its own (not just the\n * derived minimum) so the number `measureCompactionTrigger` returns is\n * checkable: print these and the timestamps prove which three events\n * produced it, rather than asking for trust.\n */\nexport function selectedCompactionSamples(\n cwd: string,\n projectsDir: string = CLAUDE_PROJECTS_DIR\n): CompactBoundarySample[] {\n if (!cwd) return [];\n return readCompactBoundarySamples(cwd, projectsDir).slice(0, MEASURED_TRIGGER_SAMPLE_SIZE);\n}\n\n/**\n * The measured compaction trigger for a project, or null when it has no\n * compaction history yet (a brand-new project, or one whose transcripts\n * this process cannot read). Minimum of the most recent\n * MEASURED_TRIGGER_SAMPLE_SIZE DISTINCT compact_boundary events, ordered by\n * the events' own timestamps across every transcript the project has\n * (live and archived) \u2014 see the module comment above for why minimum, not\n * mean, and readCompactBoundarySamples for why \"distinct\" and \"own\n * timestamp\" both matter (a file-mtime-ordered, non-deduplicated version of\n * this returned a stale pre-regime-change trigger on real project data).\n */\nexport function measureCompactionTrigger(\n cwd: string,\n projectsDir: string = CLAUDE_PROJECTS_DIR\n): number | null {\n const samples = selectedCompactionSamples(cwd, projectsDir);\n if (samples.length === 0) return null;\n const trigger = Math.min(...samples.map((s) => s.preTokens));\n console.error(\n `[context-fill] measured trigger for ${cwd}: ${trigger} (minimum of ` +\n samples.map((s) => `${s.preTokens}@${s.timestamp}`).join(\", \") + \")\"\n );\n return trigger;\n}\n\n/**\n * Read CLAUDE_AUTOCOMPACT_PCT_OVERRIDE from the environment. Absent \u2192\n * DEFAULT_AUTOCOMPACT_PCT. Present but not a finite number in (0, 100] \u2192\n * also DEFAULT_AUTOCOMPACT_PCT, logged, so a typo in the override degrades\n * to the documented default instead of silently producing nonsense\n * thresholds (0, negative, or a fraction so large no session ever reaches\n * it).\n */\nexport function resolveAutocompactPct(env: NodeJS.ProcessEnv = process.env): number {\n const raw = env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE;\n if (raw === undefined || raw === \"\") return DEFAULT_AUTOCOMPACT_PCT;\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 100) {\n console.error(\n `[context-fill] CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=\"${raw}\" is not a usable percentage \u2014 ` +\n `falling back to the default ${DEFAULT_AUTOCOMPACT_PCT}.`\n );\n return DEFAULT_AUTOCOMPACT_PCT;\n }\n return parsed;\n}\n\n/**\n * The two thresholds that actually fire a handover job. \"immediate\" is not a\n * third job \u2014 see `isImmediate` below \u2014 it only changes how urgently a\n * crossing of these two is treated.\n */\nexport type ThresholdName = \"warmup\" | \"refresh\";\n\n/** Which basis actually produced effectiveTriggerTokens \u2014 reported so the\n * number is checkable rather than trusted. */\nexport type TriggerSource = \"measured\" | \"configured\" | \"measured-clamped\";\n\nexport interface ContextFillThresholds {\n warmupTokens: number;\n refreshTokens: number;\n immediateTokens: number;\n /** The derived compaction trigger these were measured back from \u2014\n * min(measured, configured) when a measured value exists. */\n effectiveTriggerTokens: number;\n /** The raw measured value from this project's own compact_boundary\n * history, before any clamping \u2014 null when the project has no history.\n * Kept alongside effectiveTriggerTokens so a clamp is visible rather\n * than silent: a caller can see both what was measured and what was\n * actually used. */\n measuredTriggerTokens: number | null;\n /** The raw configured-chain value (env override or default, as a\n * fraction of the window) \u2014 always computed and reported, even when it\n * wasn't what ended up being used. */\n configuredTriggerTokens: number;\n /** The autocompact percentage from the configured chain. */\n autocompactPct: number;\n /** \"measured\": a measured value existed and was <= configured, so it was\n * used directly \u2014 the better estimate, since it reflects reality the\n * configured percentage cannot know.\n * \"measured-clamped\": a measured value existed but was HIGHER than\n * configured \u2014 it reflects a regime that may no longer apply (this\n * project's last compaction predates a since-changed trigger), so the\n * lower, safer configured value was used instead.\n * \"configured\": no measured value exists yet (no compaction history for\n * this project) \u2014 the configured chain is all there is. */\n triggerSource: TriggerSource;\n /** True when these were computed against a window size Claude Code itself\n * reported (the statusline source). False when the window is only an\n * assumed default (the transcript-fallback source never learns the real\n * window size) \u2014 callers should log this: a threshold silently computed\n * against the wrong basis is the same class of fault as printing a token\n * count larger than the window. */\n windowConfirmed: boolean;\n}\n\nexport interface ContextFillThresholdOpts {\n /** The session's project directory, used to look up its own compaction\n * history for the measured trigger. Omit when unknown \u2014 falls back to\n * the configured chain, same as a project with no history yet. */\n cwd?: string;\n /** Override the measured-trigger lookup instead of scanning transcripts \u2014\n * primarily for tests. `null` forces the configured fallback even when\n * `cwd` is given; `undefined` (the default) does the real lookup. */\n measuredTrigger?: number | null;\n}\n\n/**\n * Derive warmup/refresh/immediate thresholds from a fill reading.\n *\n * effectiveTrigger = min(measured, configuredChainValue) when a measured\n * value exists \u2014 NOT the measured value outright. A project whose newest\n * compaction predates a regime change measures a stale-HIGH trigger: a real\n * case on this machine measured 998,267 for a project whose ACTUAL current\n * boundary (from a different project's fresher history, cross-checked\n * independently) is ~784,000 \u2014 using 998,267 directly would compute a\n * warm-up of 898,267, above the real boundary, so the handover would never\n * fire there. The measurement is honest; it is just old.\n *\n * The minimum is correct in both directions: the measured value is the\n * better estimate when it is LOWER than configured (it reflects reality the\n * configured percentage cannot know \u2014 see the module comment above, the\n * 100%-to-78% regime change this project itself lived through); it is\n * unsafe when it is HIGHER (it reflects a regime that no longer applies).\n * Taking the minimum costs nothing in the safe direction \u2014 one wasted\n * summary if the project's regime actually did move up \u2014 and prevents the\n * unsafe direction, where a stale-high measurement suppresses the handover\n * past the real boundary. That asymmetry is the same one the 80-not-100\n * default was chosen for.\n *\n * Margins below effectiveTrigger are clamped at 0 (and logged) for a window\n * small enough that a margin would otherwise go negative \u2014 a pathological\n * input should degrade to \"fire immediately\", never to a threshold below\n * zero.\n */\nexport function contextFillThresholds(\n reading: ContextFillReading,\n env: NodeJS.ProcessEnv = process.env,\n opts: ContextFillThresholdOpts = {}\n): ContextFillThresholds {\n const windowConfirmed = reading.source === \"statusline\";\n const autocompactPct = resolveAutocompactPct(env);\n const configuredTriggerTokens = Math.round(reading.windowSize * (autocompactPct / 100));\n\n const measuredTriggerTokens = opts.measuredTrigger !== undefined\n ? opts.measuredTrigger\n : opts.cwd\n ? measureCompactionTrigger(opts.cwd)\n : null;\n\n let effectiveTriggerTokens: number;\n let triggerSource: TriggerSource;\n\n if (measuredTriggerTokens === null) {\n effectiveTriggerTokens = configuredTriggerTokens;\n triggerSource = \"configured\";\n console.error(\n `[context-fill] trigger source: CONFIGURED \u2014 no compaction history for this project yet. ` +\n `measured=none, configured=${configuredTriggerTokens} (${autocompactPct}% of a ${reading.windowSize}-token ` +\n `window) -> using ${effectiveTriggerTokens}.`\n );\n } else if (measuredTriggerTokens <= configuredTriggerTokens) {\n effectiveTriggerTokens = measuredTriggerTokens;\n triggerSource = \"measured\";\n console.error(\n `[context-fill] trigger source: MEASURED \u2014 measured=${measuredTriggerTokens} ` +\n `(minimum of the most recent ${MEASURED_TRIGGER_SAMPLE_SIZE} compact_boundary events), ` +\n `configured=${configuredTriggerTokens} -> using ${effectiveTriggerTokens} (measured, \u2264 configured).`\n );\n } else {\n effectiveTriggerTokens = configuredTriggerTokens;\n triggerSource = \"measured-clamped\";\n console.error(\n `[context-fill] trigger source: MEASURED-CLAMPED \u2014 measured=${measuredTriggerTokens} is HIGHER than ` +\n `configured=${configuredTriggerTokens} (a stale regime this project's history predates) -> ` +\n `using ${effectiveTriggerTokens} (configured, the safer bound).`\n );\n }\n\n const clamp = (name: string, value: number): number => {\n if (value >= 0) return value;\n console.error(\n `[context-fill] ${name} threshold went negative (${value}) for a ` +\n `${reading.windowSize}-token window \u2014 clamping to 0.`\n );\n return 0;\n };\n\n return {\n warmupTokens: clamp(\"warmup\", effectiveTriggerTokens - THRESHOLD_MARGIN_TOKENS.warmup),\n refreshTokens: clamp(\"refresh\", effectiveTriggerTokens - THRESHOLD_MARGIN_TOKENS.refresh),\n immediateTokens: clamp(\"immediate\", effectiveTriggerTokens - THRESHOLD_MARGIN_TOKENS.immediate),\n effectiveTriggerTokens,\n measuredTriggerTokens,\n configuredTriggerTokens,\n autocompactPct,\n triggerSource,\n windowConfirmed,\n };\n}\n\n/**\n * Which named thresholds `usedTokens` has newly crossed, given the ones that\n * have already fired this session. Ascending order (warmup before refresh).\n *\n * A single check can return more than one name \u2014 e.g. a session first\n * observed at 99% of its window crosses warmup and refresh in the same tick,\n * because there was no earlier clean crossing to catch it at. That is the\n * \"fire immediately\" case: the caller enqueues one handover and marks every\n * newly-crossed name fired, rather than waiting for a crossing that already\n * happened.\n */\nexport function crossedThresholds(\n usedTokens: number,\n thresholds: ContextFillThresholds,\n alreadyFired: ThresholdName[]\n): ThresholdName[] {\n const fired = new Set(alreadyFired);\n const ordered: Array<[ThresholdName, number]> = [\n [\"warmup\", thresholds.warmupTokens],\n [\"refresh\", thresholds.refreshTokens],\n ];\n return ordered.filter(([name, tokens]) => !fired.has(name) && usedTokens >= tokens).map(([name]) => name);\n}\n\n/**\n * True once a session is at or above the \"no time left for a clean crossing\"\n * floor (0.985 of the window). Purely informational for callers \u2014 it never\n * gates whether `crossedThresholds` fires (a poll-based check already fires\n * any unfired threshold the moment `usedTokens` reaches it, on whatever tick\n * observes it) \u2014 but it distinguishes \"this crossed on schedule\" from \"this\n * was first observed already almost out of room\", which is worth a different\n * log line and, for a caller that queues work, a higher priority.\n */\nexport function isImmediate(usedTokens: number, thresholds: ContextFillThresholds): boolean {\n return usedTokens >= thresholds.immediateTokens;\n}\n", "/**\n * transcript-text.ts \u2014 small, pure pieces of transcript parsing that are\n * worth testing in isolation.\n *\n * Split out of context-compression-hook.ts (a hook entrypoint that calls\n * `main()` and `process.exit()` at import time, which makes it unsafe to\n * import directly from a test) so the two bug fixes that live here can be\n * covered by a real regression test rather than only a manual before/after\n * digest.\n */\n\n/**\n * Turn Claude content (string or content block array) into plain text.\n *\n * BUG (found in ~1 in 4 injected digests on this machine): a \"user\" turn is\n * not always something the user typed \u2014 a tool_result is also delivered as a\n * user-role message, and its `content` block looks like\n * `{ type: \"tool_result\", content: [...] }` where the inner `content` is\n * itself an array of blocks, not a string. The old code fell back to\n * `String(c.content)` for anything without a `.text` field, and `String()`\n * on an array of objects produces literal `\"[object Object],[object\n * Object]\"` \u2014 which is exactly what showed up under \"Recent user requests\".\n * Only ever take the `.text` of a `type: \"text\"` block; everything else\n * (tool_use, tool_result, image, and any nested content) is ignored rather\n * than stringified.\n */\nexport function contentToText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === 'string') return c;\n if (c && typeof c === 'object' && c.type === 'text' && typeof c.text === 'string') {\n return c.text;\n }\n return '';\n })\n .join(' ')\n .trim();\n }\n return '';\n}\n\n/**\n * True for a modified-file path that is noise for a handover, not signal.\n *\n * BUG: a real digest showed 8 of 10 \"Files modified\" slots filled with\n * throwaway paths under a jobs/<id>/tmp/ directory (msg8.txt, msg9.txt \u2026),\n * crowding out the two real source files also edited that session. Anything\n * under a `tmp/`, `jobs/`, or `scratchpad/` path segment is excluded \u2014 those\n * are working scratch space, not the codebase the next session needs to\n * remember it touched.\n */\nexport function isNoiseFilePath(filePath: string, scratchpadDir?: string): boolean {\n if (scratchpadDir && filePath.startsWith(scratchpadDir)) return true;\n const segments = filePath.split('/');\n return segments.includes('tmp') || segments.includes('jobs') || segments.includes('scratchpad');\n}\n\n/**\n * Order a modified-file list so entries inside `cwd` are preferred when a\n * caller has to truncate to a fixed slot count \u2014 an unrelated repo touched\n * in passing should not crowd out the files that actually matter for this\n * session's working directory.\n */\nexport function preferCwdFiles(files: string[], cwd?: string): string[] {\n if (!cwd) return files;\n const inCwd = files.filter((f) => f.startsWith(cwd));\n const outCwd = files.filter((f) => !f.startsWith(cwd));\n return [...inCwd, ...outCwd];\n}\n", "/**\n * context-handover-cache.ts \u2014 the well-known file where the threshold-\n * triggered pre-compaction handover (see ../../../daemon/context-handover-\n * worker.ts) is written, and where context-compression-hook.ts looks for it\n * at compaction time.\n *\n * Deliberately dependency-light (fs/os/path only). The daemon worker that\n * WRITES this file needs the heavier machinery in session-summary-worker.ts\n * (spawning Claude, git log, etc.), but the PreCompact hook that READS it\n * runs as a short-lived process on every compaction and must not drag in\n * daemon state, database pools, or anything else that module graph carries \u2014\n * this file is the shared seam so neither side has to.\n */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport type HandoverThreshold = \"warmup\" | \"refresh\";\n\nexport interface ContextHandoverCache {\n sessionId: string;\n cwd: string;\n threshold: HandoverThreshold;\n generatedAt: string; // ISO timestamp\n model: string;\n summary: string;\n}\n\nexport function contextHandoverCachePath(sessionId: string): string {\n return join(tmpdir(), `pai-context-handover-${sessionId}.json`);\n}\n\nexport function readContextHandoverCache(sessionId: string): ContextHandoverCache | null {\n const path = contextHandoverCachePath(sessionId);\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf-8\")) as ContextHandoverCache;\n } catch {\n return null;\n }\n}\n\nexport function writeContextHandoverCache(cache: ContextHandoverCache): void {\n writeFileSync(contextHandoverCachePath(cache.sessionId), JSON.stringify(cache, null, 2), \"utf-8\");\n}\n", "/**\n * Evidence binding for the model-written pre-compaction handover.\n *\n * The handover is prose written by a model about the session. Nothing else\n * checks its claims, and the successor session cannot: by the time it reads\n * the handover, the transcript it describes has been compacted away. This\n * module does the one check that is cheap and deterministic \u2014 every\n * checkable identifier in the handover (commit hashes, versions, paths,\n * pids, timestamps, token counts) must appear verbatim in the transcript\n * the handover was written from. Identifiers that do not are reported as\n * unverified. Nothing is ever dropped or rewritten: the reader gets the\n * handover unchanged plus one footer line saying how much of it is\n * anchored in the record.\n *\n * Borrowed from the evidence-binding step of graph extractors that check an\n * LLM's output against the chunk it was given. No model is called.\n */\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { contentToText } from \"./transcript-text.js\";\n\nexport interface EvidenceResult {\n /** Distinct checkable identifiers found in the handover. */\n total: number;\n /** Identifiers present verbatim (after normalisation) in the transcript. */\n found: string[];\n /** Identifiers absent from the transcript \u2014 unverified, not wrong. */\n missing: string[];\n /** One line for the reader, never empty. */\n footer: string;\n}\n\n/** Minimum identifier length worth checking; shorter strings match by accident. */\nconst MIN_TOKEN_LENGTH = 4;\n/** How many unverified identifiers the footer names before \"+N more\". */\nconst FOOTER_NAMED_MISSING = 5;\n/** Length of the handover's opening used to recognise its own echo in a tool result. */\nconst ECHO_HEAD_LENGTH = 80;\n\n/** Markers of text that was injected INTO the session rather than produced by it. */\nconst INJECTED_MARKERS = [\n \"HANDOVER FROM THE PREVIOUS SESSION\",\n \"MODEL-WRITTEN HANDOVER\",\n \"SESSION STATE RECOVERED AFTER COMPACTION\",\n];\n\n/** Tools whose input would contain the handover itself when it targets the handover file. */\nconst HANDOVER_WRITERS = new Set([\"Write\", \"Edit\", \"NotebookEdit\"]);\n\n/**\n * Identifier classes, in priority order. Each pattern yields either its\n * whole match or capture group 1 when present. Kept deliberately literal:\n * an identifier is something a reader could grep for.\n */\nconst TOKEN_PATTERNS: RegExp[] = [\n // git hashes: 7-40 hex chars with at least one letter (a plain number is not a hash)\n /\\b(?=[0-9a-f]*[a-f])[0-9a-f]{7,40}\\b/g,\n // package@version\n /(?:@[\\w.-]+\\/)?[\\w.-]+@\\d+\\.\\d+\\.\\d+(?:-[\\w.]+)?/g,\n // semver\n /\\bv?\\d+\\.\\d+\\.\\d+(?:-[\\w.]+)?\\b/g,\n // absolute paths (two or more segments)\n /(?:^|[\\s(`'\"])(\\/[\\w.-]+(?:\\/[\\w.-]+)+)/gm,\n // relative paths with a slash and an extension\n /(?:^|[\\s(`'\"])((?:~\\/|\\.\\.?\\/)?[\\w.-]+(?:\\/[\\w.-]+)+\\.[a-z0-9]{1,5})\\b/gm,\n // process ids\n /\\bpid\\s*[:=#]?\\s*(\\d{3,7})\\b/gi,\n // ISO dates and timestamps\n /\\b\\d{4}-\\d{2}-\\d{2}(?:[T ]\\d{2}:\\d{2}(?::\\d{2})?(?:\\.\\d+)?Z?)?\\b/g,\n // numbers with units \u2014 digits only are kept\n /\\b(\\d[\\d,]*)\\s*(?:tokens?|ms|MB|GB)\\b/g,\n];\n\n/** Strip thousands separators so \"784,000\" and \"784000\" compare equal. */\nfunction normalise(text: string): string {\n return text.replace(/(\\d),(?=\\d{3}\\b)/g, \"$1\");\n}\n\nfunction stripSystemReminders(text: string): string {\n return text.replace(/<system-reminder>[\\s\\S]*?<\\/system-reminder>/g, \"\");\n}\n\n/** Every distinct checkable identifier in a handover, in order of first appearance. */\nexport function extractCheckableTokens(markdown: string): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const pattern of TOKEN_PATTERNS) {\n pattern.lastIndex = 0;\n for (const match of markdown.matchAll(pattern)) {\n const raw = (match[1] ?? match[0]).trim();\n const token = normalise(raw);\n if (token.length < MIN_TOKEN_LENGTH || seen.has(token)) continue;\n seen.add(token);\n out.push(token);\n }\n }\n return out;\n}\n\ninterface TranscriptEntry {\n type?: string;\n message?: { content?: unknown };\n}\n\ninterface ContentPart {\n type?: string;\n text?: string;\n name?: string;\n input?: { file_path?: unknown } & Record<string, unknown>;\n content?: unknown;\n}\n\n/**\n * Everything the session itself said, did, or was shown \u2014 minus the two\n * places where the handover could verify against itself: the tool call\n * that wrote the handover file, and injected blocks carried over from an\n * earlier compaction.\n */\nfunction buildHaystack(\n transcriptPaths: string[],\n handoverPath: string | null,\n handoverHead: string\n): string {\n const parts: string[] = [];\n const resolvedHandover = handoverPath ? resolve(handoverPath) : null;\n\n const pushUserText = (text: string): void => {\n if (INJECTED_MARKERS.some((m) => text.includes(m))) return;\n parts.push(stripSystemReminders(text));\n };\n\n for (const path of transcriptPaths) {\n let raw: string;\n try {\n raw = readFileSync(path, \"utf-8\");\n } catch {\n continue;\n }\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n let entry: TranscriptEntry;\n try {\n entry = JSON.parse(line) as TranscriptEntry;\n } catch {\n continue;\n }\n const content = entry.message?.content;\n if (content === undefined) continue;\n\n if (entry.type === \"user\") {\n if (typeof content === \"string\") {\n pushUserText(content);\n continue;\n }\n if (!Array.isArray(content)) continue;\n for (const part of content as ContentPart[]) {\n if (part?.type === \"text\" && typeof part.text === \"string\") pushUserText(part.text);\n else if (part?.type === \"tool_result\") {\n const text = contentToText(part.content);\n if (handoverHead && text.startsWith(handoverHead)) continue;\n parts.push(text);\n }\n }\n } else if (entry.type === \"assistant\" && Array.isArray(content)) {\n for (const part of content as ContentPart[]) {\n if (part?.type === \"text\" && typeof part.text === \"string\") parts.push(part.text);\n else if (part?.type === \"tool_use\") {\n const target = part.input?.file_path;\n if (\n resolvedHandover &&\n HANDOVER_WRITERS.has(part.name ?? \"\") &&\n typeof target === \"string\" &&\n resolve(target) === resolvedHandover\n ) {\n continue;\n }\n parts.push(JSON.stringify(part.input ?? {}));\n }\n }\n }\n }\n }\n return normalise(parts.join(\"\\n\"));\n}\n\nfunction footerFor(found: string[], missing: string[]): string {\n const total = found.length + missing.length;\n if (total === 0) return \"Evidence: no checkable identifiers\";\n let footer = `Evidence: ${found.length}/${total} identifiers found in the transcript`;\n if (missing.length > 0) {\n const named = missing.slice(0, FOOTER_NAMED_MISSING).join(\", \");\n const more = missing.length - FOOTER_NAMED_MISSING;\n footer += `; unverified: ${named}${more > 0 ? ` +${more} more` : \"\"}`;\n }\n return footer;\n}\n\n/**\n * Check a handover's identifiers against the transcript(s) it was written\n * from. Never throws: any failure degrades to a footer that says the check\n * was unavailable, so the handover itself is always delivered.\n *\n * @param handoverMarkdown the handover text, returned to the reader unchanged\n * @param handoverPath where the handover was written, if it was written\n * to a file \u2014 its own Write/Edit is excluded as evidence\n * @param transcriptPaths the session's transcript file(s)\n */\nexport function bindHandoverEvidence(\n handoverMarkdown: string,\n handoverPath: string | null,\n transcriptPaths: string[]\n): EvidenceResult {\n try {\n const tokens = extractCheckableTokens(handoverMarkdown);\n if (tokens.length === 0) {\n return { total: 0, found: [], missing: [], footer: footerFor([], []) };\n }\n const head = handoverMarkdown.trim().slice(0, ECHO_HEAD_LENGTH);\n const haystack = buildHaystack(transcriptPaths, handoverPath, head);\n const found: string[] = [];\n const missing: string[] = [];\n for (const token of tokens) (haystack.includes(token) ? found : missing).push(token);\n return { total: tokens.length, found, missing, footer: footerFor(found, missing) };\n } catch {\n return { total: 0, found: [], missing: [], footer: \"Evidence: check unavailable\" };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["existsSync", "readFileSync", "writeFileSync", "basename", "join", "tmpdir", "existsSync", "join", "join", "existsSync", "join", "existsSync", "existsSync", "readFileSync", "join", "homedir", "resolve", "existsSync", "mkdirSync", "readdirSync", "readFileSync", "join", "basename", "content", "existsSync", "join", "basename", "existsSync", "mkdirSync", "readFileSync", "writeFileSync", "join", "basename", "existsSync", "readFileSync", "writeFileSync", "readdirSync", "renameSync", "mkdirSync", "join", "join", "existsSync", "readFileSync", "mkdirSync", "writeFileSync", "renameSync", "existsSync", "existsSync", "join", "mkdirSync", "writeFileSync", "readFileSync", "writeFileSync", "existsSync", "readdirSync", "readFileSync", "homedir", "join", "join", "homedir", "existsSync", "readFileSync", "writeFileSync", "tmpdir", "join", "readFileSync", "resolve", "resolve", "readFileSync", "readFileSync", "basename", "join", "existsSync", "writeFileSync", "resolve", "tmpdir"]
|
|
7
7
|
}
|