Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:58:13 +08:00
commit c2e58f5d7e
12 changed files with 3441 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
{
"name": "tabula-scripta",
"description": "Working memory system for Claude Code - integrates with Obsidian via MCP for persistent, cross-session memory management",
"version": "0.1.0",
"author": {
"name": "Drew Ritter",
"email": "drew@ritter.dev"
},
"skills": [
"./skills"
],
"commands": [
"./commands"
],
"hooks": [
"./hooks"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# tabula-scripta
Working memory system for Claude Code - integrates with Obsidian via MCP for persistent, cross-session memory management

386
commands/recall.md Normal file
View File

@@ -0,0 +1,386 @@
# Recall - Search Existing Memories
Search for existing memory notes in the Obsidian vault and present the top results.
## Usage
```
/recall [query]
```
**Arguments:**
- `query` - Search terms to find relevant memories (required)
**Examples:**
```
/recall git worktrees
/recall debugging race condition
/recall react hooks patterns
/recall authentication flow
```
## Implementation
When this command is invoked:
### 1. Parse and Validate Query
1. Parse the query by trimming whitespace from the command input
2. Validate that the query is not empty
- If query is empty, return error message: "Query is required. Usage: /recall [query]"
### 2. Detect Project Context
1. Attempt to detect the current project from the git repository:
- Run `git rev-parse --show-toplevel` to find the git repository root
- Extract the project name from the repository directory name
2. If not in a git repository:
- Fall back to using the current working directory name
- If that fails, use 'default' as the project name
3. Store the detected project name for scoped search
### 3. Attempt Semantic Search (Smart Connections)
**Note:** Semantic search via Smart Connections plugin is an optional enhancement. If available, it provides better relevance ranking and conceptual matching.
1. Set initial search method to 'semantic' and initialize empty results array
2. Attempt to use semantic search (if Smart Connections plugin is installed):
- Try to perform semantic search across both project notes and global entities
- Search path: `claude/projects/{currentProject}/**` and `claude/global/**`
- Retrieve top 10 results for ranking
3. If Smart Connections is not available or fails:
- Set search method to 'text' for fallback
- Continue to text search in next step
**Implementation Note:** Since obsidian-mcp-plugin does not provide native Smart Connections integration, this step may require custom MCP extensions or can be skipped in favor of text search only.
### 4. Perform Text Search
If semantic search was unavailable or returned no results, use text search:
1. Invoke MCP `search_notes` operation for project notes:
- vault: `~/.claude-memory`
- query: The user's search query
- path_filter: `claude/projects/{currentProject}/**`
2. Invoke MCP `search_notes` operation for global entities:
- vault: `~/.claude-memory`
- query: The user's search query
- path_filter: `claude/global/entities/**`
3. Combine the project results and global results, removing any duplicates
4. Set search method to 'text'
5. Handle errors:
**If MCPUnavailableError:**
- Return graceful degradation message explaining:
- MCP server is unavailable
- Steps to restore: ensure Obsidian is running, check plugin installation, verify config
- Reference to docs/setup-guide.md
**If other error:**
- Return error message with details
### 5. Rank and Filter Results
1. Rank results by relevance:
- If semantic search was used: Results are already ranked by vector similarity
- If text search was used: Rank by match frequency and recency
- Calculate match count: How many times the query appears in the result
- Calculate recency boost: More recent notes ranked higher
- Combine into relevance score: match count + (recency boost × 10)
- Sort results by score in descending order
2. Limit to top 5 results for presentation
3. Check if no results were found:
- If results are empty, return info message with:
- "No memories found for query: '{query}'"
- Suggestions: Try different terms, use /remember to create memories, search more broadly
- Current project and search scope information
### 6. Track Cross-Project Recalls
Use the cross-project tracking patterns defined in the managing-working-memory skill:
1. Identify cross-project recalls by filtering results:
- A recall is cross-project if the note's project differs from the current project
- Exclude global entities (project = 'global') as they're expected to be cross-project
2. For each cross-project entity recall:
- Load the current note using MCP `read_note`
- Append a new entry to the `cross_project_recalls` frontmatter array:
- project: Current project name
- date: Current date (YYYY-MM-DD)
- context: "Recalled via search: '{query}'"
- Update the note using MCP `update_note` with:
- Updated cross_project_recalls array
- Updated claude_last_accessed timestamp
3. Check if promotion threshold is met:
- If cross_project_recalls length >= 3, trigger promotion prompt (see below)
- Promotion prompt is handled by the managing-working-memory skill
**Promotion Prompt Format (I4):**
When an entity reaches 3 cross-project recalls, display:
```
I've referenced [[{Entity Name}]] from {source-project} while working on other projects 3 times now:
1. {project-name} ({date}): {context}
2. {project-name} ({date}): {context}
3. {project-name} ({date}): {context}
This pattern seems reusable across projects. Should I promote it to global knowledge?
Options:
1. Yes, promote to global (move to ~/.claude-memory/claude/global/entities/)
2. Remind me later (ask again after 5 cross-project recalls)
3. No, it's project-specific (stop tracking)
What should I do?
```
User responses:
- If option 1: Execute promotion process (move entity, update frontmatter, create redirect)
- If option 2: Continue tracking, increase threshold to 5
- If option 3: Clear cross_project_recalls array and stop tracking
4. Handle errors silently:
- Cross-project tracking is best-effort
- Log warnings for failed tracking but don't interrupt the search flow
### 7. Present Results
1. Format the output message with:
- Header: "Found {count} {memory/memories} for: '{query}'"
- Search method used: Semantic or Text search
- Current project name
- Blank line
2. For each result (numbered 1-5):
- Extract title from path (filename without extension)
- Get type, project, and updated date from frontmatter
- Get snippet preview from search result
- Format as:
```
{number}. [[{title}]] ({type})
Project: {project}
Updated: {updated}
Preview: {first 150 chars of snippet}...
```
3. Add footer with options:
- "Type a number (1-{count}) to load full note content"
- "Continue conversation to work with these memories"
- "Use /remember to create a new memory if nothing matches"
4. Return the formatted output as a success message
## Error Handling
### Missing Query
**Input:** `/recall`
**Output:**
```
Error: Query is required.
Usage: /recall [query]
Examples:
/recall git worktrees
/recall debugging race condition
/recall react hooks patterns
```
### No Results Found
**Input:** `/recall nonexistent topic`
**Output:**
```
No memories found for query: "nonexistent topic"
Suggestions:
- Try different search terms
- Check if you've created memories for this topic (use /remember)
- Search more broadly (fewer specific terms)
Current project: tabula-scripta
Searched: Project notes + global entities
```
### MCP Unavailable
**Input:** `/recall authentication` (when MCP server is down)
**Output:**
```
Obsidian MCP server is unavailable. Cannot search memories.
To restore memory features:
1. Ensure Obsidian is running
2. Check obsidian-mcp-plugin is installed in ~/.claude-memory/
3. Verify Claude Code config includes MCP server
See docs/setup-guide.md for troubleshooting.
```
## Success Output
**Input:** `/recall git worktrees`
**Output:**
```
Found 3 memories for: "git worktrees"
Search method: Semantic (Smart Connections)
Project: tabula-scripta
1. [[Git Worktrees]] (entity)
Project: tabula-scripta
Updated: 2025-11-18
Preview: Git worktrees enable isolated directory trees for parallel development. Each worktree has its own working directory but shares the .git repository...
2. [[Parallel Execution Patterns]] (topic)
Project: global
Updated: 2025-11-17
Preview: Techniques for concurrent task execution including git worktrees, background processes, and isolation strategies...
3. [[2025-11-18 - Spectacular Implementation]] (session)
Project: tabula-scripta
Updated: 2025-11-18
Preview: Implementing parallel phase execution using git worktrees for task isolation. Decision: Use trap handlers for cleanup...
Options:
- Type a number (1-3) to load full note content
- Continue conversation to work with these memories
- Use /remember to create a new memory if nothing matches
```
## Search Scope
The `/recall` command searches:
1. **Current project notes:**
- `claude/projects/{current-project}/sessions/**`
- `claude/projects/{current-project}/entities/**`
2. **Global entities:**
- `claude/global/entities/**`
- `claude/global/topics/**`
3. **Excludes:**
- Archived sessions (`claude/projects/{project}/archive/**`)
- Other project's sessions (unless global)
## Semantic Search vs Text Search
### Semantic Search (Preferred)
**Requirements:**
- Smart Connections plugin installed in Obsidian
- Plugin configured for `~/.claude-memory/` vault
**Advantages:**
- Better relevance ranking
- Finds conceptually similar notes
- Handles synonyms and related concepts
**Example:**
- Query: "concurrency issues"
- Finds: "Race Condition Debugging", "Parallel Execution Gotchas"
### Text Search (Fallback)
**When used:**
- Smart Connections not installed
- Smart Connections unavailable
- Semantic search fails
**Behavior:**
- Exact/fuzzy text matching
- Ranked by match frequency and recency
- Still effective for keyword search
**Example:**
- Query: "race condition"
- Finds: Notes containing exact phrase "race condition"
## Cross-Project Recall Tracking
When a memory from Project A is recalled while working in Project B:
1. **Silent logging:**
- Update `cross_project_recalls` frontmatter array
- No user-visible output (non-intrusive)
2. **Threshold detection:**
- After 3 cross-project recalls
- Trigger promotion prompt (via `managing-working-memory` skill)
3. **Context capture:**
```yaml
cross_project_recalls:
- project: tabula-scripta
date: 2025-11-18
context: "Recalled via search: \"git worktrees\""
- project: another-project
date: 2025-11-19
context: "Recalled via search: \"isolation patterns\""
```
## Interactive Follow-Up
After presenting results, user can:
1. **Load full note content:**
- User types: `1`
- Claude loads: Full content of result #1
2. **Continue conversation:**
- User asks: "What did we decide about worktree cleanup?"
- Claude references loaded memories in response
3. **Create new memory:**
- User types: `/remember entity Worktree Cleanup`
- New entity created based on discussion
## Relevance Filtering
Results are filtered for relevance:
- **Minimum score threshold:** Only include results with score > 0.3
- **Recency boost:** Recently updated notes ranked higher
- **Type priority:** Entities ranked above sessions (more persistent knowledge)
- **Project scoping:** Current project results ranked above cross-project results
## Acceptance Criteria
- [ ] Finds notes via semantic search (Smart Connections) when available
- [ ] Falls back to text search when semantic search unavailable
- [ ] Presents top 5 results with relevance ranking
- [ ] Handles MCP unavailable gracefully
- [ ] Detects project context correctly from git repo
- [ ] Tracks cross-project recalls silently
- [ ] Searches both project notes and global entities
- [ ] Shows clear message when no results found
- [ ] Includes note type, project, updated date, and preview in results
- [ ] Offers interactive follow-up options
## Integration with managing-working-memory Skill
This command provides the manual interface for memory search. The `managing-working-memory` skill uses the same search logic for:
- Proactive recall at session start
- Finding related entities during updates
- Cross-project pattern detection
**Relationship:**
- `/recall` - Manual, user-initiated search
- `managing-working-memory` - Automatic, skill-driven search
- Both use same search methods (semantic → text fallback)
- Both track cross-project recalls

265
commands/remember.md Normal file
View File

@@ -0,0 +1,265 @@
# Remember - Create New Memory Note
Create a new memory note in the Obsidian vault at `~/.claude-memory/`.
## Usage
```
/remember [type] [title]
```
**Arguments:**
- `type`: The type of memory note (required)
- `session` - Temporal note for current work session
- `entity` - Persistent note for a concept/component/pattern
- `topic` - Organizational note (Map of Content)
- `title` - The title for the memory note (required)
**Examples:**
```
/remember entity Git Worktrees
/remember session 2025-11-18 - Working Memory Implementation
/remember topic React Patterns
```
## Implementation
When this command is invoked:
### 1. Parse and Validate Arguments
1. Parse the command input by splitting on spaces
2. Extract the first argument as the type
3. Extract remaining arguments and join with spaces as the title
4. Validate that type is one of the allowed values: session, entity, or topic
- If type is invalid, return error message: "Invalid type '{type}'. Must be one of: session, entity, topic"
5. Validate that title is provided and not empty
- If title is missing or empty, return error message: "Title is required. Usage: /remember [type] [title]"
### 2. Detect Project Context
1. Attempt to detect the current project from the git repository:
- Run `git rev-parse --show-toplevel` to find the git repository root
- Extract the project name from the repository directory name
2. If not in a git repository (command fails):
- Fall back to using the current working directory name as the project
- If that also fails, use 'default' as the project name
3. Store the detected project name for use in vault path generation
### 3. Generate Vault Path
1. Sanitize the title to create a valid filename:
- Replace any characters that are invalid in filenames with hyphens
- Invalid characters include: / \ : * ? " < > |
2. Generate the vault path based on the note type:
- For session notes: `claude/projects/{project}/sessions/{sanitized-title}.md`
- For entity notes: `claude/projects/{project}/entities/{sanitized-title}.md`
- For topic notes: `claude/global/topics/{sanitized-title}.md`
Note that topic notes are always global (not project-specific)
### 4. Generate Frontmatter
1. Get the current date in YYYY-MM-DD format
2. Create frontmatter with the following fields:
- type: The note type (session, entity, or topic)
- project: The project name, or 'global' for topic notes
- tags: Type-specific tags
- Session notes: [session, work-in-progress]
- Entity notes: [entity]
- Topic notes: [topic, moc]
- created: Current date (YYYY-MM-DD)
- updated: Current date (YYYY-MM-DD)
- status: 'active'
- claude_last_accessed: Current date (YYYY-MM-DD)
- cross_project_recalls: Empty array (for tracking cross-project usage)
### 5. Generate Note Content from Template
Use the note templates defined in the managing-working-memory skill to generate the initial content. The templates vary by note type:
**For session notes:**
- Include frontmatter with all required fields
- Add a main heading with the session title
- Include sections for:
- Context (why we're working on this)
- Work Log (timestamped entries of what happened)
- Decisions Made (with rationale)
- Open Questions (blockers or uncertainties)
- Next Steps (action items as checkboxes)
- Related Entities (wikilinks to relevant entities)
**For entity notes:**
- Include frontmatter with all required fields
- Add a main heading with the entity name
- Include sections for:
- Overview (purpose and role)
- Architecture (structure and how it works)
- Key Decisions (with date, rationale, alternatives, impact)
- Gotchas & Troubleshooting (symptom, root cause, solution, tags)
- Recent Changes (timestamped log)
- Related Entities (wikilinks with relationship descriptions)
- References (external docs, code paths, commits)
**For topic notes:**
- Include frontmatter with project set to 'global'
- Add a main heading with the topic name
- Include sections for:
- Overview (what the topic covers)
- Key Concepts (wikilinks to core entities)
- Patterns & Best Practices (descriptions and related entities)
- Common Pitfalls (antipatterns to avoid)
- Learning Path (ordered sequence of entities)
- References (external resources and documentation)
All templates use placeholder text in curly braces {like this} to indicate where content should be filled in.
### 6. Invoke MCP create_note
1. Invoke the MCP `create_note` operation with:
- vault: `~/.claude-memory`
- path: The generated vault path
- content: The generated note content with frontmatter
2. Handle the response:
**If successful:**
- Return success message including:
- Confirmation with wikilink to the created note
- Type and project information
- Vault path
- Note that the note is ready for editing
**If FileExistsError:**
- Return error message indicating the note already exists
- Suggest using /update-memory to update it, or choosing a different title
**If MCPUnavailableError:**
- Return graceful degradation message explaining:
- MCP server is unavailable
- Steps to restore: ensure Obsidian is running, check plugin installation, verify config
- Reference to docs/setup-guide.md
- Offer to create a draft in a local markdown file instead
**If unknown error:**
- Return error message with the error details
## Error Handling
### Invalid Type
**Input:** `/remember invalid-type My Note`
**Output:**
```
Error: Invalid type "invalid-type". Must be one of: session, entity, topic
Usage: /remember [type] [title]
Examples:
/remember entity Git Worktrees
/remember session 2025-11-18 - Working Memory Implementation
/remember topic React Patterns
```
### Missing Title
**Input:** `/remember entity`
**Output:**
```
Error: Title is required.
Usage: /remember [type] [title]
Examples:
/remember entity Git Worktrees
/remember session 2025-11-18 - Working Memory Implementation
/remember topic React Patterns
```
### Note Already Exists
**Input:** `/remember entity Git Worktrees` (when it already exists)
**Output:**
```
Error: Memory note [[Git Worktrees]] already exists at claude/projects/tabula-scripta/entities/Git Worktrees.md
Use /update-memory to update an existing note, or choose a different title.
```
### MCP Unavailable
**Input:** `/remember entity My Component` (when MCP server is down)
**Output:**
```
Obsidian MCP server is unavailable. Cannot create memory note.
To restore memory features:
1. Ensure Obsidian is running
2. Check obsidian-mcp-plugin is installed in ~/.claude-memory/
3. Verify Claude Code config includes MCP server
See docs/setup-guide.md for troubleshooting.
Would you like me to create a draft in a local markdown file instead?
```
## Success Output
**Input:** `/remember entity Git Worktrees`
**Output:**
```
Created memory note: [[Git Worktrees]]
Type: entity
Project: tabula-scripta
Path: claude/projects/tabula-scripta/entities/Git Worktrees.md
The note is ready for editing in Obsidian or via /update-memory.
```
## Integration with managing-working-memory Skill
This command provides the manual interface for memory creation. The `managing-working-memory` skill uses the same underlying logic but triggers automatically based on workflow events (code review, debugging, etc.).
**Relationship:**
- `/remember` - Manual, user-initiated memory creation
- `managing-working-memory` - Automatic, skill-driven memory creation
- Both use identical frontmatter schema and note templates
- Both invoke same MCP operations
## Wikilink Generation
All created notes support Obsidian wikilinks:
- Entity reference: `[[Git Worktrees]]`
- Session reference: `[[2025-11-18 - Working Memory Implementation]]`
- Topic reference: `[[React Patterns]]`
Links work bidirectionally in Obsidian's graph view.
## Project Context Detection
The command detects project context in priority order:
1. **Git repository name** - `git rev-parse --show-toplevel`
2. **Working directory name** - `path.basename(process.cwd())`
3. **Fallback** - `'default'`
This ensures session and entity notes are scoped to the correct project.
## Acceptance Criteria
- [ ] Creates notes at correct vault path with valid frontmatter
- [ ] Validates type argument (session/entity/topic)
- [ ] Requires title argument
- [ ] Detects project context from git repo
- [ ] Generates wikilinks correctly
- [ ] Handles MCP unavailable gracefully
- [ ] Shows clear error for note already exists
- [ ] Uses templates from managing-working-memory skill
- [ ] Frontmatter includes all required fields
- [ ] Timestamp format is YYYY-MM-DD

459
commands/update-memory.md Normal file
View File

@@ -0,0 +1,459 @@
# Update Memory - Update Existing Note
Update an existing memory note with new information, using patch operations and conflict detection.
## Usage
```
/update-memory [title]
```
**Arguments:**
- `title` - The title of the existing memory note to update (required)
**Examples:**
```
/update-memory Git Worktrees
/update-memory 2025-11-18 - Working Memory Implementation
/update-memory React Patterns
```
## Implementation
When this command is invoked:
### 1. Parse and Validate Title
1. Parse the title by trimming whitespace from the command input
2. Validate that the title is not empty
- If title is empty, return error message: "Title is required. Usage: /update-memory [title]"
### 2. Detect Project Context
1. Attempt to detect the current project from the git repository:
- Run `git rev-parse --show-toplevel` to find the git repository root
- Extract the project name from the repository directory name
2. If not in a git repository:
- Fall back to using the current working directory name
- If that fails, use 'default' as the project name
3. Store the detected project name for locating the note
### 3. Search for Note
1. Try to find the note by checking multiple possible locations in order:
- Current project entities: `claude/projects/{currentProject}/entities/{title}.md`
- Current project sessions: `claude/projects/{currentProject}/sessions/{title}.md`
- Global entities: `claude/global/entities/{title}.md`
- Global topics: `claude/global/topics/{title}.md`
2. For each possible path:
- Attempt to read the note using MCP `read_note`
- If successful, store the path and note content, then stop searching
- If FileNotFoundError, continue to next path
- If other error, propagate it
3. If not found in standard locations, use search as fallback:
- Invoke MCP `search_notes` with query=title and path_filter='claude/**'
- Find exact title match in results (case-insensitive comparison)
- If exact match found, load the note using MCP `read_note`
4. If still not found after all attempts:
- Return error message listing all searched locations
- Suggest using /remember to create the note
- Provide example command
### 4. Load Note and Check Timestamps
1. Store the loaded timestamp from frontmatter for conflict detection later
- Save note.frontmatter.updated as the loaded timestamp
2. Update claude_last_accessed in frontmatter to current date (YYYY-MM-DD)
3. Display the current note content to the user:
- Show wikilink title, type, project, last updated date, and path
- Display full current content
- Add separator line
- Prompt: "What would you like to update? I'll apply patch operations to preserve existing content."
### 5. Collect User Updates
This is a conversational step where the user describes what they want to update.
1. User provides their update request in natural language
- Example: "Add a new decision about using trap handlers for cleanup"
2. Parse the user's intent to identify the appropriate patch operation type:
- Append to section: Add new entry to an existing section (e.g., "## Recent Changes")
- Add new section: Create a completely new section (e.g., "## Troubleshooting")
- Update frontmatter: Modify frontmatter fields (e.g., add a tag)
- Insert in list: Add item to an existing list (e.g., "## Related Entities")
3. Confirm with the user what will be done
- Example: "I'll add this to the Key Decisions section."
### 6. Check for Conflicts (Before Writing)
Use the conflict detection logic defined in the managing-working-memory skill:
1. Before applying the patch, reload the note using MCP `read_note` to get the current state
2. Compare timestamps to detect conflicts:
- Get the current timestamp from the reloaded note's frontmatter.updated
- Compare with the loaded timestamp saved in step 4
- If current timestamp > loaded timestamp: CONFLICT DETECTED (human edited since Claude loaded)
- If timestamps match: No conflict, safe to update
3. If conflict detected:
- Trigger conflict resolution flow (see managing-working-memory skill for detailed flow)
- Present both changes to user and offer resolution options
4. If no conflict:
- Proceed to apply patch operation in next step
### 7. Apply Patch Operation
Use the patch operation patterns defined in the managing-working-memory skill. Apply the identified patch operation type:
**For append_to_section:**
- Locate the specified section in the note content
- Append the new content to the end of that section
- Preserve all existing content in the section
**For add_new_section:**
- Create a new markdown section with the specified title
- Add the section content
- Insert at appropriate location in the note structure
**For update_frontmatter:**
- Modify the specified frontmatter field(s)
- Common operations: adding tags, updating status, etc.
**For insert_in_list:**
- Locate the specified list within a section
- Add the new list item
- Maintain proper markdown list formatting
After applying content changes:
1. Update frontmatter timestamps:
- Set updated to current date (YYYY-MM-DD)
- Set claude_last_accessed to current date (YYYY-MM-DD)
2. If adding tags, merge new tags with existing tags (remove duplicates)
### 8. Write Updated Note
1. Invoke MCP `update_note` with:
- vault: `~/.claude-memory`
- path: The note path
- content: The updated content
- frontmatter: The updated frontmatter
2. Handle the response:
**If successful:**
- Return success message including:
- Confirmation with wikilink
- Description of applied changes
- Updated timestamp
- Path
- Note that Obsidian has been updated
**If MCPUnavailableError:**
- Return graceful degradation message explaining:
- MCP server is unavailable
- Steps to restore: ensure Obsidian is running, check plugin installation, verify config
- Reference to docs/setup-guide.md
- Offer to save pending update to a local file
**If other error:**
- Return error message with details
## Conflict Detection and Resolution
All conflict detection and resolution flows are defined in the managing-working-memory skill. The /update-memory command uses these patterns:
### Case 1: Clean Update (No Conflict)
Timeline:
- T1: Claude loads note (updated = "2025-11-17")
- T2: User discusses updates with Claude
- T3: Claude applies patch (updated still "2025-11-17")
- Result: No conflict, proceed with update
See managing-working-memory skill for timestamp comparison logic.
**Example Output:**
```
Updated: [[Git Worktrees]]
Applied changes:
- Added new entry to "Recent Changes"
- Appended decision about trap handlers to "Key Decisions"
- Updated tags: [entity, git, worktrees, error-handling]
Updated: 2025-11-18
Path: claude/projects/tabula-scripta/entities/Git Worktrees.md
The note has been updated in Obsidian.
```
### Case 2: Human Edit Conflict
Timeline:
- T1: Claude loads note (updated = "2025-11-17")
- T2: Human edits note in Obsidian (updated = "2025-11-18")
- T3: Claude attempts patch (detects conflict)
- Result: Conflict detected, show diff and offer resolution options
See managing-working-memory skill for conflict resolution flow and user options.
**Example Output:**
```
I want to update [[Git Worktrees]] but you've edited it since I loaded it.
Your changes (at 2025-11-18):
+ Added new section "## Performance Considerations"
+ Updated "Overview" with additional context
~ Modified frontmatter tags: added #optimization
My pending changes:
- Add new entry to "Recent Changes"
- Append decision about trap handlers to "Key Decisions"
Options:
1. Show me both diffs and I'll merge manually
2. Abort your update (keep my changes only)
3. Create new section "## Claude's Updates (conflicted)" with your changes
4. Let's discuss and resolve together
What should I do?
```
### Case 3: Major Rewrite Needed
When Claude's understanding contradicts existing note fundamentally, use the major rewrite pattern from managing-working-memory skill to present options without overwriting user knowledge.
**Example Output:**
```
My understanding of [[Git Worktrees]] changed significantly.
Current note says:
"Git worktrees are primarily for parallel feature development and should be short-lived."
But I now think:
"Git worktrees can be long-lived for maintaining multiple release branches simultaneously."
Options:
1. Create new entity note with alternative understanding
2. Update existing note (I'll show you the full diff first)
3. Let's discuss the contradiction - maybe both are valid
What should I do?
```
## Patch Operation Types
All patch operations are defined in the managing-working-memory skill. The /update-memory command uses these standard patterns:
### 1. Append to Section
Add new content at the end of an existing section. Example: Adding to "## Recent Changes" section.
See managing-working-memory skill for appendToSection pattern.
### 2. Add New Section
Create a new section in the note. Example: Adding "## Performance Considerations" section.
See managing-working-memory skill for addSection pattern.
### 3. Update Frontmatter
Modify frontmatter fields such as tags or status.
Examples:
- Adding a tag: Append to tags array
- Changing status: Set status field to new value
See managing-working-memory skill for frontmatter update pattern.
### 4. Insert in List
Add item to an existing list like "## Related Entities" or "## References".
See managing-working-memory skill for insertInList pattern.
## Error Handling
### Note Not Found
**Input:** `/update-memory Nonexistent Entity`
**Output:**
```
Error: Memory note [[Nonexistent Entity]] not found.
Searched:
- Project entities: claude/projects/tabula-scripta/entities/
- Project sessions: claude/projects/tabula-scripta/sessions/
- Global entities: claude/global/entities/
- Global topics: claude/global/topics/
Would you like to create it with /remember?
Example: /remember entity Nonexistent Entity
```
### MCP Unavailable
**Input:** `/update-memory Git Worktrees` (when MCP server is down)
**Output:**
```
Obsidian MCP server is unavailable. Cannot update memory note.
To restore memory features:
1. Ensure Obsidian is running
2. Check obsidian-mcp-plugin is installed in ~/.claude-memory/
3. Verify Claude Code config includes MCP server
See docs/setup-guide.md for troubleshooting.
Would you like me to save the pending update to a local file?
```
### Missing Title
**Input:** `/update-memory`
**Output:**
```
Error: Title is required.
Usage: /update-memory [title]
Examples:
/update-memory Git Worktrees
/update-memory 2025-11-18 - Working Memory Implementation
/update-memory React Patterns
```
## Success Output (Clean Update)
**Input:** `/update-memory Git Worktrees`
**Conversation:**
```
User: /update-memory Git Worktrees
Claude: Loaded: [[Git Worktrees]]
Type: entity
Project: tabula-scripta
Last updated: 2025-11-17
Path: claude/projects/tabula-scripta/entities/Git Worktrees.md
Current content:
[... full note content ...]
---
What would you like to update? I'll apply patch operations to preserve existing content.
User: Add a decision about using trap handlers for cleanup
Claude: I'll add this to the Key Decisions section.
[Claude applies patch]
Updated: [[Git Worktrees]]
Applied changes:
- Added new entry to "Key Decisions"
### Trap Handlers for Cleanup
Date: 2025-11-18
Rationale: Ensures worktree cleanup even if subagent crashes
Alternatives Considered: Manual cleanup (error-prone), atexit hooks (unreliable)
Impact: Improves reliability of parallel execution
Updated: 2025-11-18
Path: claude/projects/tabula-scripta/entities/Git Worktrees.md
The note has been updated in Obsidian.
```
## Timestamp Tracking
### Frontmatter Fields
```yaml
---
created: 2025-11-17 # Never changes, original creation
updated: 2025-11-18 # Modified every save (human or Claude)
claude_last_accessed: 2025-11-18 # When Claude loaded into context
---
```
### Conflict Detection Logic
See the managing-working-memory skill for the complete timestamp comparison logic. The basic flow is:
1. **When loading:** Store the updated timestamp and set claude_last_accessed
2. **When updating:** Reload the note and compare timestamps
3. **If current timestamp > loaded timestamp:** Conflict detected
4. **If timestamps match:** Safe to update
## Graceful Degradation (MCP Unavailable)
When MCP server is unavailable:
1. **Detect failure** - Catch `MCPUnavailableError`
2. **Offer alternatives:**
- Save pending update to local markdown file
- Export as diff patch for manual application
- Continue conversation without update
3. **Guide troubleshooting:**
- Link to `docs/setup-guide.md`
- Check Obsidian is running
- Verify plugin installation
## Interactive Update Flow
The `/update-memory` command is conversational:
1. **Load note** - Show current content to user
2. **Collect intent** - User describes what to update
3. **Confirm operation** - Claude describes patch operation
4. **Check conflict** - Reload note to detect edits
5. **Apply or resolve** - Update cleanly or trigger conflict resolution
6. **Confirm success** - Show what changed
This ensures transparency and prevents data loss.
## Acceptance Criteria
- [ ] Loads existing note via MCP read_note
- [ ] Checks timestamps for conflict detection (updated vs loaded)
- [ ] Detects conflicts when human edited since load
- [ ] Shows diff when conflict detected
- [ ] Applies patch operations (append, add section, update frontmatter)
- [ ] Preserves existing content (no full rewrites)
- [ ] Updates frontmatter timestamps (updated, claude_last_accessed)
- [ ] Handles MCP unavailable gracefully
- [ ] Offers to create note if not found
- [ ] Searches multiple locations (project entities, sessions, global)
- [ ] Shows clear success message with changes applied
## Integration with managing-working-memory Skill
This command provides the manual interface for memory updates. The `managing-working-memory` skill uses the same update logic for:
- Automatic updates after code review
- Updates after debugging sessions
- Periodic checkpoint updates
- Session end compaction
**Relationship:**
- `/update-memory` - Manual, user-initiated updates
- `managing-working-memory` - Automatic, skill-driven updates
- Both use identical conflict detection and patch operations
- Both track timestamps for data integrity

26
hooks/hooks.json Normal file
View File

@@ -0,0 +1,26 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
}
]
}
],
"SessionEnd": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh"
}
]
}
]
}
}

620
hooks/session-end.md Normal file
View File

@@ -0,0 +1,620 @@
# Session End Hook
This hook runs automatically when a Claude Code session ends, handling session note updates, threshold-based compaction, and archival.
## Overview
**Purpose:** Finalize session notes, compact if threshold exceeded, and archive completed sessions
**Compaction Threshold:** 500 lines OR 3 days old (whichever comes first)
**Archival Strategy:** Never delete, always archive to preserve history
## Implementation
### 1. Load Current Session Note
1. Detect the project context (same logic as session-start hook)
2. Determine the session note path:
- Get current date in YYYY-MM-DD format
- Infer session topic from work log or user input
- Construct path: `claude/projects/{projectContext}/sessions/{date}-{topic}.md`
3. Attempt to load the session note using MCP vault tool:
```javascript
mcp__obsidian-vault__vault({
action: "read",
path: `claude/projects/${projectContext}/sessions/${date}-${topic}.md`,
returnFullFile: true
})
```
4. Handle different cases:
**If session note exists:**
- Proceed to finalize the session (next steps)
**If FileNotFoundError:**
- No session note was created this session
- Display: "No session note created this session."
- Exit normally (no-op)
**If MCPUnavailableError:**
- Display: "MCP unavailable. Cannot finalize session."
- See error recovery section below for options
### 2. Final Session Note Update
1. Append session closing entry to the "## Work Log" section:
- Add timestamp with current time
- Add heading: "Session End"
- Add message: "Session completed. Work finalized."
2. Update frontmatter:
- Set updated to current date (YYYY-MM-DD)
- Set claude_last_accessed to current date
3. Write the final update using MCP vault tool:
```javascript
mcp__obsidian-vault__vault({
action: "update",
path: sessionPath,
content: updatedContentWithFrontmatter
})
```
Or use edit tool for efficient append:
```javascript
// Append closing entry
mcp__obsidian-vault__edit({
action: "append",
path: sessionPath,
content: `\n### ${timestamp} - Session End\n\nSession completed. Work finalized.`
});
// Update frontmatter
mcp__obsidian-vault__edit({
action: "patch",
path: sessionPath,
targetType: "frontmatter",
target: "updated",
operation: "replace",
content: new Date().toISOString().split('T')[0]
});
```
4. Proceed to check compaction threshold (next step)
### 3. Check Compaction Threshold
**Threshold Logic:**
1. Count lines in the session note by splitting content on newlines
2. Calculate age in days:
- Parse created date from frontmatter
- Compare with current date
- Calculate difference in days
3. Check if either threshold is exceeded:
- Line threshold: >= 500 lines
- Age threshold: >= 3 days
4. If either threshold is exceeded:
- Display reason: "Session note {exceeds 500 lines / exceeds 3 days old}. Triggering compaction..."
- Trigger compaction (next step)
5. If both thresholds are not exceeded:
- Display: "Session note below threshold ({lineCount} lines, {ageInDays} days old). Keeping active."
- Mark session status as "active" in frontmatter
- Update the note using MCP edit tool:
```javascript
mcp__obsidian-vault__edit({
action: "patch",
path: sessionPath,
targetType: "frontmatter",
target: "status",
operation: "replace",
content: "active"
})
```
**Threshold Rules:**
- **500 lines:** Session note becomes too large to navigate efficiently
- **3 days old:** Knowledge should be consolidated into entities for long-term retention
- **Whichever comes first:** More aggressive compaction ensures timely knowledge extraction
### 4. Compact Session Note (I1: Simplified Algorithm)
**Compaction Process:**
1. Display: "Compacting session note into entity notes..."
2. Parse the session note to identify extractable knowledge:
- Use high-level heuristics rather than detailed algorithms
- Look for distinct concepts and decisions
3. For each identified piece of knowledge:
- Update or create the appropriate entity note
- See step 5 for entity update logic
4. Archive the session note (step 6)
5. Display: "Compaction complete. Session archived."
**Knowledge Extraction (Simplified):**
Parse the session note to identify key extractable knowledge:
1. **Extract decisions** from "## Decisions Made" section
- Each decision entry becomes an update to an entity note
- Infer which entity based on context and related entities
2. **Extract gotchas** from "## Work Log" section
- Look for debugging work: "fixed bug", "troubleshot", "resolved issue"
- Each gotcha updates the troubleshooting section of an entity
3. **Extract references** from wikilinks throughout the note
- All wikilinked entities get updated with reference to this session
**Extraction Categories:**
- **Architectural decisions** → Update entity notes with decision and rationale
- **Bug fixes / gotchas** → Update entity troubleshooting section
- **New patterns** → Create topic notes or new entities
- **User preferences** → Update user preference entity
**Note:** Keep extraction logic flexible. Claude should use judgment to identify valuable knowledge rather than following rigid algorithms.
### 5. Update Entity Notes
Use the patch operations and conflict detection patterns from the managing-working-memory skill.
For each extracted piece of knowledge:
1. Determine the target entity note path:
- `claude/projects/{projectContext}/entities/{entityName}.md`
2. Attempt to load the existing entity using MCP vault tool:
```javascript
const loadResult = await mcp__obsidian-vault__vault({
action: "read",
path: `claude/projects/${projectContext}/entities/${entityName}.md`,
returnFullFile: true
});
```
3. Store the loaded content for conflict detection
4. Apply patch operation based on knowledge type (using efficient edit tool):
- **Decision:** Append to "## Key Decisions" section
```javascript
mcp__obsidian-vault__edit({
action: "patch",
path: entityPath,
targetType: "heading",
target: "Key Decisions",
operation: "append",
content: `\n- ${date}: ${decisionText}`
})
```
- **Gotcha:** Append to "## Gotchas & Troubleshooting" section
- **Reference:** Append to "## Recent Changes" section
5. Before writing, reload the entity and check for conflicts:
- Compare loaded content with current content
- If conflict detected, trigger conflict resolution
- If no conflict, proceed with update
6. Update frontmatter timestamps:
```javascript
mcp__obsidian-vault__edit({
action: "patch",
path: entityPath,
targetType: "frontmatter",
target: "updated",
operation: "replace",
content: new Date().toISOString().split('T')[0]
})
```
7. Display: "Updated [[{entityName}]] with knowledge from session."
8. Handle errors:
- If FileNotFoundError: Create new entity from session knowledge
- If other error: Propagate
**Conflict Handling:** Uses same timestamp-based detection as defined in `managing-working-memory.md`
### 6. Archive Session Note
**Archival Process:**
1. Determine archive path:
- Format: `claude/projects/{projectContext}/archive/sessions/{filename}`
- Use same filename as original session note
2. Update session note metadata:
- Set status to "archived" in frontmatter
- Set updated to current date
- Add archived_date field with current date
- Add archived_reason: "Compaction threshold exceeded"
3. Ensure archive directory exists (create if needed)
4. Copy session note to archive location using MCP vault tool:
```javascript
mcp__obsidian-vault__vault({
action: "create",
path: `claude/projects/${projectContext}/archive/sessions/${filename}`,
content: sessionContentWithArchivedFrontmatter
})
```
5. Verify archive was successful by attempting to read the archived note
6. If verification successful:
- Display: "Session archived to: {archivePath}"
- Leave original note with archived status (default behavior)
- User can manually delete original if desired
7. If verification fails:
- Throw error: "Archive verification failed. Session not moved."
- Keep original note intact
**Archive Location:** `~/.claude-memory/claude/projects/{project}/archive/sessions/{date}-{topic}.md`
**Preservation Strategy:**
- Never delete notes (always archive)
- Archived notes retain full content and frontmatter
- Status marked as "archived" in frontmatter
- Original note can optionally be removed (default: keep with archived status)
### 7. Create Backlinks
**Link archived session to updated entities:**
For each entity that was updated during compaction:
1. Determine the entity path:
- `claude/projects/{projectContext}/entities/{entityName}.md`
2. Load the entity using MCP vault tool:
```javascript
mcp__obsidian-vault__vault({
action: "read",
path: `claude/projects/${projectContext}/entities/${entityName}.md`,
returnFullFile: true
})
```
3. Create a backlink to the archived session:
- Format: `- [[{session-filename}]] - Archived session (compacted)`
- Extract filename from session path (without .md extension)
4. Append the backlink to the "## References" section of the entity
5. Update the entity frontmatter:
- Set updated to current date
6. Write the updated entity using MCP edit tool:
```javascript
// Append backlink to References section
mcp__obsidian-vault__edit({
action: "patch",
path: entityPath,
targetType: "heading",
target: "References",
operation: "append",
content: `\n- [[${sessionFilename}]] - Archived session (compacted)`
});
// Update frontmatter
mcp__obsidian-vault__edit({
action: "patch",
path: entityPath,
targetType: "frontmatter",
target: "updated",
operation: "replace",
content: new Date().toISOString().split('T')[0]
});
```
This creates bidirectional links between archived sessions and the entities they contributed to.
## Error Handling and Recovery (I2)
### MCP Unavailable
When MCP server is unavailable at session end:
1. Detect the MCPUnavailableError
2. Display message listing pending actions:
```
Obsidian MCP unavailable. Cannot finalize session.
Pending actions:
- Session note update
- Compaction check
- Archival (if threshold exceeded)
```
3. **Decision Point - Ask User:**
- **Option A: Exit without finalization**
- Session ends without memory updates
- User must manually run session cleanup later
- Provide manual steps:
1. Check session note line count and age
2. If threshold exceeded, compact into entity notes
3. Archive to archive/sessions/ directory
- **Option B: Wait and retry**
- Pause session end
- Wait for user to fix MCP connection
- Retry finalization once fixed
- **Option C: Export pending work**
- Create local file with session summary
- User can manually update memory later
- Provide file path and instructions
4. Display troubleshooting reference: "See docs/setup-guide.md for MCP troubleshooting."
### Compaction Failure
When compaction fails for any reason:
1. Catch the error and log it
2. **Decision Point - Do NOT archive if compaction failed**
- Leave session note as-is
- Session remains active until compaction succeeds
3. Display error message with manual action steps:
```
Session compaction failed. Session note preserved.
Manual action required:
1. Review session note: {sessionPath}
2. Extract knowledge into entity notes manually
3. Archive session when complete
Error: {error.message}
```
4. Mark session with error status:
- Set frontmatter.status = "compaction_failed"
- Add frontmatter.error = error message
- Update note using MCP edit tool:
```javascript
mcp__obsidian-vault__edit({
action: "patch",
path: sessionPath,
targetType: "frontmatter",
target: "status",
operation: "replace",
content: "compaction_failed"
});
mcp__obsidian-vault__edit({
action: "patch",
path: sessionPath,
targetType: "frontmatter",
target: "error",
operation: "replace",
content: error.message
});
```
5. **Recovery options:**
- User can manually compact and archive later
- On next session, user can trigger manual compaction
- Session will remain in active state with error marker
### Archive Verification Failed
When archive verification fails:
1. Detect that archived note cannot be read
2. Display error: "Archive verification failed. Session not moved."
3. **Decision Point - Abort archival, keep session active:**
- Do NOT remove original session note
- Revert session status to "active"
- Add frontmatter.archive_error = "Verification failed"
4. Update the original session note using MCP edit tool:
```javascript
mcp__obsidian-vault__edit({
action: "patch",
path: sessionPath,
targetType: "frontmatter",
target: "status",
operation: "replace",
content: "active"
});
mcp__obsidian-vault__edit({
action: "patch",
path: sessionPath,
targetType: "frontmatter",
target: "archive_error",
operation: "replace",
content: "Verification failed"
});
```
5. Throw error to halt session end process:
- "Archive failed - session preserved at original location"
6. **Recovery options:**
- User can investigate why archive failed
- Retry archival on next session end
- Manually move to archive directory if needed
## Integration with Managing Working Memory Skill
**Session End Checklist:** Uses TodoWrite checklist from `skills/managing-working-memory.md`:
```markdown
- [ ] Final session note update with closing context
- [ ] Check session note size (line count)
- [ ] Check session note age (created date)
- [ ] If threshold met (500 lines OR 3 days old):
- [ ] Parse session note for extractable knowledge
- [ ] Identify entities to create or update
- [ ] Apply patches to entity notes
- [ ] Create new entities if needed
- [ ] Archive session note to archive/sessions/
- [ ] Verify archive successful
- [ ] If threshold not met:
- [ ] Mark session note status: active
- [ ] Note next checkpoint time
- [ ] Review cross-project recall tracking
- [ ] Check for promotion threshold (3 recalls)
- [ ] Confirm all writes successful
```
**Compaction Triggers:** Aligns with triggers defined in managing-working-memory skill
## Performance Considerations
### Compaction Performance
To avoid session-end delays, optimize compaction performance:
1. **Track timing:**
- Record start time before compaction begins
- Record end time after archival completes
- Calculate total duration in milliseconds
2. **Optimize operations:**
- Parse session (fast - single pass)
- Batch entity updates in parallel using Promise.all
- Archive in single operation (not per-entity)
3. **Display timing:**
- Show "Compaction completed in {duration}ms"
4. **Performance target:** <5 seconds for compaction (even with 10+ entity updates)
### Async Session End
Don't block session exit if MCP operations are slow:
1. Display: "Finalizing session..."
2. Run session-end hook asynchronously:
- Execute all finalization steps
- Allow user to exit if it takes too long
3. Handle completion:
**If successful:**
- Display: "Session finalized successfully."
**If error:**
- Display: "Session end failed: {error.message}"
- Display: "Session state preserved. Manual cleanup may be needed."
- Allow user to exit anyway (don't block)
## Testing
### Manual Testing
1. Create session note with >500 lines
2. Run session-end hook
3. Verify compaction triggered
4. Check entity notes updated
5. Verify session archived at correct path
6. Confirm original session marked as "archived"
### Edge Cases
- **Session note <500 lines and <3 days old:** No compaction, marked active
- **MCP unavailable:** Graceful error, pending actions logged
- **Compaction failure:** Session preserved, error status set
- **No session note:** No-op, clean exit
- **Archive directory missing:** Create directory automatically
### Threshold Testing
**Test Case 1: Line Threshold**
- Session with 600 lines, 1 day old
- Expected: Compact (exceeds 500 lines)
**Test Case 2: Age Threshold**
- Session with 300 lines, 4 days old
- Expected: Compact (exceeds 3 days)
**Test Case 3: Both Thresholds**
- Session with 600 lines, 4 days old
- Expected: Compact (exceeds both thresholds)
**Test Case 4: Neither Threshold**
- Session with 200 lines, 1 day old
- Expected: Mark active, no compaction
## Example Session End Flow
```
$ exit
[Session End Hook Executing...]
Finalizing session for project: tabula-scripta
Session note: 2025-11-18-session-hooks.md
Status: 487 lines, 1 day old (below threshold)
Marking session as active (no compaction needed).
Session finalized successfully.
---
Goodbye!
```
**With Compaction:**
```
$ exit
[Session End Hook Executing...]
Finalizing session for project: tabula-scripta
Session note: 2025-11-15-memory-design.md
Status: 623 lines, 1 day old (exceeds 500 line threshold)
Triggering compaction...
Extracting knowledge:
- 3 architectural decisions → [[Memory System Architecture]]
- 2 debugging insights → [[Conflict Detection]]
- 5 entity references → Updated backlinks
Compaction complete. Session archived to:
archive/sessions/2025-11-15-memory-design.md
Session finalized successfully.
---
Goodbye!
```
## Summary
This session-end hook finalizes working memory at the end of each session:
1. **Loads current session note** and applies final update
2. **Checks compaction threshold** (500 lines OR 3 days old)
3. **Compacts if threshold exceeded** by extracting knowledge into entity notes
4. **Archives session note** to preserve history (never deletes)
5. **Updates entity notes** with decisions, gotchas, and patterns
6. **Creates backlinks** from entities to archived session
7. **Handles errors gracefully** (MCP unavailable, compaction failures, etc.)
Users experience automatic knowledge consolidation without manual effort, while maintaining full session history in archived notes for future reference.

27
hooks/session-end.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# SessionEnd hook for tabula-scripta plugin
# Injects working memory finalization instructions into Claude's context
set -euo pipefail
# Determine plugin root directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Read session-end hook documentation
session_end_instructions=$(cat "${PLUGIN_ROOT}/hooks/session-end.md" 2>&1 || echo "Error reading session-end hook documentation")
# Escape for JSON
instructions_escaped=$(echo "$session_end_instructions" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
# Output context injection as JSON
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "SessionEnd",
"additionalContext": "<IMPORTANT>\n**Tabula Scripta: Session Finalization**\n\nThis hook automatically finalizes working memory at session end.\n\n**Instructions:**\n\n${instructions_escaped}\n\n**Action Required:**\n1. Follow the session-end implementation steps above\n2. Check compaction threshold (500 lines OR 3 days old)\n3. If threshold met: extract knowledge, update entities, archive session\n4. Use the correct MCP tool names (mcp__obsidian-vault__vault, mcp__obsidian-vault__edit, etc.)\n5. Handle errors gracefully\n\n**Note:** If MCP is unavailable, notify user and provide manual cleanup steps.\n</IMPORTANT>"
}
}
EOF
exit 0

509
hooks/session-start.md Normal file
View File

@@ -0,0 +1,509 @@
# MANDATORY SESSION START PROTOCOL
**YOU MUST execute these steps BEFORE responding to the user's first message.**
## Why This Matters
Without loading memory proactively, you will:
- Lose context from previous sessions
- Repeat solved problems
- Miss important decisions and open questions
- Waste user's time re-establishing context
## The Iron Law
```
LOAD MEMORY FIRST, RESPOND TO USER SECOND
```
No exceptions. Execute the protocol below, THEN respond to whatever the user asked.
## Required Protocol (Execute in Order)
**Performance Target:** <2 seconds total
**User Experience:** 1-3 sentence summary, then respond to their message
## Implementation
### 1. Detect Project Context
1. Attempt to detect the current project from the git repository:
- Run `git rev-parse --show-toplevel` to find the git repository root
- Extract the project name from the repository directory name
- Return the project name
2. If not in a git repository (command fails):
- Fall back to using the current working directory name
- If that also fails, use 'default' as the project name
3. Store the detected project context for all subsequent operations
**Fallback:** If not in git repo, use current directory name. If that fails, use "default".
### 2. Load Project Index
1. Construct the project index path: `claude/projects/{projectContext}/_index.md`
2. Attempt to load the project index using MCP vault tool:
```javascript
mcp__obsidian-vault__vault({
action: "read",
path: `claude/projects/${projectContext}/_index.md`,
returnFullFile: true
})
```
3. If successful:
- Extract wikilinks from the index content
- These represent the most important entities for this project
4. Handle errors:
**If FileNotFoundError:**
- This is the first time working on this project
- Display message: "Starting new project: {projectContext}"
- Offer to create project index for future sessions
**If MCPUnavailableError:**
- Graceful degradation - continue without memory
- Display message: "Obsidian MCP unavailable. See docs/setup-guide.md"
- Continue session without memory loading (see error recovery in section below)
### 3. Query Last 3 Session Notes
**Using Dataview Query (if available):**
1. Construct a Dataview query to find recent sessions:
- Source: `claude/projects/{projectContext}/sessions`
- Filter: status = "active" OR status = "archived"
- Sort: created DESC (most recent first)
- Limit: 3
2. Invoke MCP bases tool to query sessions:
```javascript
mcp__obsidian-vault__bases({
action: "query",
filters: [
{ property: "status", operator: "in", value: ["active", "archived"] }
],
sort: { property: "created", order: "desc" },
pagination: { page: 1, pageSize: 3 }
})
```
Note: If Dataview base not configured, falls back to search (see fallback below)
3. For each returned session path, load the full content in parallel:
```javascript
mcp__obsidian-vault__vault({
action: "read",
path: sessionPath,
returnFullFile: true
})
```
4. Handle errors:
**If Dataview base not available:**
- Fallback to list and filter approach:
```javascript
// List all session files
const listResult = await mcp__obsidian-vault__vault({
action: "list",
directory: `claude/projects/${projectContext}/sessions`
});
// Load frontmatter for each in parallel
const sessions = await Promise.all(
listResult.result.map(async path => {
const r = await mcp__obsidian-vault__vault({
action: "read",
path: path
});
return { path, frontmatter: r.result.frontmatter };
})
);
// Filter and sort
const recent = sessions
.filter(s => s.frontmatter.status === 'active' || s.frontmatter.status === 'archived')
.sort((a, b) => b.frontmatter.created.localeCompare(a.frontmatter.created))
.slice(0, 3);
```
**Fallback Strategy:** If Dataview base unavailable, use list + filter approach with frontmatter sorting.
### 4. Load Linked Entities
1. From the project index, extract the linked entities (wikilinks)
2. Limit to top 5 most important entities to keep load time under 2 seconds
3. For each entity (in parallel):
- First attempt: Try to load from project entities path:
```javascript
mcp__obsidian-vault__vault({
action: "read",
path: `claude/projects/${projectContext}/entities/${entityName}.md`,
returnFullFile: true
})
```
- If not found (error.message includes "not found"): Try global entities path:
```javascript
mcp__obsidian-vault__vault({
action: "read",
path: `claude/global/entities/${entityName}.md`,
returnFullFile: true
})
```
4. Collect all successfully loaded entities
**Optimization:** Only load top 5 most important entities (limit based on recency or importance)
### 5. Generate Summary
**Summary Generation:**
1. Initialize an empty highlights array
2. Extract key information from the most recent session:
- Parse the "## Decisions Made" section to find recent decisions
- Parse the "## Open Questions" section to find blockers or uncertainties
- If decisions exist, add the most recent one to highlights
- If open questions exist, add the first one with "Open:" prefix
3. Limit to 1-3 highlights total to create a concise summary
4. Join highlights into a single summary string (1-3 sentences)
**Summary Content Focus:**
- Recent decisions and rationale
- Open questions or blockers
- Key patterns or architectural choices
- Next steps from previous session
**Length Limit:** 1-3 sentences maximum (not a memory dump)
### 6. Present Summary to User
**Output Format:**
```
Working Memory Loaded for Project: {project-name}
Summary:
{1-3 sentence summary of recent context}
Last session: {date} - {topic}
Active entities: {count} loaded
Recent sessions: {count} reviewed
Need more context? Ask me to recall specific entities or sessions.
```
**Example:**
```
Working Memory Loaded for Project: tabula-scripta
Summary:
Last session completed plugin foundation and core memory skill. Currently implementing session hooks with proactive recall. Open question: how to handle MCP connection failures gracefully.
Last session: 2025-11-17 - Memory System Design
Active entities: 5 loaded
Recent sessions: 3 reviewed
Need more context? Ask me to recall specific entities or sessions.
```
### 7. Offer Additional Context
**User can request more:**
```
User: "Tell me about the memory system architecture"
Claude: Loads [[Memory System Architecture]] entity and presents details
```
**User can request session details:**
```
User: "What did we work on last session?"
Claude: Loads full content of last session note and summarizes
```
## Red Flags - STOP and Execute Protocol
If you catch yourself thinking ANY of these thoughts, STOP. Execute the memory loading protocol FIRST:
- "User seems urgent, I'll respond immediately"
- "Their question is simple, I don't need memory"
- "I'll load memory if they specifically ask for it"
- "Let me respond quickly, memory later"
- "They just asked 'what do u remember' - that's different"
- "I can see the git history, that's good enough"
- "This is just a greeting, skip the protocol"
**ALL of these mean:** Stop. Execute the MANDATORY SESSION START PROTOCOL. THEN respond.
## Common Rationalizations (Don't Do These)
| Excuse | Reality |
|--------|---------|
| "User seems urgent, respond immediately" | Protocol takes <2 seconds. User benefits from context. |
| "Question is simple, don't need memory" | You don't know what's simple without loading context. |
| "I'll load if they ask for it" | Proactive loading IS the feature. Don't make user ask. |
| "Git history is good enough" | Git shows files, not decisions/questions/context. Load memory. |
| "This is just a greeting" | Every session starts with greeting. Still load memory. |
| "They said 'what do u remember'" | That's still a first message. Execute protocol FIRST. |
## Error Handling and Recovery
### MCP Unavailable (I2: Error Recovery Paths)
When MCP server is unavailable at session start:
1. Detect the MCPUnavailableError during any memory operation
2. Display user-friendly message:
```
Obsidian MCP server unavailable. Working memory features disabled.
To restore memory:
1. Ensure Obsidian is running
2. Verify obsidian-mcp-plugin installed
3. Check Claude Code config includes MCP server
See docs/setup-guide.md for setup instructions.
```
3. **Decision Point - Ask User:**
- **Option A: Continue without memory**
- Session proceeds normally but without context loading
- Memory writes will also be disabled for this session
- User can still work on tasks, just no memory features
- **Option B: Wait and retry**
- Pause session startup
- Wait for user to fix MCP connection
- Retry memory loading once fixed
- **Option C: Exit and fix setup**
- Exit the session
- User fixes MCP setup
- Restart session with working memory
4. If user chooses to continue without memory:
- Set session flag: `memory_disabled = true`
- Skip all memory operations for this session
- Display reminder that memory is disabled
### Project Index Not Found
If the project index doesn't exist (FileNotFoundError):
1. Display message:
```
Starting fresh project: {projectContext}
I'll create a project index to track your work.
Would you like me to create it now? (yes/no)
```
2. If user confirms:
- Create a new project index note
- Initialize with basic structure
3. If user declines:
- Continue session without index
- Index will be created on first memory write
### No Previous Sessions
If no session notes are found for this project:
1. Display welcome message:
```
Welcome to project: {projectContext}
This is your first session. I'll start building working memory as we work.
Ready to begin!
```
2. Return and start the session normally
- Memory features are active but no context to load yet
## Performance Optimization and Measurement
### Parallel Loading
1. Load project index, sessions, and entities in parallel using Promise.all:
- Load project index
- Query recent sessions (limit 3)
- Load linked entities (limit 5)
2. Wait for all operations to complete before generating summary
**Target:** <2 seconds total time for all operations
### I3: Performance Measurement and Warning
1. **Track timing:**
- Record start time before beginning memory loading
- Record end time after all operations complete
- Calculate total duration in milliseconds
2. **Performance check:**
- If total duration < 2000ms: Success, no message needed
- If total duration >= 2000ms but < 5000ms: Display warning
```
Working memory loaded in {duration}ms (target: <2s)
Consider optimizing:
- Reduce number of linked entities in project index
- Archive old session notes
- Check Obsidian vault performance
```
- If total duration >= 5000ms: Display strong warning
```
Working memory loading is slow ({duration}ms)
This may impact session startup time. Recommendations:
1. Archive sessions older than 30 days
2. Limit project index to 5 most important entities
3. Check Obsidian performance (large vault, plugins)
4. Consider reducing session note size threshold
Continue with memory features? (yes/no)
```
3. **User decision on slow performance:**
- If user says no: Disable memory for this session
- If user says yes: Continue with warning noted
### Lazy Loading
Instead of loading full session content upfront, load summaries first:
1. For each recent session, extract only:
- Path
- Title from frontmatter
- Created date from frontmatter
2. Don't load full content until user requests it
3. This speeds up initial load for sessions with large notes
### Caching
To avoid repeated MCP calls during the session:
1. Create a session cache (Map or similar structure)
2. When loading a note:
- Check if already in cache
- If in cache: Return cached version
- If not in cache: Load via MCP vault tool and store in cache
```javascript
mcp__obsidian-vault__vault({
action: "read",
path: notePath,
returnFullFile: true
})
```
3. Cache persists for session duration only (cleared on exit)
## Integration with Managing Working Memory Skill
**Relationship:** This hook triggers the recall flow defined in `skills/managing-working-memory.md`
**Update claude_last_accessed:** When loading notes, update frontmatter:
1. For each loaded note:
- Set frontmatter.claude_last_accessed to current date (YYYY-MM-DD)
- Update the note using MCP edit tool (efficient patch):
```javascript
mcp__obsidian-vault__edit({
action: "patch",
path: notePath,
targetType: "frontmatter",
target: "claude_last_accessed",
operation: "replace",
content: new Date().toISOString().split('T')[0] // YYYY-MM-DD
})
```
**Cross-Project Tracking:** If loading entity from different project, log cross-project recall:
1. Check if entity is from a different project:
- Compare entity.frontmatter.project with currentProject
- Exclude global entities (project = 'global')
2. If cross-project recall detected:
- Append to cross_project_recalls array:
- project: Current project
- date: Current date
- context: "Session start recall"
3. Check promotion threshold:
- If cross_project_recalls length >= 3, trigger promotion prompt
- Use promotion flow defined in managing-working-memory skill
## Testing
### Manual Testing
1. Start Claude Code session in git repo
2. Verify project detection works (correct project name)
3. Check summary generated (1-3 sentences)
4. Verify <2 second load time
5. Request additional context (entities, sessions)
6. Test in non-git directory (fallback to directory name)
### Edge Cases
- **No previous sessions:** Welcome message
- **MCP unavailable:** Graceful degradation message
- **Large vault (>100 notes):** Performance still <2s
- **Project index missing:** Offer to create
- **Dataview not installed:** Fallback to search_notes
## Example Session Start Flow
```
$ claude-code
[Session Start Hook Executing...]
Working Memory Loaded for Project: tabula-scripta
Summary:
Completed Phase 2 (Core Memory Skill) implementing write triggers and conflict detection. Now implementing Phase 3 session hooks. Performance target is <2 seconds for session start recall.
Last session: 2025-11-17 - Implementing Core Skill
Active entities: 3 loaded ([[Memory System]], [[MCP Integration]], [[Conflict Detection]])
Recent sessions: 2 reviewed
Need more context? Ask me to recall specific entities or sessions.
---
How can I help you today?
```
## Summary
This session-start hook provides proactive recall of working memory at the beginning of each session:
1. **Detects project** from git repo or directory name
2. **Loads project index** with linked entities
3. **Queries last 3 sessions** via Dataview
4. **Generates 1-3 sentence summary** (not overwhelming)
5. **Presents context** with option to request more
6. **Performs in <2 seconds** via parallel loading
7. **Handles errors gracefully** (MCP unavailable, no sessions, etc.)
Users experience automatic context restoration without manual searching, while maintaining control to request additional details as needed.

27
hooks/session-start.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# SessionStart hook for tabula-scripta plugin
# Injects working memory recall instructions into Claude's context
set -euo pipefail
# Determine plugin root directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Read session-start hook documentation
session_start_instructions=$(cat "${PLUGIN_ROOT}/hooks/session-start.md" 2>&1 || echo "Error reading session-start hook documentation")
# Escape for JSON
instructions_escaped=$(echo "$session_start_instructions" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
# Output context injection as JSON
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "<IMPORTANT>\n**Tabula Scripta: Working Memory System**\n\nThis hook automatically loads relevant context from previous sessions.\n\n**Instructions:**\n\n${instructions_escaped}\n\n**Action Required:**\n1. Follow the session-start implementation steps above\n2. Use the correct MCP tool names (mcp__obsidian-vault__vault, mcp__obsidian-vault__edit, etc.)\n3. Present a concise 1-3 sentence summary to the user\n4. Complete within 2 seconds performance target\n\n**Note:** If MCP is unavailable, gracefully degrade and continue without memory.\n</IMPORTANT>"
}
}
EOF
exit 0

77
plugin.lock.json Normal file
View File

@@ -0,0 +1,77 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:arittr/tabula-scripta:",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "120f764e0b0d06857fbdcf70793daff123c131a6",
"treeHash": "3773a703a1aedc0dcde848fea63cf232e4890d752b516e2c1fdebafcfbb44a8f",
"generatedAt": "2025-11-28T10:13:56.461575Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "tabula-scripta",
"description": "Working memory system for Claude Code - integrates with Obsidian via MCP for persistent, cross-session memory management",
"version": "0.1.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "2691094f3f222cef48ea212628c6050986fa14f42a49bf8fb2c32d4d063aa93b"
},
{
"path": "hooks/session-end.md",
"sha256": "e284342f4ce70f730afad88a582ba0a2aaf9396fb1331836671e24edbdead5bd"
},
{
"path": "hooks/session-start.md",
"sha256": "6496cb6726a135afdf0bbe181608550cadb4bc97b6ce4fe741726d21e6c10073"
},
{
"path": "hooks/session-start.sh",
"sha256": "0ecff7f9f3aebb33525a3225ac6827a90e95230f6bc35fe3910117010e9a2885"
},
{
"path": "hooks/hooks.json",
"sha256": "dc8a7435c9c8d9fc7fadb72a6ce3a5ad317dfb5c43c7108df5261ef077ff6ab0"
},
{
"path": "hooks/session-end.sh",
"sha256": "672dc9ba555b3aa37f86b1db89af0abd408e5e62d20543ac8e32e7464a6ccf91"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "19045bda50519c9488048c196579ddadd989557c565f7e0976540aaf1c7e8378"
},
{
"path": "commands/update-memory.md",
"sha256": "5ff841bdebc8542a6a653845871d10386842f07d7855888a73227b8dd6790d7d"
},
{
"path": "commands/remember.md",
"sha256": "20180fea6dd034411f87b4abf89366748aec06a8a062fdfe764f6a789fe44a2d"
},
{
"path": "commands/recall.md",
"sha256": "be8574c605d229812acdb8bd2823ce646addd568921699985e18e13694cb9ffe"
},
{
"path": "skills/managing-working-memory.md",
"sha256": "9425d8a15321c68dc5e0697b3caeb442b3f9f12e343103388e0f4bf5664b4405"
}
],
"dirSha256": "3773a703a1aedc0dcde848fea63cf232e4890d752b516e2c1fdebafcfbb44a8f"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

File diff suppressed because it is too large Load Diff