Adds a short 'where we left off' brief (last journal entries, memory state, in-progress plan phase) to the session's system prompt

Gives Claude a short "where we left off" brief from the project's working journals, so "resume" / "reprends" works reliably in a new session. Complements auto-context (git state and TODOS.md), which it does not duplicate.
COMMANDS.md: the last 3 ## N — date entries (each cut to 200 characters)MEMORY.md: the last 4 bullets of its ## State section (300 characters each)PLAN.md: the ## Phase … headings marked 🟡, with their 🟡/⬜ sub-bullets (8 lines max)CHANGES.md: the last 5 non-empty lines| Command | Effect |
|---|---|
/resume-brief | Shows the brief currently injected |
/resume-brief off | Stops injecting (kept across sessions) |
/resume-brief on | Resumes injecting and refreshes |
claude --plugin-dir /path/to/ModsTools/mods/resume-brief
## N — date headings in COMMANDS.md, a ## State section in MEMORY.md, 🟡/⬜ markers in PLAN.md); other layouts yield nothing for that file.claude plugin validate mods/resume-brief
claude plugin test mods/resume-brief # 14 testshooks/register.ts 65 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { buildBrief } from './brief'
5
6const SECTION = 'resume-brief:session'
7const text = atom({ plugin: 'resume-brief', key: 'text' } as const, null)
8
9async function isEnabled($: EngineInterface): Promise<boolean> {
10 return (await $.store.get('enabled')) !== false
11}
12
13async function load($: EngineInterface, cwd: string, name: string): Promise<string | null> {
14 try {
15 return await $.fs.read(`${cwd}/${name}`)
16 } catch {
17 return null
18 }
19}
20
21async function refresh($: EngineInterface) {
22 const cwd = await $.session.cwd()
23 const [commands, memory, plan, changes] = await Promise.all(['COMMANDS.md', 'MEMORY.md', 'PLAN.md', 'CHANGES.md'].map(name => load($, cwd, name)))
24 const value = buildBrief({ commands, memory, plan, changes })
25 await update($, text, () => value)
26 $.ui.invalidate('prompt.section')
27}
28
29export const register: Register = on => {
30 on('session.start', async ($, e, next) => {
31 await $.command.register({ name: 'resume-brief', description: 'Show the "where we left off" brief; "off" or "on" toggles it' })
32 await refresh($)
33
34 return next(e)
35 })
36
37 on('turn.complete', async ($, e, next) => {
38 if (e.agentId === undefined) await refresh($)
39
40 return next(e)
41 })
42
43 on('prompt.compose', async ($, e, next) => {
44 const composed = await next(e)
45 const value = await read($, text)
46 if (value === null || !(await isEnabled($))) return composed
47
48 return { sections: [...composed.sections, { id: SECTION, text: value, scope: 'session' as const }] }
49 })
50
51 on('command.run', { command: 'resume-brief' }, async ($, e) => {
52 const arg = e.args.trim()
53 if (arg === 'on' || arg === 'off') {
54 await $.store.set('enabled', arg === 'on')
55 $.ui.invalidate('prompt.section')
56 if (arg === 'on') await refresh($)
57
58 return { text: `resume-brief ${arg}.` }
59 }
60 if (!(await isEnabled($))) return { text: 'resume-brief is off.' }
61
62 return { text: (await read($, text)) ?? 'Nothing to brief (no COMMANDS.md, MEMORY.md, PLAN.md or CHANGES.md content).' }
63 })
64}
65hooks/brief.ts 75 lines1export type Sources = { commands?: string | null; memory?: string | null; plan?: string | null; changes?: string | null }
2
3const MAX_LINES = 30
4
5function lines(text: string): string[] {
6 return text.replace(/\r\n?/g, '\n').split('\n')
7}
8
9export function truncate(text: string, max: number): string {
10 const chars = Array.from(text.replace(/\s+/g, ' ').trim())
11
12 return chars.length > max ? `${chars.slice(0, max - 1).join('')}…` : chars.join('')
13}
14
15export function lastCommands(text: string, count = 3): string[] {
16 const entries: { head: string; body: string[] }[] = []
17 for (const line of lines(text)) {
18 const m = /^##\s+(\d+)\s*[—–-]\s*(\S.*)$/.exec(line)
19 if (m) entries.push({ head: `#${m[1]} ${m[2]?.trim()}`, body: [] })
20 else entries[entries.length - 1]?.body.push(line)
21 }
22
23 return entries
24 .slice(-count)
25 .map(e => `${e.head}: ${truncate(e.body.join(' '), 200)}`)
26}
27
28export function stateBullets(text: string, count = 4): string[] {
29 const bullets: string[] = []
30 let inState = false
31 for (const line of lines(text)) {
32 if (/^##\s/.test(line)) inState = /^##\s+State\s*$/i.test(line)
33 else if (inState && /^[-*]\s+\S/.test(line)) bullets.push(line.replace(/^[-*]\s+/, ''))
34 else if (inState && bullets.length > 0 && /^\s+\S/.test(line)) bullets[bullets.length - 1] += ` ${line.trim()}`
35 }
36
37 return bullets.slice(-count).map(b => `- ${truncate(b, 300)}`)
38}
39
40export function inProgress(text: string, max = 8): string[] {
41 const out: string[] = []
42 let active = false
43 for (const line of lines(text)) {
44 if (/^##\s/.test(line)) {
45 active = /^##\s+Phase\b/.test(line) && line.includes('🟡')
46 if (active) out.push(truncate(line.replace(/^##\s+/, ''), 160))
47 } else if (active && /^\s*[-*]\s+(🟡|⬜)/.test(line)) out.push(` ${truncate(line.replace(/^\s*[-*]\s+/, '- '), 160)}`)
48 }
49
50 return out.slice(0, max)
51}
52
53export function lastLines(text: string, count = 5): string[] {
54 return lines(text)
55 .filter(l => l.trim() !== '')
56 .slice(-count)
57 .map(l => truncate(l, 200))
58}
59
60function section(title: string, body: string[]): string[] {
61 return body.length > 0 ? [`${title}:`, ...body] : []
62}
63
64export function buildBrief(src: Sources): string | null {
65 const parts = [
66 ...section('Last user messages (COMMANDS.md)', lastCommands(src.commands ?? '')),
67 ...section('Project state (MEMORY.md)', stateBullets(src.memory ?? '')),
68 ...section('In progress (PLAN.md)', inProgress(src.plan ?? '')),
69 ...section('Recent changes (CHANGES.md)', lastLines(src.changes ?? '')),
70 ]
71 if (parts.length === 0) return null
72
73 return ['Where we left off (from the project journals):', ...parts].slice(0, MAX_LINES).join('\n')
74}
75types/index.d.ts 8 lines1export type BriefText = string | null
2
3declare module 'claude-code' {
4 interface PluginState {
5 'resume-brief': { text: BriefText }
6 }
7}
8