@tekmidian/pai 0.5.3 → 0.5.5

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.
@@ -437,8 +437,24 @@ export function createSessionNote(notesDir: string, description: string): string
437
437
  */
438
438
  export function appendCheckpoint(notePath: string, checkpoint: string): void {
439
439
  if (!existsSync(notePath)) {
440
- console.error(`Note file not found: ${notePath}`);
441
- return;
440
+ // Note vanished (cloud sync, cleanup, etc.) — recreate it
441
+ console.error(`Note file not found, recreating: ${notePath}`);
442
+ try {
443
+ const parentDir = join(notePath, '..');
444
+ if (!existsSync(parentDir)) {
445
+ mkdirSync(parentDir, { recursive: true });
446
+ }
447
+ const noteFilename = basename(notePath);
448
+ const numberMatch = noteFilename.match(/^(\d+)/);
449
+ const noteNumber = numberMatch ? numberMatch[1] : '0000';
450
+ const date = new Date().toISOString().split('T')[0];
451
+ 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`;
452
+ writeFileSync(notePath, content);
453
+ console.error(`Recreated session note: ${noteFilename}`);
454
+ } catch (err) {
455
+ console.error(`Failed to recreate note: ${err}`);
456
+ return;
457
+ }
442
458
  }
443
459
 
444
460
  const content = readFileSync(notePath, 'utf-8');
@@ -891,6 +907,7 @@ ${sessionSummary ? `**Session Summary:** ${sessionSummary}\n\n` : ''}${backlogSe
891
907
  /**
892
908
  * Add a checkpoint entry to TODO.md (without replacing tasks)
893
909
  * Ensures only ONE timestamp line at the end
910
+ * Works regardless of TODO.md structure — appends if no known section found
894
911
  */
895
912
  export function addTodoCheckpoint(cwd: string, checkpoint: string): void {
896
913
  const todoPath = ensureTodoMd(cwd);
@@ -899,11 +916,28 @@ export function addTodoCheckpoint(cwd: string, checkpoint: string): void {
899
916
  // Remove ALL existing timestamp lines and trailing separators
900
917
  content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)+$/g, '');
901
918
 
902
- // Add checkpoint before Backlog section
919
+ const checkpointText = `\n**Checkpoint (${new Date().toISOString()}):** ${checkpoint}\n\n`;
920
+
921
+ // Try to insert before Backlog section
903
922
  const backlogIndex = content.indexOf('## Backlog');
904
923
  if (backlogIndex !== -1) {
905
- const checkpointText = `\n**Checkpoint (${new Date().toISOString()}):** ${checkpoint}\n\n`;
906
924
  content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);
925
+ } else {
926
+ // No Backlog section — try before Continue section, or just append
927
+ const continueIndex = content.indexOf('## Continue');
928
+ if (continueIndex !== -1) {
929
+ // Insert after the Continue section (find the next ## or ---)
930
+ const afterContinue = content.indexOf('\n---', continueIndex);
931
+ if (afterContinue !== -1) {
932
+ const insertAt = afterContinue + 4; // after \n---
933
+ content = content.substring(0, insertAt) + '\n' + checkpointText + content.substring(insertAt);
934
+ } else {
935
+ content = content.trimEnd() + '\n' + checkpointText;
936
+ }
937
+ } else {
938
+ // No known section — just append before the end
939
+ content = content.trimEnd() + '\n' + checkpointText;
940
+ }
907
941
  }
908
942
 
909
943
  // Add exactly ONE timestamp at the end
@@ -912,3 +946,55 @@ export function addTodoCheckpoint(cwd: string, checkpoint: string): void {
912
946
  writeFileSync(todoPath, content);
913
947
  console.error(`Checkpoint added to TODO.md`);
914
948
  }
949
+
950
+ /**
951
+ * Update the ## Continue section at the top of TODO.md.
952
+ * This mirrors "pause session" behavior — gives the next session a starting point.
953
+ * Replaces any existing ## Continue section.
954
+ */
955
+ export function updateTodoContinue(
956
+ cwd: string,
957
+ noteFilename: string,
958
+ state: string | null,
959
+ tokenDisplay: string
960
+ ): void {
961
+ const todoPath = ensureTodoMd(cwd);
962
+ let content = readFileSync(todoPath, 'utf-8');
963
+
964
+ // Remove existing ## Continue section (from ## Continue to the first standalone --- line)
965
+ content = content.replace(/## Continue\n[\s\S]*?\n---\n+/, '');
966
+
967
+ const now = new Date().toISOString();
968
+ const stateLines = state
969
+ ? state.split('\n').filter(l => l.trim()).slice(0, 10).map(l => `> ${l}`).join('\n')
970
+ : `> Check the latest session note for details.`;
971
+
972
+ const continueSection = `## Continue
973
+
974
+ > **Last session:** ${noteFilename.replace('.md', '')}
975
+ > **Paused at:** ${now}
976
+ >
977
+ ${stateLines}
978
+
979
+ ---
980
+
981
+ `;
982
+
983
+ // Remove leading whitespace from content
984
+ content = content.replace(/^\s+/, '');
985
+
986
+ // If content starts with # title, insert after it
987
+ const titleMatch = content.match(/^(# [^\n]+\n+)/);
988
+ if (titleMatch) {
989
+ content = titleMatch[1] + continueSection + content.substring(titleMatch[0].length);
990
+ } else {
991
+ content = continueSection + content;
992
+ }
993
+
994
+ // Clean up trailing timestamps and add fresh one
995
+ content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)+$/g, '');
996
+ content = content.trimEnd() + `\n\n---\n\n*Last updated: ${now}*\n`;
997
+
998
+ writeFileSync(todoPath, content);
999
+ console.error('TODO.md ## Continue section updated');
1000
+ }
@@ -2,14 +2,13 @@
2
2
  /**
3
3
  * PreCompact Hook - Triggered before context compression
4
4
  *
5
- * Two critical jobs:
6
- * 1. Save checkpoint to session note + send notification (existing)
7
- * 2. OUTPUT session state to stdout so it gets injected into the conversation
8
- * as a <system-reminder> BEFORE compaction. This ensures the compaction
9
- * summary retains awareness of what was being worked on.
5
+ * Three critical jobs:
6
+ * 1. Save rich checkpoint to session note work items, state, meaningful rename
7
+ * 2. Update TODO.md with a proper ## Continue section for the next session
8
+ * 3. Save session state to temp file for post-compact injection via SessionStart(compact)
10
9
  *
11
- * Without (2), compaction produces a generic summary and the session loses
12
- * critical context: current task, recent requests, file paths, decisions.
10
+ * This hook now mirrors "pause session" behavior: the session note gets a
11
+ * descriptive name, rich content, and TODO.md gets a continuation prompt.
13
12
  */
14
13
 
15
14
  import { readFileSync, writeFileSync } from 'fs';
@@ -18,11 +17,12 @@ import { tmpdir } from 'os';
18
17
  import {
19
18
  sendNtfyNotification,
20
19
  getCurrentNotePath,
20
+ createSessionNote,
21
21
  appendCheckpoint,
22
22
  addWorkToSessionNote,
23
23
  findNotesDir,
24
- findTodoPath,
25
- addTodoCheckpoint,
24
+ renameSessionNote,
25
+ updateTodoContinue,
26
26
  calculateSessionTokens,
27
27
  WorkItem,
28
28
  } from '../lib/project-utils';
@@ -36,6 +36,16 @@ interface HookInput {
36
36
  trigger?: string;
37
37
  }
38
38
 
39
+ /** Structured data extracted from a transcript in a single pass. */
40
+ interface TranscriptData {
41
+ userMessages: string[];
42
+ summaries: string[];
43
+ captures: string[];
44
+ lastCompleted: string;
45
+ filesModified: string[];
46
+ workItems: WorkItem[];
47
+ }
48
+
39
49
  // ---------------------------------------------------------------------------
40
50
  // Helpers
41
51
  // ---------------------------------------------------------------------------
@@ -79,24 +89,23 @@ function getTranscriptStats(transcriptPath: string): { messageCount: number; isL
79
89
  }
80
90
 
81
91
  // ---------------------------------------------------------------------------
82
- // Session state extraction
92
+ // Unified transcript parser — single pass extracts everything
83
93
  // ---------------------------------------------------------------------------
84
94
 
85
- /**
86
- * Extract structured session state from the transcript JSONL.
87
- * Returns a human-readable summary (<2000 chars) suitable for injection
88
- * into the conversation before compaction.
89
- */
90
- function extractSessionState(transcriptPath: string, cwd?: string): string | null {
95
+ function parseTranscript(transcriptPath: string): TranscriptData {
96
+ const data: TranscriptData = {
97
+ userMessages: [],
98
+ summaries: [],
99
+ captures: [],
100
+ lastCompleted: '',
101
+ filesModified: [],
102
+ workItems: [],
103
+ };
104
+
91
105
  try {
92
106
  const raw = readFileSync(transcriptPath, 'utf-8');
93
107
  const lines = raw.trim().split('\n');
94
-
95
- const userMessages: string[] = [];
96
- const summaries: string[] = [];
97
- const captures: string[] = [];
98
- let lastCompleted = '';
99
- const filesModified = new Set<string>();
108
+ const seenSummaries = new Set<string>();
100
109
 
101
110
  for (const line of lines) {
102
111
  if (!line.trim()) continue;
@@ -106,151 +115,165 @@ function extractSessionState(transcriptPath: string, cwd?: string): string | nul
106
115
  // --- User messages ---
107
116
  if (entry.type === 'user' && entry.message?.content) {
108
117
  const text = contentToText(entry.message.content).slice(0, 300);
109
- if (text) userMessages.push(text);
118
+ if (text) data.userMessages.push(text);
110
119
  }
111
120
 
112
- // --- Assistant structured sections ---
121
+ // --- Assistant content ---
113
122
  if (entry.type === 'assistant' && entry.message?.content) {
114
123
  const text = contentToText(entry.message.content);
115
124
 
125
+ // Summaries → also create work items
116
126
  const summaryMatch = text.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
117
127
  if (summaryMatch) {
118
128
  const s = summaryMatch[1].trim();
119
- if (s.length > 5 && !summaries.includes(s)) summaries.push(s);
129
+ if (s.length > 5 && !data.summaries.includes(s)) {
130
+ data.summaries.push(s);
131
+ if (!seenSummaries.has(s)) {
132
+ seenSummaries.add(s);
133
+ const details: string[] = [];
134
+ const actionsMatch = text.match(/ACTIONS:\s*(.+?)(?=\n[A-Z]+:|$)/is);
135
+ if (actionsMatch) {
136
+ const actionLines = actionsMatch[1].split('\n')
137
+ .map(l => l.replace(/^[-*•]\s*/, '').replace(/^\d+\.\s*/, '').trim())
138
+ .filter(l => l.length > 3 && l.length < 100);
139
+ details.push(...actionLines.slice(0, 3));
140
+ }
141
+ data.workItems.push({ title: s, details: details.length > 0 ? details : undefined, completed: true });
142
+ }
143
+ }
120
144
  }
121
145
 
146
+ // Captures
122
147
  const captureMatch = text.match(/CAPTURE:\s*(.+?)(?:\n|$)/i);
123
148
  if (captureMatch) {
124
149
  const c = captureMatch[1].trim();
125
- if (c.length > 5 && !captures.includes(c)) captures.push(c);
150
+ if (c.length > 5 && !data.captures.includes(c)) data.captures.push(c);
126
151
  }
127
152
 
153
+ // Completed
128
154
  const completedMatch = text.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
129
155
  if (completedMatch) {
130
- lastCompleted = completedMatch[1].trim().replace(/\*+/g, '');
156
+ data.lastCompleted = completedMatch[1].trim().replace(/\*+/g, '');
157
+ if (data.workItems.length === 0 && !seenSummaries.has(data.lastCompleted) && data.lastCompleted.length > 5) {
158
+ seenSummaries.add(data.lastCompleted);
159
+ data.workItems.push({ title: data.lastCompleted, completed: true });
160
+ }
131
161
  }
132
- }
133
162
 
134
- // --- Tool use: file modifications ---
135
- if (entry.type === 'assistant' && entry.message?.content && Array.isArray(entry.message.content)) {
136
- for (const block of entry.message.content) {
137
- if (block.type === 'tool_use') {
138
- const tool = block.name;
139
- if ((tool === 'Edit' || tool === 'Write') && block.input?.file_path) {
140
- filesModified.add(block.input.file_path);
163
+ // File modifications (from tool_use blocks)
164
+ if (Array.isArray(entry.message.content)) {
165
+ for (const block of entry.message.content) {
166
+ if (block.type === 'tool_use') {
167
+ const tool = block.name;
168
+ if ((tool === 'Edit' || tool === 'Write') && block.input?.file_path) {
169
+ if (!data.filesModified.includes(block.input.file_path)) {
170
+ data.filesModified.push(block.input.file_path);
171
+ }
172
+ }
141
173
  }
142
174
  }
143
175
  }
144
176
  }
145
177
  }
178
+ } catch (err) {
179
+ console.error(`parseTranscript error: ${err}`);
180
+ }
146
181
 
147
- // Build the output — keep it concise
148
- const parts: string[] = [];
182
+ return data;
183
+ }
149
184
 
150
- if (cwd) {
151
- parts.push(`Working directory: ${cwd}`);
152
- }
185
+ // ---------------------------------------------------------------------------
186
+ // Format session state as human-readable string
187
+ // ---------------------------------------------------------------------------
153
188
 
154
- // Last 3 user messages
155
- const recentUser = userMessages.slice(-3);
156
- if (recentUser.length > 0) {
157
- parts.push('\nRecent user requests:');
158
- for (const msg of recentUser) {
159
- // Trim to first line or 200 chars
160
- const firstLine = msg.split('\n')[0].slice(0, 200);
161
- parts.push(`- ${firstLine}`);
162
- }
163
- }
189
+ function formatSessionState(data: TranscriptData, cwd?: string): string | null {
190
+ const parts: string[] = [];
164
191
 
165
- // Summaries (last 3)
166
- const recentSummaries = summaries.slice(-3);
167
- if (recentSummaries.length > 0) {
168
- parts.push('\nWork summaries:');
169
- for (const s of recentSummaries) {
170
- parts.push(`- ${s.slice(0, 150)}`);
171
- }
172
- }
192
+ if (cwd) parts.push(`Working directory: ${cwd}`);
173
193
 
174
- // Captures (last 5)
175
- const recentCaptures = captures.slice(-5);
176
- if (recentCaptures.length > 0) {
177
- parts.push('\nCaptured context:');
178
- for (const c of recentCaptures) {
179
- parts.push(`- ${c.slice(0, 150)}`);
180
- }
194
+ const recentUser = data.userMessages.slice(-3);
195
+ if (recentUser.length > 0) {
196
+ parts.push('\nRecent user requests:');
197
+ for (const msg of recentUser) {
198
+ parts.push(`- ${msg.split('\n')[0].slice(0, 200)}`);
181
199
  }
200
+ }
182
201
 
183
- // Files modified (last 10)
184
- const files = Array.from(filesModified).slice(-10);
185
- if (files.length > 0) {
186
- parts.push('\nFiles modified this session:');
187
- for (const f of files) {
188
- parts.push(`- ${f}`);
189
- }
190
- }
202
+ const recentSummaries = data.summaries.slice(-3);
203
+ if (recentSummaries.length > 0) {
204
+ parts.push('\nWork summaries:');
205
+ for (const s of recentSummaries) parts.push(`- ${s.slice(0, 150)}`);
206
+ }
191
207
 
192
- if (lastCompleted) {
193
- parts.push(`\nLast completed: ${lastCompleted.slice(0, 150)}`);
194
- }
208
+ const recentCaptures = data.captures.slice(-5);
209
+ if (recentCaptures.length > 0) {
210
+ parts.push('\nCaptured context:');
211
+ for (const c of recentCaptures) parts.push(`- ${c.slice(0, 150)}`);
212
+ }
195
213
 
196
- const result = parts.join('\n');
197
- return result.length > 50 ? result : null;
198
- } catch (err) {
199
- console.error(`extractSessionState error: ${err}`);
200
- return null;
214
+ const files = data.filesModified.slice(-10);
215
+ if (files.length > 0) {
216
+ parts.push('\nFiles modified this session:');
217
+ for (const f of files) parts.push(`- ${f}`);
218
+ }
219
+
220
+ if (data.lastCompleted) {
221
+ parts.push(`\nLast completed: ${data.lastCompleted.slice(0, 150)}`);
201
222
  }
223
+
224
+ const result = parts.join('\n');
225
+ return result.length > 50 ? result : null;
202
226
  }
203
227
 
204
228
  // ---------------------------------------------------------------------------
205
- // Work item extraction (same pattern as stop-hook.ts)
229
+ // Derive a meaningful title for the session note
206
230
  // ---------------------------------------------------------------------------
207
231
 
208
- function extractWorkFromTranscript(transcriptPath: string): WorkItem[] {
209
- try {
210
- const raw = readFileSync(transcriptPath, 'utf-8');
211
- const lines = raw.trim().split('\n');
212
- const workItems: WorkItem[] = [];
213
- const seenSummaries = new Set<string>();
214
-
215
- for (const line of lines) {
216
- if (!line.trim()) continue;
217
- let entry: any;
218
- try { entry = JSON.parse(line); } catch { continue; }
219
-
220
- if (entry.type === 'assistant' && entry.message?.content) {
221
- const text = contentToText(entry.message.content);
222
-
223
- const summaryMatch = text.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
224
- if (summaryMatch) {
225
- const summary = summaryMatch[1].trim();
226
- if (summary && !seenSummaries.has(summary) && summary.length > 5) {
227
- seenSummaries.add(summary);
228
- const details: string[] = [];
229
- const actionsMatch = text.match(/ACTIONS:\s*(.+?)(?=\n[A-Z]+:|$)/is);
230
- if (actionsMatch) {
231
- const actionLines = actionsMatch[1].split('\n')
232
- .map(l => l.replace(/^[-*•]\s*/, '').replace(/^\d+\.\s*/, '').trim())
233
- .filter(l => l.length > 3 && l.length < 100);
234
- details.push(...actionLines.slice(0, 3));
235
- }
236
- workItems.push({ title: summary, details: details.length > 0 ? details : undefined, completed: true });
237
- }
238
- }
232
+ function deriveTitle(data: TranscriptData): string {
233
+ let title = '';
239
234
 
240
- const completedMatch = text.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
241
- if (completedMatch && workItems.length === 0) {
242
- const completed = completedMatch[1].trim().replace(/\*+/g, '');
243
- if (completed && !seenSummaries.has(completed) && completed.length > 5) {
244
- seenSummaries.add(completed);
245
- workItems.push({ title: completed, completed: true });
246
- }
247
- }
235
+ // 1. Last work item title (most descriptive of what was accomplished)
236
+ if (data.workItems.length > 0) {
237
+ title = data.workItems[data.workItems.length - 1].title;
238
+ }
239
+ // 2. Last summary
240
+ else if (data.summaries.length > 0) {
241
+ title = data.summaries[data.summaries.length - 1];
242
+ }
243
+ // 3. Last completed marker
244
+ else if (data.lastCompleted && data.lastCompleted.length > 5) {
245
+ title = data.lastCompleted;
246
+ }
247
+ // 4. Last substantive user message
248
+ else if (data.userMessages.length > 0) {
249
+ for (let i = data.userMessages.length - 1; i >= 0; i--) {
250
+ const msg = data.userMessages[i].split('\n')[0].trim();
251
+ if (msg.length > 10 && msg.length < 80 &&
252
+ !msg.toLowerCase().startsWith('yes') &&
253
+ !msg.toLowerCase().startsWith('ok')) {
254
+ title = msg;
255
+ break;
248
256
  }
249
257
  }
250
- return workItems;
251
- } catch {
252
- return [];
253
258
  }
259
+ // 5. Derive from files modified
260
+ if (!title && data.filesModified.length > 0) {
261
+ const basenames = data.filesModified.slice(-5).map(f => {
262
+ const b = basename(f);
263
+ return b.replace(/\.[^.]+$/, '');
264
+ });
265
+ const unique = [...new Set(basenames)];
266
+ title = unique.length <= 3
267
+ ? `Updated ${unique.join(', ')}`
268
+ : `Modified ${data.filesModified.length} files`;
269
+ }
270
+
271
+ // Clean up for filename use
272
+ return title
273
+ .replace(/[^\w\s-]/g, ' ') // Remove special chars
274
+ .replace(/\s+/g, ' ') // Normalize whitespace
275
+ .trim()
276
+ .substring(0, 60);
254
277
  }
255
278
 
256
279
  // ---------------------------------------------------------------------------
@@ -287,45 +310,81 @@ async function main() {
287
310
  ? `${Math.round(tokenCount / 1000)}k`
288
311
  : String(tokenCount);
289
312
 
313
+ // -----------------------------------------------------------------
314
+ // Single-pass transcript parsing
315
+ // -----------------------------------------------------------------
316
+ const data = parseTranscript(hookInput.transcript_path);
317
+ const state = formatSessionState(data, hookInput.cwd);
318
+
290
319
  // -----------------------------------------------------------------
291
320
  // Persist session state to numbered session note (like "pause session")
292
321
  // -----------------------------------------------------------------
293
- const state = extractSessionState(hookInput.transcript_path, hookInput.cwd);
322
+ let notePath: string | null = null;
294
323
 
295
324
  try {
296
- // Find notes dir — prefer local, fallback to central
297
325
  const notesInfo = hookInput.cwd
298
326
  ? findNotesDir(hookInput.cwd)
299
327
  : { path: join(dirname(hookInput.transcript_path), 'Notes'), isLocal: false };
300
- const currentNotePath = getCurrentNotePath(notesInfo.path);
301
-
302
- if (currentNotePath) {
303
- // 1. Write rich checkpoint with full session state
304
- const checkpointBody = state
305
- ? `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.\n\n${state}`
306
- : `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.`;
307
- appendCheckpoint(currentNotePath, checkpointBody);
308
-
309
- // 2. Write work items to "Work Done" section (same as stop-hook)
310
- const workItems = extractWorkFromTranscript(hookInput.transcript_path);
311
- if (workItems.length > 0) {
312
- addWorkToSessionNote(currentNotePath, workItems, `Pre-Compact (~${tokenDisplay} tokens)`);
313
- console.error(`Added ${workItems.length} work item(s) to session note`);
314
- }
328
+ notePath = getCurrentNotePath(notesInfo.path);
329
+
330
+ // If no note found, or the latest note is completed, create a new one
331
+ if (!notePath) {
332
+ console.error('No session note found — creating one for checkpoint');
333
+ notePath = createSessionNote(notesInfo.path, 'Recovered Session');
334
+ } else {
335
+ try {
336
+ const noteContent = readFileSync(notePath, 'utf-8');
337
+ if (noteContent.includes('**Status:** Completed') || noteContent.includes('**Completed:**')) {
338
+ console.error(`Latest note is completed (${basename(notePath)}) — creating new one`);
339
+ notePath = createSessionNote(notesInfo.path, 'Continued Session');
340
+ }
341
+ } catch { /* proceed with existing note */ }
342
+ }
343
+
344
+ // 1. Write rich checkpoint with full session state
345
+ const checkpointBody = state
346
+ ? `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.\n\n${state}`
347
+ : `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.`;
348
+ appendCheckpoint(notePath, checkpointBody);
315
349
 
316
- console.error(`Rich checkpoint saved: ${basename(currentNotePath)}`);
350
+ // 2. Write work items to "Work Done" section
351
+ if (data.workItems.length > 0) {
352
+ addWorkToSessionNote(notePath, data.workItems, `Pre-Compact (~${tokenDisplay} tokens)`);
353
+ console.error(`Added ${data.workItems.length} work item(s) to session note`);
317
354
  }
355
+
356
+ // 3. Rename session note with a meaningful title (instead of "New Session")
357
+ const title = deriveTitle(data);
358
+ if (title) {
359
+ const newPath = renameSessionNote(notePath, title);
360
+ if (newPath !== notePath) {
361
+ // Update H1 title inside the note to match
362
+ try {
363
+ let noteContent = readFileSync(newPath, 'utf-8');
364
+ noteContent = noteContent.replace(
365
+ /^(# Session \d+:)\s*.*$/m,
366
+ `$1 ${title}`
367
+ );
368
+ writeFileSync(newPath, noteContent);
369
+ console.error(`Updated note H1 to match rename`);
370
+ } catch { /* ignore */ }
371
+ notePath = newPath;
372
+ }
373
+ }
374
+
375
+ console.error(`Rich checkpoint saved: ${basename(notePath)}`);
318
376
  } catch (noteError) {
319
377
  console.error(`Could not save checkpoint: ${noteError}`);
320
378
  }
321
379
 
322
380
  // -----------------------------------------------------------------
323
- // Update TODO.md with checkpoint (like "pause session")
381
+ // Update TODO.md with proper ## Continue section (like "pause session")
324
382
  // -----------------------------------------------------------------
325
- if (hookInput.cwd && state) {
383
+ if (hookInput.cwd && notePath) {
326
384
  try {
327
- addTodoCheckpoint(hookInput.cwd, `Pre-compact checkpoint (~${tokenDisplay} tokens):\n${state}`);
328
- console.error('TODO.md checkpoint added');
385
+ const noteFilename = basename(notePath);
386
+ updateTodoContinue(hookInput.cwd, noteFilename, state, tokenDisplay);
387
+ console.error('TODO.md ## Continue section updated');
329
388
  } catch (todoError) {
330
389
  console.error(`Could not update TODO.md: ${todoError}`);
331
390
  }