2026-01-23 15:08:07 +08:00
#!/usr/bin/env node
/ * *
* Stop Hook ( Session End ) - Persist learnings when session ends
*
* Cross - platform ( Windows , macOS , Linux )
*
2026-02-11 23:56:41 -08:00
* Runs when Claude session ends . Extracts a meaningful summary from
2026-02-12 15:33:55 -08:00
* the session transcript ( via stdin JSON transcript _path ) and saves it
2026-02-11 23:56:41 -08:00
* to a session file for cross - session continuity .
2026-01-23 15:08:07 +08:00
* /
const path = require ( 'path' ) ;
const fs = require ( 'fs' ) ;
const {
getSessionsDir ,
getDateString ,
getTimeString ,
2026-01-25 18:21:27 -08:00
getSessionIdShort ,
2026-01-23 15:08:07 +08:00
ensureDir ,
2026-02-11 23:56:41 -08:00
readFile ,
2026-01-23 15:08:07 +08:00
writeFile ,
replaceInFile ,
log
} = require ( '../lib/utils' ) ;
2026-02-11 23:56:41 -08:00
/ * *
* Extract a meaningful summary from the session transcript .
* Reads the JSONL transcript and pulls out key information :
* - User messages ( tasks requested )
* - Tools used
* - Files modified
* /
function extractSessionSummary ( transcriptPath ) {
const content = readFile ( transcriptPath ) ;
if ( ! content ) return null ;
const lines = content . split ( '\n' ) . filter ( Boolean ) ;
const userMessages = [ ] ;
const toolsUsed = new Set ( ) ;
const filesModified = new Set ( ) ;
2026-02-12 07:06:53 -08:00
let parseErrors = 0 ;
2026-02-11 23:56:41 -08:00
for ( const line of lines ) {
try {
const entry = JSON . parse ( line ) ;
// Collect user messages (first 200 chars each)
if ( entry . type === 'user' || entry . role === 'user' ) {
const text = typeof entry . content === 'string'
? entry . content
: Array . isArray ( entry . content )
fix: 2 bugs fixed, 17 tests added for hook scripts
Bug fixes:
- evaluate-session.js: whitespace-tolerant regex for counting user
messages in JSONL transcripts (/"type":"user"/ → /"type"\s*:\s*"user"/)
- session-end.js: guard against null elements in content arrays
(c.text → (c && c.text) to prevent TypeError)
New tests (17):
- evaluate-session: whitespace JSON regression test
- session-end: null content array elements regression test
- post-edit-console-warn: 5 tests (warn, skip non-JS, clean files,
missing file, stdout passthrough)
- post-edit-format: 3 tests (empty stdin, non-JS skip, invalid JSON)
- post-edit-typecheck: 4 tests (empty stdin, non-TS skip, missing file,
no tsconfig)
Total test count: 191 (up from 164)
2026-02-12 16:02:31 -08:00
? entry . content . map ( c => ( c && c . text ) || '' ) . join ( ' ' )
2026-02-11 23:56:41 -08:00
: '' ;
if ( text . trim ( ) ) {
userMessages . push ( text . trim ( ) . slice ( 0 , 200 ) ) ;
}
}
// Collect tool names and modified files
if ( entry . type === 'tool_use' || entry . tool _name ) {
const toolName = entry . tool _name || entry . name || '' ;
if ( toolName ) toolsUsed . add ( toolName ) ;
const filePath = entry . tool _input ? . file _path || entry . input ? . file _path || '' ;
if ( filePath && ( toolName === 'Edit' || toolName === 'Write' ) ) {
filesModified . add ( filePath ) ;
}
}
} catch {
2026-02-12 07:06:53 -08:00
parseErrors ++ ;
2026-02-11 23:56:41 -08:00
}
}
2026-02-12 07:06:53 -08:00
if ( parseErrors > 0 ) {
log ( ` [SessionEnd] Skipped ${ parseErrors } / ${ lines . length } unparseable transcript lines ` ) ;
}
2026-02-11 23:56:41 -08:00
if ( userMessages . length === 0 ) return null ;
return {
userMessages : userMessages . slice ( - 10 ) , // Last 10 user messages
toolsUsed : Array . from ( toolsUsed ) . slice ( 0 , 20 ) ,
filesModified : Array . from ( filesModified ) . slice ( 0 , 30 ) ,
totalMessages : userMessages . length
} ;
}
2026-02-12 15:33:55 -08:00
// Read hook input from stdin (Claude Code provides transcript_path via stdin JSON)
const MAX _STDIN = 1024 * 1024 ;
let stdinData = '' ;
fix: 3 bugs fixed, stdin encoding hardened, 37 CI validator tests added
Bug fixes:
- utils.js: glob-to-regex conversion now escapes all regex special chars
(+, ^, $, |, (), {}, [], \) before converting * and ? wildcards
- validate-hooks.js: escape sequence processing order corrected —
\\\\ now processed before \\n and \\t to prevent double-processing
- 6 hooks: added process.stdin.setEncoding('utf8') to prevent
multi-byte UTF-8 character corruption at chunk boundaries
(check-console-log, post-edit-format, post-edit-typecheck,
post-edit-console-warn, session-end, evaluate-session)
New tests (37):
- CI validator test suite (tests/ci/validators.test.js):
- validate-agents: 9 tests (real project, frontmatter parsing,
BOM/CRLF, colons in values, missing fields, non-md skip)
- validate-hooks: 13 tests (real project, invalid JSON, invalid
event types, missing fields, async/timeout validation, inline JS
syntax, array commands, legacy format)
- validate-skills: 6 tests (real project, missing SKILL.md, empty
files, non-directory entries)
- validate-commands: 5 tests (real project, empty files, non-md skip)
- validate-rules: 4 tests (real project, empty files)
Total test count: 228 (up from 191)
2026-02-12 16:08:49 -08:00
process . stdin . setEncoding ( 'utf8' ) ;
2026-02-12 15:33:55 -08:00
process . stdin . on ( 'data' , chunk => {
if ( stdinData . length < MAX _STDIN ) {
stdinData += chunk ;
}
} ) ;
process . stdin . on ( 'end' , ( ) => {
runMain ( ) ;
} ) ;
function runMain ( ) {
main ( ) . catch ( err => {
console . error ( '[SessionEnd] Error:' , err . message ) ;
process . exit ( 0 ) ;
} ) ;
}
2026-01-23 15:08:07 +08:00
async function main ( ) {
2026-02-12 15:33:55 -08:00
// Parse stdin JSON to get transcript_path
let transcriptPath = null ;
try {
const input = JSON . parse ( stdinData ) ;
transcriptPath = input . transcript _path ;
} catch {
// Fallback: try env var for backwards compatibility
transcriptPath = process . env . CLAUDE _TRANSCRIPT _PATH ;
}
2026-01-23 15:08:07 +08:00
const sessionsDir = getSessionsDir ( ) ;
const today = getDateString ( ) ;
2026-01-25 18:21:27 -08:00
const shortId = getSessionIdShort ( ) ;
const sessionFile = path . join ( sessionsDir , ` ${ today } - ${ shortId } -session.tmp ` ) ;
2026-01-23 15:08:07 +08:00
ensureDir ( sessionsDir ) ;
const currentTime = getTimeString ( ) ;
2026-02-11 23:56:41 -08:00
// Try to extract summary from transcript
let summary = null ;
2026-02-12 13:40:14 -08:00
if ( transcriptPath ) {
if ( fs . existsSync ( transcriptPath ) ) {
summary = extractSessionSummary ( transcriptPath ) ;
} else {
log ( ` [SessionEnd] Transcript not found: ${ transcriptPath } ` ) ;
}
2026-02-11 23:56:41 -08:00
}
2026-01-23 15:08:07 +08:00
if ( fs . existsSync ( sessionFile ) ) {
2026-02-11 23:56:41 -08:00
// Update existing session file
2026-02-12 13:40:14 -08:00
const updated = replaceInFile (
2026-01-23 15:08:07 +08:00
sessionFile ,
/\*\*Last Updated:\*\*.*/ ,
` **Last Updated:** ${ currentTime } `
) ;
2026-02-12 13:40:14 -08:00
if ( ! updated ) {
log ( ` [SessionEnd] Failed to update timestamp in ${ sessionFile } ` ) ;
}
2026-01-23 15:08:07 +08:00
2026-02-11 23:56:41 -08:00
// If we have a new summary and the file still has the blank template, replace it
if ( summary ) {
const existing = readFile ( sessionFile ) ;
if ( existing && existing . includes ( '[Session context goes here]' ) ) {
fix: broken cross-references, version sync, and enhanced command validator
- Fix /build-and-fix → /build-fix in tdd.md, plan.md (+ cursor, zh-CN)
- Fix non-existent explorer agent → planner in orchestrate.md (+ cursor, zh-CN, zh-TW)
- Fix /python-test → /tdd in python-review.md (+ cursor, zh-CN)
- Sync package.json version from 1.0.0 to 1.4.1 to match plugin.json
- Enhance validate-commands.js with cross-reference checking:
command refs, agent path refs, skill dir refs, workflow diagrams
- Strip fenced code blocks before scanning to avoid false positives
- Skip hypothetical "Creates:" lines in evolve.md examples
- Add 46 new tests (suggest-compact, session-manager, utils, hooks)
2026-02-12 16:19:04 -08:00
// Use a flexible regex that tolerates CRLF, extra whitespace, and minor template variations
2026-02-11 23:56:41 -08:00
const updatedContent = existing . replace (
fix: broken cross-references, version sync, and enhanced command validator
- Fix /build-and-fix → /build-fix in tdd.md, plan.md (+ cursor, zh-CN)
- Fix non-existent explorer agent → planner in orchestrate.md (+ cursor, zh-CN, zh-TW)
- Fix /python-test → /tdd in python-review.md (+ cursor, zh-CN)
- Sync package.json version from 1.0.0 to 1.4.1 to match plugin.json
- Enhance validate-commands.js with cross-reference checking:
command refs, agent path refs, skill dir refs, workflow diagrams
- Strip fenced code blocks before scanning to avoid false positives
- Skip hypothetical "Creates:" lines in evolve.md examples
- Add 46 new tests (suggest-compact, session-manager, utils, hooks)
2026-02-12 16:19:04 -08:00
/## Current State\s*\n\s*\[Session context goes here\][\s\S]*?### Context to Load\s*\n```\s*\n\[relevant files\]\s*\n```/ ,
2026-02-11 23:56:41 -08:00
buildSummarySection ( summary )
) ;
writeFile ( sessionFile , updatedContent ) ;
}
2026-01-23 15:08:07 +08:00
}
2026-02-11 23:56:41 -08:00
log ( ` [SessionEnd] Updated session file: ${ sessionFile } ` ) ;
2026-01-23 15:08:07 +08:00
} else {
2026-02-11 23:56:41 -08:00
// Create new session file
const summarySection = summary
? buildSummarySection ( summary )
: ` ## Current State \n \n [Session context goes here] \n \n ### Completed \n - [ ] \n \n ### In Progress \n - [ ] \n \n ### Notes for Next Session \n - \n \n ### Context to Load \n \` \` \` \n [relevant files] \n \` \` \` ` ;
2026-01-23 15:08:07 +08:00
const template = ` # Session: ${ today }
* * Date : * * $ { today }
* * Started : * * $ { currentTime }
* * Last Updated : * * $ { currentTime }
-- -
2026-02-11 23:56:41 -08:00
$ { summarySection }
` ;
2026-01-23 15:08:07 +08:00
2026-02-11 23:56:41 -08:00
writeFile ( sessionFile , template ) ;
log ( ` [SessionEnd] Created session file: ${ sessionFile } ` ) ;
}
2026-01-23 15:08:07 +08:00
2026-02-11 23:56:41 -08:00
process . exit ( 0 ) ;
}
2026-01-23 15:08:07 +08:00
2026-02-11 23:56:41 -08:00
function buildSummarySection ( summary ) {
let section = '## Session Summary\n\n' ;
2026-01-23 15:08:07 +08:00
2026-02-12 14:14:21 -08:00
// Tasks (from user messages — escape backticks to prevent markdown breaks)
2026-02-11 23:56:41 -08:00
section += '### Tasks\n' ;
for ( const msg of summary . userMessages ) {
2026-02-12 14:14:21 -08:00
section += ` - ${ msg . replace ( /`/g , '\\`' ) } \n ` ;
2026-02-11 23:56:41 -08:00
}
section += '\n' ;
2026-01-23 15:08:07 +08:00
2026-02-11 23:56:41 -08:00
// Files modified
if ( summary . filesModified . length > 0 ) {
section += '### Files Modified\n' ;
for ( const f of summary . filesModified ) {
section += ` - ${ f } \n ` ;
}
section += '\n' ;
}
2026-01-23 15:08:07 +08:00
2026-02-11 23:56:41 -08:00
// Tools used
if ( summary . toolsUsed . length > 0 ) {
section += ` ### Tools Used \n ${ summary . toolsUsed . join ( ', ' ) } \n \n ` ;
2026-01-23 15:08:07 +08:00
}
2026-02-11 23:56:41 -08:00
section += ` ### Stats \n - Total user messages: ${ summary . totalMessages } \n ` ;
return section ;
2026-01-23 15:08:07 +08:00
}