chore: gitignore .claude/plans and .claude/worktrees

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-02-14 16:52:36 -05:00
parent 0ac4c7b8a9
commit f9ff2b22f2
21 changed files with 2 additions and 4635 deletions
@@ -1,299 +0,0 @@
# Plan: Fix 81 Test Failures from Incomplete Logger Mocks
## Problem Summary
**Root Cause**: NOT circular dependency (which is handled gracefully), but **incomplete logger mocks** that pollute across test files when Bun runs tests in alphabetical order.
When `tests/context/` runs before `tests/utils/`, the incomplete mocks replace the real logger module globally, causing subsequent tests to fail with `TypeError: logger.formatTool is not a function`.
## Phase 0: Documentation Discovery (COMPLETED)
### Sources Consulted
- `src/utils/logger.ts` - Full logger interface (lines 136, 289-373)
- `tests/context/context-builder.test.ts` - Mock pattern (lines 22-29)
- `tests/context/observation-compiler.test.ts` - Mock pattern (lines 4-10)
- `tests/server/server.test.ts` - Mock pattern (lines 4-11)
- `tests/server/error-handler.test.ts` - Mock pattern (lines 5-12)
- `tests/worker/agents/response-processor.test.ts` - Mock pattern (lines 32-39)
### Logger Methods (Complete List)
All 11 methods that must be in any logger mock:
1. `formatTool(toolName: string, toolInput?: any): string` (line 136)
2. `debug(component, message, context?, data?): void` (line 289)
3. `info(component, message, context?, data?): void` (line 293)
4. `warn(component, message, context?, data?): void` (line 297)
5. `error(component, message, context?, data?): void` (line 301)
6. `dataIn(component, message, context?, data?): void` (line 308)
7. `dataOut(component, message, context?, data?): void` (line 315)
8. `success(component, message, context?, data?): void` (line 322)
9. `failure(component, message, context?, data?): void` (line 329)
10. `timing(component, message, durationMs, context?): void` (line 336)
11. `happyPathError<T>(message, context?): T` (line 362)
### Files Requiring Updates
1. `tests/context/observation-compiler.test.ts` (lines 4-10)
2. `tests/context/context-builder.test.ts` (lines 22-29)
3. `tests/server/server.test.ts` (lines 4-11)
4. `tests/server/error-handler.test.ts` (lines 5-12)
5. `tests/worker/agents/response-processor.test.ts` (lines 32-39)
---
## Phase 1: Create Shared Logger Mock Utility
### Objective
Create a reusable complete logger mock to avoid duplication and ensure consistency.
### Implementation
**Create new file**: `tests/test-utils/mock-logger.ts`
```typescript
/**
* Complete logger mock for tests.
* Includes ALL logger methods to prevent mock pollution across test files.
*/
import { mock } from 'bun:test';
export function createMockLogger() {
return {
logger: {
// Core logging methods
debug: mock(() => {}),
info: mock(() => {}),
warn: mock(() => {}),
error: mock(() => {}),
// Data flow logging
dataIn: mock(() => {}),
dataOut: mock(() => {}),
// Status logging
success: mock(() => {}),
failure: mock(() => {}),
// Performance logging
timing: mock(() => {}),
// Tool formatting - returns string
formatTool: mock((toolName: string, _toolInput?: any) => toolName),
// Error helper - returns the message
happyPathError: mock((message: string, _context?: any) => message),
},
};
}
```
### Verification Checklist
- [ ] File created at `tests/test-utils/mock-logger.ts`
- [ ] All 11 logger methods included
- [ ] `formatTool` returns string (not void)
- [ ] `happyPathError` returns the message (not void)
- [ ] File compiles without errors: `bunx tsc --noEmit tests/test-utils/mock-logger.ts`
### Anti-Patterns to Avoid
- ❌ Don't forget `formatTool` - it returns a string, not void
- ❌ Don't forget `happyPathError` - it's generic and returns the message
- ❌ Don't use `() => {}` for methods that return values
---
## Phase 2: Update Affected Test Files
### Objective
Replace incomplete logger mocks with the complete shared mock.
### Files to Update (5 total)
#### 2.1 `tests/context/observation-compiler.test.ts`
**Current (lines 4-10)**:
```typescript
mock.module('../../src/utils/logger.js', () => ({
logger: {
debug: mock(() => {}),
failure: mock(() => {}),
error: mock(() => {}),
},
}));
```
**Replace with**:
```typescript
import { createMockLogger } from '../test-utils/mock-logger.js';
mock.module('../../src/utils/logger.js', () => createMockLogger());
```
#### 2.2 `tests/context/context-builder.test.ts`
**Current (lines 22-29)**:
```typescript
mock.module('../../src/utils/logger.js', () => ({
logger: {
debug: mock(() => {}),
failure: mock(() => {}),
error: mock(() => {}),
info: mock(() => {}),
},
}));
```
**Replace with**:
```typescript
import { createMockLogger } from '../test-utils/mock-logger.js';
mock.module('../../src/utils/logger.js', () => createMockLogger());
```
#### 2.3 `tests/server/server.test.ts`
**Current (lines 4-11)**:
```typescript
mock.module('../../src/utils/logger.js', () => ({
logger: {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
},
}));
```
**Replace with**:
```typescript
import { createMockLogger } from '../test-utils/mock-logger.js';
mock.module('../../src/utils/logger.js', () => createMockLogger());
```
#### 2.4 `tests/server/error-handler.test.ts`
**Current (lines 5-12)**:
```typescript
mock.module('../../src/utils/logger.js', () => ({
logger: {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
},
}));
```
**Replace with**:
```typescript
import { createMockLogger } from '../test-utils/mock-logger.js';
mock.module('../../src/utils/logger.js', () => createMockLogger());
```
#### 2.5 `tests/worker/agents/response-processor.test.ts`
**Current (lines 32-39)**:
```typescript
mock.module('../../../src/utils/logger.js', () => ({
logger: {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
},
}));
```
**Replace with**:
```typescript
import { createMockLogger } from '../../test-utils/mock-logger.js';
mock.module('../../../src/utils/logger.js', () => createMockLogger());
```
### Verification Checklist
- [ ] All 5 files updated with import statement
- [ ] All 5 files use `createMockLogger()` instead of inline mock
- [ ] Import paths are correct (relative to each file's location)
- [ ] Each file still has `mock.module` BEFORE the module imports it mocks
### Anti-Patterns to Avoid
- ❌ Don't place import AFTER the mock.module call
- ❌ Don't use wrong relative path (../test-utils vs ../../test-utils)
- ❌ Don't forget the .js extension in imports
---
## Phase 3: Verification
### Objective
Confirm all 81 failures are fixed.
### Test Commands
```bash
# 1. Run individual test groups first
bun test tests/context/
bun test tests/server/
bun test tests/utils/
bun test tests/shared/
bun test tests/worker/
# 2. Run full suite
bun test
# 3. Verify specific test counts
# Expected: 733+ tests pass (was 652 before)
```
### Verification Checklist
- [ ] `bun test tests/context/` - all pass
- [ ] `bun test tests/server/` - all pass
- [ ] `bun test tests/utils/` - all pass (including 56 formatTool tests)
- [ ] `bun test tests/shared/` - all pass (including 27 settings tests)
- [ ] `bun test` - 730+ tests pass, 0 failures
- [ ] No `TypeError: logger.formatTool is not a function` errors
### Anti-Pattern Grep Checks
```bash
# Check no incomplete logger mocks remain
grep -r "logger: {" tests/ --include="*.ts" | grep -v mock-logger
# Verify all test files use createMockLogger
grep -r "createMockLogger" tests/ --include="*.ts"
```
---
## Phase 4: Commit
### Commit Message
```
fix(tests): complete logger mocks to prevent cross-test pollution
The 81 test failures were caused by incomplete logger mocks that
polluted the module cache when tests ran in alphabetical order.
Changes:
- Create shared mock-logger.ts with all 11 logger methods
- Update 5 test files to use complete mock
- Fix TypeError: logger.formatTool is not a function
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
```
---
## Summary
| Phase | Files Changed | Purpose |
|-------|--------------|---------|
| 1 | 1 new file | Create shared mock utility |
| 2 | 5 files | Update to use shared mock |
| 3 | 0 files | Verification only |
| 4 | 0 files | Commit |
**Total**: 6 files changed, fixing all 81 test failures.
-371
View File
@@ -1,371 +0,0 @@
# Comprehensive Claude-Mem Installer with @clack/prompts
## Overview
Build a beautiful, animated CLI installer for claude-mem using `@clack/prompts` (v1.0.1). Distributable via `npx claude-mem-installer` and `curl -fsSL https://install.cmem.ai | bash`. Replaces the need for users to manually clone, build, configure settings, and start the worker.
**Worktree**: `feat/animated-installer` at `.claude/worktrees/animated-installer`
---
## Phase 0: Documentation & API Reference
### Allowed APIs (@clack/prompts v1.0.1, ESM-only)
| API | Signature | Use Case |
|-----|-----------|----------|
| `intro(title?)` | `void` | Opening banner |
| `outro(message?)` | `void` | Completion message |
| `cancel(message?)` | `void` | User cancelled |
| `isCancel(value)` | `boolean` | Check if user pressed Ctrl+C |
| `text(opts)` | `Promise<string \| symbol>` | API key input, port, data dir |
| `password(opts)` | `Promise<string \| symbol>` | API key input (masked) |
| `select(opts)` | `Promise<Value \| symbol>` | Provider, model, auth method |
| `multiselect(opts)` | `Promise<Value[] \| symbol>` | IDE selection, observation types |
| `confirm(opts)` | `Promise<boolean \| symbol>` | Enable Chroma, start worker |
| `spinner()` | `SpinnerResult` | Installing deps, building, starting worker |
| `progress(opts)` | `ProgressResult` | Multi-step installation progress |
| `tasks(tasks[])` | `Promise<void>` | Sequential install steps |
| `group(prompts, opts)` | `Promise<Results>` | Chain prompts with shared results |
| `note(message, title)` | `void` | Display settings summary, next steps |
| `log.info/success/warn/error(msg)` | `void` | Status messages |
| `box(message, title, opts)` | `void` | Welcome box, completion summary |
### Anti-Patterns
- Do NOT use `require()` — package is ESM-only
- Do NOT call prompts without TTY check first — hangs indefinitely in non-TTY
- Do NOT forget `isCancel()` check after every prompt (or use `group()` with `onCancel`)
- Do NOT use `chalk` — use `picocolors` (clack's dep) for consistency
- `text()` has no numeric mode — validate manually for port numbers
- `spinner.stop()` does not accept status codes — use `spinner.error()` for failures
### Distribution Patterns
- **npx**: `package.json` `bin` field → `"./dist/index.js"`, file needs `#!/usr/bin/env node`
- **curl|bash**: Shell bootstrap downloads JS, runs `node script.js` directly (preserves TTY)
- **esbuild**: Bundle to single ESM file, `platform: 'node'`, `banner` for shebang
### Key Source Files to Reference
- Settings defaults: `src/shared/SettingsDefaultsManager.ts` (lines 73-125)
- Settings validation: `src/services/server/SettingsRoutes.ts`
- Worker startup: `src/services/worker-service.ts` (lines 337-359)
- Health check: `src/services/infrastructure/HealthMonitor.ts`
- Plugin registration: `plugin/.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`
- Marketplace sync: `scripts/sync-marketplace.cjs`
- Cursor integration: `src/services/integrations/CursorHooksInstaller.ts`
- Existing OpenClaw installer: `install/public/openclaw.sh` (reference for logic, not code to copy)
---
## Phase 1: Project Scaffolding
**Goal**: Set up the installer package structure with build tooling.
### Tasks
1. **Create directory structure** in the worktree:
```
installer/
├── src/
│ ├── index.ts # Entry point with TTY guard
│ ├── steps/
│ │ ├── welcome.ts # intro + version check
│ │ ├── dependencies.ts # bun, uv, git checks
│ │ ├── ide-selection.ts # IDE picker + registration
│ │ ├── provider.ts # AI provider + API key
│ │ ├── settings.ts # Additional settings config
│ │ ├── install.ts # Clone, build, register plugin
│ │ ├── worker.ts # Start worker + health check
│ │ └── complete.ts # Summary + next steps
│ └── utils/
│ ├── system.ts # OS detection, command runner
│ ├── dependencies.ts # bun/uv/git install helpers
│ └── settings-writer.ts # Write ~/.claude-mem/settings.json
├── build.mjs # esbuild config
├── package.json # bin, type: module, deps
└── tsconfig.json
```
2. **Create `package.json`**:
```json
{
"name": "claude-mem-installer",
"version": "1.0.0",
"type": "module",
"bin": { "claude-mem-installer": "./dist/index.js" },
"files": ["dist"],
"scripts": {
"build": "node build.mjs",
"dev": "node build.mjs && node dist/index.js"
},
"dependencies": {
"@clack/prompts": "^1.0.1",
"picocolors": "^1.1.1"
},
"devDependencies": {
"esbuild": "^0.24.0",
"typescript": "^5.7.0",
"@types/node": "^22.0.0"
},
"engines": { "node": ">=18.0.0" }
}
```
3. **Create `build.mjs`**:
- esbuild bundle: `entryPoints: ['src/index.ts']`, `format: 'esm'`, `platform: 'node'`, `target: 'node18'`
- Banner: `#!/usr/bin/env node`
- Output: `dist/index.js`
4. **Create `tsconfig.json`**:
- `module: "ESNext"`, `target: "ES2022"`, `moduleResolution: "bundler"`
5. **Run `npm install`** in installer/ directory
### Verification
- [ ] `node build.mjs` succeeds
- [ ] `dist/index.js` exists with shebang
- [ ] `node dist/index.js` runs (even if empty installer)
---
## Phase 2: Entry Point + Welcome Screen
**Goal**: Create the main entry point with TTY detection and a beautiful welcome screen.
### Tasks
1. **`src/index.ts`** — Entry point:
- TTY guard: if `!process.stdin.isTTY`, print error directing user to `npx claude-mem-installer`, exit 1
- Import and call `runInstaller()` from steps
- Top-level catch → `p.cancel()` + exit 1
2. **`src/steps/welcome.ts`** — Welcome step:
- `p.intro()` with styled title using picocolors: `" claude-mem installer "`
- Display version info via `p.log.info()`
- Check if already installed (detect `~/.claude-mem/settings.json` and `~/.claude/plugins/marketplaces/thedotmack/`)
- If upgrade detected, `p.confirm()`: "claude-mem is already installed. Upgrade?"
- `p.select()` for install mode: Fresh Install vs Upgrade vs Configure Only
3. **`src/utils/system.ts`** — System utilities:
- `detectOS()`: returns 'macos' | 'linux' | 'windows'
- `commandExists(cmd)`: checks if command is in PATH
- `runCommand(cmd, args)`: executes shell command, returns { stdout, stderr, exitCode }
- `expandHome(path)`: resolves `~` to home directory
### Verification
- [ ] Running `node dist/index.js` shows intro banner
- [ ] Ctrl+C triggers cancel message
- [ ] Non-TTY (piped) shows error and exits
---
## Phase 3: Dependency Checks
**Goal**: Check and install required dependencies (Bun, uv, git, Node.js version).
### Tasks
1. **`src/steps/dependencies.ts`** — Dependency checker:
- Use `p.tasks()` to check each dependency sequentially with animated spinners:
- **Node.js**: Verify >= 18.0.0 via `process.version`
- **git**: `commandExists('git')`, show install instructions per OS if missing
- **Bun**: Check PATH + common locations (`~/.bun/bin/bun`, `/usr/local/bin/bun`, `/opt/homebrew/bin/bun`). Min version 1.1.14. Offer to auto-install from `https://bun.sh/install`
- **uv**: Check PATH + common locations (`~/.local/bin/uv`, `~/.cargo/bin/uv`). Offer to auto-install from `https://astral.sh/uv/install.sh`
- For missing deps: `p.confirm()` to auto-install, or show manual instructions
- After install attempts, re-verify each dep
2. **`src/utils/dependencies.ts`** — Install helpers:
- `installBun()`: downloads and runs bun install script
- `installUv()`: downloads and runs uv install script
- `findBinary(name, extraPaths[])`: searches PATH + known locations
- `checkVersion(binary, minVersion)`: parses `--version` output
### Verification
- [ ] Shows green checkmarks for found dependencies
- [ ] Shows yellow warnings for missing deps with install option
- [ ] Auto-install actually installs bun/uv when confirmed
- [ ] Fails gracefully if git is missing (can't auto-install)
---
## Phase 4: IDE Selection & Provider Configuration
**Goal**: Let user choose IDEs and configure AI provider with API keys.
### Tasks
1. **`src/steps/ide-selection.ts`** — IDE picker:
- `p.multiselect()` with options:
- Claude Code (default selected, hint: "recommended")
- Cursor
- Windsurf (hint: "coming soon", disabled: true)
- For Claude Code: explain plugin will be registered via marketplace
- For Cursor: explain hooks will be installed via CursorHooksInstaller pattern
- Store selections for later installation steps
2. **`src/steps/provider.ts`** — AI provider configuration:
- `p.select()` for provider:
- **Claude** (hint: "recommended — uses your Claude subscription")
- **Gemini** (hint: "free tier available")
- **OpenRouter** (hint: "free models available")
- **If Claude selected**:
- `p.select()` for auth method: "CLI (Max Plan subscription)" vs "API Key"
- If API key: `p.password()` for key input
- **If Gemini selected**:
- `p.password()` for API key (required)
- `p.select()` for model: gemini-2.5-flash-lite (default), gemini-2.5-flash, gemini-3-flash-preview
- `p.confirm()` for rate limiting (default: true)
- **If OpenRouter selected**:
- `p.password()` for API key (required)
- `p.text()` for model (default: `xiaomi/mimo-v2-flash:free`)
- Validate API keys where possible (non-empty, format check)
### Verification
- [ ] Multiselect allows picking multiple IDEs
- [ ] Provider selection shows correct follow-up prompts
- [ ] API keys are masked during input
- [ ] Cancel at any step triggers graceful exit
---
## Phase 5: Settings Configuration
**Goal**: Configure additional settings with sensible defaults.
### Tasks
1. **`src/steps/settings.ts`** — Settings wizard:
- `p.confirm()`: "Use default settings?" (recommended) — if yes, skip detailed config
- If customizing, use `p.group()` for:
- **Worker port**: `p.text()` with default 37777, validate 1024-65535
- **Data directory**: `p.text()` with default `~/.claude-mem`
- **Context observations**: `p.text()` with default 50, validate 1-200
- **Log level**: `p.select()` — DEBUG, INFO (default), WARN, ERROR
- **Python version**: `p.text()` with default 3.13
- **Chroma vector search**: `p.confirm()` (default: true)
- If yes, `p.select()` mode: local (default) vs remote
- If remote: `p.text()` for host, port, `p.confirm()` for SSL
- Show settings summary via `p.note()` before proceeding
2. **`src/utils/settings-writer.ts`** — Write settings:
- Build flat key-value settings object matching SettingsDefaultsManager schema
- Merge with existing settings if upgrading (preserve user customizations)
- Write to `~/.claude-mem/settings.json`
- Create `~/.claude-mem/` directory if it doesn't exist
### Verification
- [ ] Default settings mode skips all detailed prompts
- [ ] Custom settings validates all inputs
- [ ] Settings file written matches SettingsDefaultsManager schema exactly
- [ ] Existing settings preserved on upgrade
---
## Phase 6: Installation Execution
**Goal**: Clone repo, build plugin, register with IDEs, start worker.
### Tasks
1. **`src/steps/install.ts`** — Installation runner:
- Use `p.tasks()` for visual progress:
- **"Cloning claude-mem repository"**: `git clone --depth 1 https://github.com/thedotmack/claude-mem.git` to temp dir
- **"Installing dependencies"**: `npm install` in cloned repo
- **"Building plugin"**: `npm run build` in cloned repo
- **"Registering plugin"**: Copy plugin files to `~/.claude/plugins/marketplaces/thedotmack/`
- Create marketplace.json, plugin.json structure
- Register in `~/.claude/plugins/known_marketplaces.json`
- Add to `~/.claude/plugins/installed_plugins.json`
- Enable in `~/.claude/settings.json` under `enabledPlugins`
- **"Installing dependencies"** (in marketplace dir): `npm install`
- For Cursor (if selected):
- **"Configuring Cursor hooks"**: Run Cursor hooks installer logic
- Write hooks.json to `~/.cursor/` or project-level `.cursor/`
- Configure MCP in `.cursor/mcp.json`
2. **`src/steps/worker.ts`** — Worker startup:
- Use `p.spinner()` for worker startup:
- Start worker: `bun plugin/scripts/worker-service.cjs` (from marketplace dir)
- Write PID file to `~/.claude-mem/worker.pid`
- Two-stage health check (copy pattern from OpenClaw installer):
- Stage 1: Poll `/api/health` — spinner message: "Starting worker service..."
- Stage 2: Poll `/api/readiness` — spinner message: "Initializing database..."
- Budget: 30 attempts, 1 second apart
- On success: `spinner.stop("Worker running on port {port}")`
- On failure: `spinner.error("Worker failed to start")`, show log path
### Verification
- [ ] Plugin files exist at `~/.claude/plugins/marketplaces/thedotmack/`
- [ ] known_marketplaces.json updated
- [ ] installed_plugins.json updated
- [ ] settings.json has enabledPlugins entry
- [ ] Worker responds to `/api/health` with 200
- [ ] Worker responds to `/api/readiness` with 200
---
## Phase 7: Completion & Summary
**Goal**: Show success screen with configuration summary and next steps.
### Tasks
1. **`src/steps/complete.ts`** — Completion screen:
- `p.note()` with configuration summary:
- Provider + model
- IDEs configured
- Data directory
- Worker port
- Chroma enabled/disabled
- `p.note()` with next steps:
- "Open Claude Code and start a conversation — memory is automatic!"
- "View your memories: http://localhost:{port}"
- "Search past work: use /mem-search in Claude Code"
- If Cursor: "Open Cursor — hooks are active in your projects"
- `p.outro()` with styled completion message
### Verification
- [ ] Summary accurately reflects chosen settings
- [ ] URLs use correct port from settings
- [ ] Next steps are relevant to selected IDEs
---
## Phase 8: curl|bash Bootstrap Script
**Goal**: Create the shell bootstrap script for `curl -fsSL https://install.cmem.ai | bash`.
### Tasks
1. **`install/public/install.sh`** — Bootstrap script:
- Check for Node.js >= 18 (required to run the installer)
- Download bundled installer JS to temp file
- Execute with `node` directly (preserves TTY for @clack/prompts)
- Cleanup temp file on exit (trap)
- Support `--non-interactive` flag passthrough
- Support `--provider=X --api-key=Y` flag passthrough
2. **Update `install/vercel.json`** to serve `install.sh` alongside `openclaw.sh`
### Verification
- [ ] `curl -fsSL https://install.cmem.ai | bash` downloads and runs installer
- [ ] Interactive prompts work after curl download
- [ ] Temp file cleaned up on success and failure
- [ ] Flags pass through correctly
---
## Phase 9: Final Verification
### Checks
- [ ] `npm run build` in installer/ produces single-file `dist/index.js`
- [ ] `node dist/index.js` runs full wizard flow
- [ ] Fresh install on clean system works end-to-end
- [ ] Upgrade path preserves existing settings
- [ ] Ctrl+C at any step exits cleanly
- [ ] Non-TTY shows error message
- [ ] All settings written match SettingsDefaultsManager.ts defaults schema
- [ ] Worker health check succeeds after install
- [ ] Plugin appears in Claude Code plugin list
- [ ] grep for deprecated/non-existent APIs returns 0 results
- [ ] No `require()` calls in source (ESM-only)
- [ ] No `chalk` imports (use picocolors)
-176
View File
@@ -1,176 +0,0 @@
# Bugfix Plan: Observer Sessions Authentication Failure
## Problem Summary
Observer sessions fail with "Invalid API key · Please run /login" because the `CLAUDE_CONFIG_DIR` environment variable is being set to an isolated directory (`~/.claude-mem/observer-config/`) that lacks authentication credentials.
## Root Cause
**File:** `src/services/worker/ProcessRegistry.ts` (lines 207-211)
```typescript
const isolatedEnv = {
...spawnOptions.env,
CLAUDE_CONFIG_DIR: OBSERVER_CONFIG_DIR // <-- This isolates auth credentials!
};
```
This was added in Issue #832 to prevent observer sessions from polluting the `claude --resume` list. However, it also isolates the authentication credentials, breaking the SDK's ability to authenticate with the Anthropic API.
## Evidence
1. Running Claude with alternate config dir reproduces the error:
```bash
CLAUDE_CONFIG_DIR=/tmp/test-claude claude --print "hello"
# Output: Invalid API key · Please run /login
```
2. The observer config directory exists but only has cached feature flags, no authentication:
- `~/.claude-mem/observer-config/.claude.json` - feature flags only
- No credentials copied from main `~/.claude/` directory
## Solution
The fix must allow authentication while still isolating session history. Claude Code stores different data types in `CLAUDE_CONFIG_DIR`:
- Authentication credentials (needed)
- Session history/resume list (should be isolated)
- Feature flags and settings (can be shared or isolated)
**Approach:** Do NOT override `CLAUDE_CONFIG_DIR`. Instead, find an alternative solution for Issue #832.
### Alternative Approaches for Session Isolation
1. **Use `--no-resume` flag** (if SDK supports it) - Prevent observer sessions from being resumable
2. **Accept pollution** - Observer sessions in resume list may be acceptable tradeoff
3. **Post-hoc cleanup** - Clean up observer session entries from history after completion
4. **SDK parameter** - Check if SDK has a session isolation option that doesn't affect auth
---
## Phase 0: Documentation Discovery
### Objective
Understand SDK options for session isolation without breaking authentication.
### Tasks
1. Read SDK documentation/source for:
- Available `query()` options
- Session isolation mechanisms
- Authentication handling
2. Read Issue #832 context:
- What was the original problem?
- How bad was the pollution?
- Are there alternative solutions mentioned?
### Verification
- [ ] List all `query()` options available
- [ ] Identify if `--no-resume` or equivalent exists
- [ ] Document the tradeoffs
---
## Phase 1: Fix Authentication
### Objective
Remove the `CLAUDE_CONFIG_DIR` override to restore authentication.
### File to Modify
`src/services/worker/ProcessRegistry.ts`
### Change
Remove lines 207-211 that override `CLAUDE_CONFIG_DIR`:
**Before:**
```typescript
const isolatedEnv = {
...spawnOptions.env,
CLAUDE_CONFIG_DIR: OBSERVER_CONFIG_DIR
};
```
**After:**
```typescript
const isolatedEnv = {
...spawnOptions.env
// CLAUDE_CONFIG_DIR removed - observer sessions need access to auth credentials
// Session isolation addressed via [alternative approach]
};
```
### Verification
- [ ] Build succeeds: `npm run build`
- [ ] Observer sessions authenticate successfully
- [ ] Observations are saved to database
---
## Phase 2: Address Session Isolation (Issue #832)
### Objective
Find alternative solution to prevent observer sessions from polluting `claude --resume` list.
### Options to Evaluate
1. **Option A: Accept the tradeoff**
- Observer sessions appear in resume list but users can ignore them
- No code changes needed beyond Phase 1
2. **Option B: Use isSynthetic flag**
- If SDK has a flag to mark sessions as non-resumable, use it
- Requires SDK documentation review
3. **Option C: Post-processing cleanup**
- After session ends, remove observer entries from history
- More complex, may have race conditions
### Decision Point
After Phase 0 documentation review, choose the appropriate option.
### Verification
- [ ] Chosen approach documented
- [ ] If code changes made, tests pass
- [ ] Observer sessions either isolated OR tradeoff accepted
---
## Phase 3: Testing
### Manual Tests
1. Start a new Claude Code session with the plugin
2. Verify observations are being saved (check logs)
3. Check that no "Invalid API key" errors appear
4. Verify `claude --resume` behavior (acceptable level of observer entries)
### Verification Checklist
- [ ] `npm run build` succeeds
- [ ] Worker service starts without errors
- [ ] Observations save to database
- [ ] No authentication errors in logs
- [ ] Issue #832 regression acceptable or addressed
---
## Anti-Patterns to Avoid
1. **DO NOT** add `ANTHROPIC_API_KEY` to environment - authentication is handled by Claude Code's built-in credential management
2. **DO NOT** copy credential files to observer config dir - credentials may be in keychain or other secure storage
3. **DO NOT** try to "fix" authentication by adding API key loading - that creates Issue #588 (unexpected API charges)
---
## Files Involved
| File | Purpose |
|------|---------|
| `src/services/worker/ProcessRegistry.ts` | Contains the problematic `CLAUDE_CONFIG_DIR` override |
| `src/shared/paths.ts` | Defines `OBSERVER_CONFIG_DIR` constant |
| `src/services/worker/SDKAgent.ts` | Uses `createPidCapturingSpawn` which sets the env |
---
## Risk Assessment
**Low Risk:** Removing the `CLAUDE_CONFIG_DIR` override is a simple, targeted change.
**Regression Risk (Issue #832):** Observer sessions may appear in `claude --resume` list again. This is a cosmetic issue vs. complete authentication failure, so the tradeoff favors removing the override.
@@ -1,262 +0,0 @@
# CLAUDE.md Path Validation Bug Fix
## Problem Summary
Claude-Mem 9.0's distributed CLAUDE.md feature has a **critical path validation bug** that creates invalid directories when Claude SDK agent outputs non-path strings in file tracking XML tags (`<files_read>`, `<files_modified>`).
### Root Cause
In `src/utils/claude-md-utils.ts:234-239`:
```typescript
if (projectRoot && !path.isAbsolute(filePath)) {
absoluteFilePath = path.join(projectRoot, filePath);
}
```
- `path.isAbsolute('~/.claude-mem/logs')` returns `false` (Node.js doesn't recognize `~`)
- Code joins: `path.join(projectRoot, '~/.claude-mem/logs')``/project/~/.claude-mem/logs`
- `mkdirSync` creates literal directories
### Invalid Directories Currently in Repo
```
./~/ ← literal tilde directory
./PR #610 on thedotmack/ ← GitHub PR reference
./git diff for src/ ← git command text
./https:/code.claude.com/docs/en/ ← URL
```
---
## Implementation Plan
### Phase 1: Add Path Validation Function
**File:** `src/utils/claude-md-utils.ts`
Add new validation function after the imports (around line 16):
```typescript
/**
* Validate that a file path is safe for CLAUDE.md generation.
* Rejects tilde paths, URLs, command-like strings, and paths with invalid chars.
*
* @param filePath - The file path to validate
* @param projectRoot - Optional project root for boundary checking
* @returns true if path is valid for CLAUDE.md processing
*/
function isValidPathForClaudeMd(filePath: string, projectRoot?: string): boolean {
// Reject empty or whitespace-only
if (!filePath || !filePath.trim()) return false;
// Reject tilde paths (Node.js doesn't expand ~)
if (filePath.startsWith('~')) return false;
// Reject URLs
if (filePath.startsWith('http://') || filePath.startsWith('https://')) return false;
// Reject paths with spaces (likely command text or PR references)
if (filePath.includes(' ')) return false;
// Reject paths with # (GitHub issue/PR references)
if (filePath.includes('#')) return false;
// If projectRoot provided, ensure resolved path stays within project
if (projectRoot) {
const resolved = path.resolve(projectRoot, filePath);
const normalizedRoot = path.resolve(projectRoot);
if (!resolved.startsWith(normalizedRoot + path.sep) && resolved !== normalizedRoot) {
return false;
}
}
return true;
}
```
### Phase 2: Integrate Validation in updateFolderClaudeMdFiles
**File:** `src/utils/claude-md-utils.ts`
Modify the file path loop in `updateFolderClaudeMdFiles` (around line 232):
```typescript
for (const filePath of filePaths) {
if (!filePath || filePath === '') continue;
// VALIDATE PATH BEFORE PROCESSING
if (!isValidPathForClaudeMd(filePath, projectRoot)) {
logger.debug('FOLDER_INDEX', 'Skipping invalid file path', {
filePath,
reason: 'Failed path validation'
});
continue;
}
// ... rest of existing logic unchanged
}
```
### Phase 3: Add Unit Tests
**File:** `tests/utils/claude-md-utils.test.ts`
Add new test block after existing tests:
```typescript
describe('path validation in updateFolderClaudeMdFiles', () => {
it('should reject tilde paths', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['~/.claude-mem/logs/worker.log'],
'test-project',
37777,
tempDir
);
expect(fetchMock).not.toHaveBeenCalled();
});
it('should reject URLs', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['https://example.com/file.ts'],
'test-project',
37777,
tempDir
);
expect(fetchMock).not.toHaveBeenCalled();
});
it('should reject paths with spaces', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['PR #610 on thedotmack/CLAUDE.md'],
'test-project',
37777,
tempDir
);
expect(fetchMock).not.toHaveBeenCalled();
});
it('should reject paths with hash symbols', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['issue#123/file.ts'],
'test-project',
37777,
tempDir
);
expect(fetchMock).not.toHaveBeenCalled();
});
it('should reject path traversal outside project', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['../../../etc/passwd'],
'test-project',
37777,
tempDir
);
expect(fetchMock).not.toHaveBeenCalled();
});
it('should accept valid relative paths', async () => {
const apiResponse = {
content: [{ text: '| #123 | 4:30 PM | 🔵 | Test | ~100 |' }]
};
const fetchMock = mock(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(apiResponse)
} as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['src/utils/logger.ts'],
'test-project',
37777,
tempDir
);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
});
```
### Phase 4: Update .gitignore
**File:** `.gitignore`
Add at end of file:
```gitignore
# Prevent literal tilde directories (path validation bug artifacts)
~*/
# Prevent other malformed path directories
http*/
https*/
```
### Phase 5: Clean Up Invalid Directories
**Command sequence:**
```bash
rm -rf "~/."
rm -rf "PR #610 on thedotmack"
rm -rf "git diff for src"
rm -rf "https:"
```
### Phase 6: Verify and Commit
1. Run test suite: `npm test`
2. Run build: `npm run build`
3. Verify no invalid directories remain
4. Commit with message: `fix: Add path validation to CLAUDE.md distribution to prevent invalid directory creation`
---
## Files Modified
| File | Change |
|------|--------|
| `src/utils/claude-md-utils.ts` | Add `isValidPathForClaudeMd()` function + integrate in loop |
| `tests/utils/claude-md-utils.test.ts` | Add 6 new path validation tests |
| `.gitignore` | Add `~*/`, `http*/`, `https*/` patterns |
## Files Deleted
| Path | Reason |
|------|--------|
| `~/` (directory tree) | Invalid literal tilde directory |
| `PR #610 on thedotmack/` | Invalid PR reference directory |
| `git diff for src/` | Invalid git command directory |
| `https:/` | Invalid URL directory |
---
## Risk Assessment
**Low Risk:**
- Validation is additive (only skips invalid paths, doesn't change valid path handling)
- Existing tests remain unchanged
- Fire-and-forget design means failures are logged but don't break hooks
**Testing Coverage:**
- 6 new unit tests covering all rejection cases
- Existing 27 tests verify valid path behavior unchanged
@@ -1,314 +0,0 @@
# Plan: Cleanup worker-service.ts Unjustified Logic
**Created:** 2026-01-13
**Source:** `docs/reports/nonsense-logic.md`
**Target:** `src/services/worker-service.ts` (813 lines)
**Goal:** Address 23 identified issues, prioritizing safe deletions first
---
## Phase 0: Documentation Discovery (COMPLETED)
### Evidence Gathered
**Exit Code Strategy (CLAUDE.md:44-54):**
```
- Exit 0: Success or graceful shutdown (Windows Terminal closes tabs)
- Exit 1: Non-blocking error
- Exit 2: Blocking error
Philosophy: Exit 0 prevents Windows Terminal tab accumulation
```
**Signal Handler Pattern (ProcessManager.ts:294-317):**
- Uses mutable reference object `isShuttingDownRef`
- Factory function `createSignalHandler()` returns handler with embedded state
- Current implementation has 3-hop indirection
**MCP Client Pattern (worker-service.ts:157-160, ChromaSync.ts:124-136):**
```typescript
this.mcpClient = new Client({
name: 'worker-search-proxy',
version: '1.0.0'
}, { capabilities: {} });
```
**Verification Results:**
- `runInteractiveSetup` (lines 439-639): **NEVER CALLED** - grep shows only definition
- `import * as fs from 'fs'` (line 13): **UNUSED** - no `fs.` usage found
- `import { spawn } from 'child_process'` (line 14): **UNUSED** - no `spawn(` calls
- `homedir` (line 15): Only used in `runInteractiveSetup` (dead code)
- `processPendingQueues` default `= 10`: Never used, all callers pass explicit args
---
## Phase 1: Safe Deletions (Dead Code & Unused Imports)
### 1.1 Delete `runInteractiveSetup` Function
**What:** Delete lines 435-639 (~201 lines)
**Why:** Function is defined but never called. Setup happens via `handleCursorCommand()`.
**Evidence:** `grep -n "runInteractiveSetup" src/services/worker-service.ts` returns only definition
**Pattern to follow:** N/A - straight deletion
**Steps:**
1. Read worker-service.ts lines 435-650
2. Delete the entire function including section comment (lines 435-639)
3. Run `npm run build` to verify no compile errors
**Verification:**
- `grep "runInteractiveSetup" src/` returns nothing
- Build succeeds
### 1.2 Remove Unused Imports
**What:** Delete lines 13, 14, 17
**Current (delete these):**
```typescript
import * as fs from 'fs'; // Line 13 - UNUSED
import { spawn } from 'child_process'; // Line 14 - UNUSED
import * as readline from 'readline'; // Line 17 - Only in dead code
```
**Keep:**
```typescript
import { homedir } from 'os'; // Line 15 - DELETE (only in dead code)
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'fs'; // Line 16 - CHECK USAGE
```
**Steps:**
1. After deleting `runInteractiveSetup`, grep for remaining usages:
- `grep "homedir" src/services/worker-service.ts`
- `grep "readline" src/services/worker-service.ts`
- `grep "detectClaudeCode\|findCursorHooksDir\|installCursorHooks\|configureCursorMcp" src/services/worker-service.ts`
2. Delete imports with zero usages
3. Run `npm run build`
**Verification:**
- No TypeScript unused import warnings
- Build succeeds
### 1.3 Clean Up Cursor Integration Imports
After deleting `runInteractiveSetup`, some CursorHooksInstaller imports become unused:
- `detectClaudeCode` - only in runInteractiveSetup
- `findCursorHooksDir` - only in runInteractiveSetup
- `installCursorHooks` - only in runInteractiveSetup
- `configureCursorMcp` - only in runInteractiveSetup
**Steps:**
1. Grep each import after dead code removal
2. Remove any that are now unused
3. Keep `updateCursorContextForProject` (re-exported) and `handleCursorCommand` (used in main)
**Verification:**
- `grep "detectClaudeCode\|findCursorHooksDir\|installCursorHooks\|configureCursorMcp" src/services/worker-service.ts` returns nothing
- Build succeeds
---
## Phase 2: Low-Risk Simplifications
### 2.1 Remove Unused Default Parameter
**What:** Line 350 - `async processPendingQueues(sessionLimit: number = 10)`
**Why:** Default never used. All callers pass explicit args (50 in startup, dynamic in HTTP)
**Change from:**
```typescript
async processPendingQueues(sessionLimit: number = 10): Promise<{...}>
```
**Change to:**
```typescript
async processPendingQueues(sessionLimit: number): Promise<{...}>
```
**Verification:**
- Build succeeds
- All call sites provide explicit values
### 2.2 Simplify onRestart Callback
**Location:** Lines 395-396 (approximate, find exact)
**Issue:** `onShutdown` and `onRestart` both call `this.shutdown()`
**Find pattern:**
```typescript
onShutdown: () => this.shutdown(),
onRestart: () => this.shutdown()
```
**Options:**
1. **Keep as-is** if restart semantically differs from shutdown (future-proofing)
2. **Add comment** explaining intentional parity
3. **Remove onRestart** if never used differently
**Investigation needed:** Grep for `onRestart` usage in Server.ts to understand contract
**Steps:**
1. Grep `onRestart` in `src/services/server/`
2. If Server.ts treats them identically, add clarifying comment
3. If different, document why both map to shutdown
### 2.3 Fix Over-Commented Lines (Sample Only)
**Strategy:** Do NOT strip all comments. Only remove comments that describe obvious code.
**Anti-pattern (remove):**
```typescript
// WHAT: Imports centralized logging utility with structured output
// WHY: All worker logs go through this for consistent formatting
import { logger } from '../utils/logger.js';
```
**Pattern to follow:** Remove WHAT/WHY on simple imports. Keep architectural comments.
**Scope:** Sample 5-10 obvious comment removals to demonstrate approach, not exhaustive
---
## Phase 3: Medium-Risk Improvements
### 3.1 Simplify Signal Handler Pattern
**Current (worker-service.ts:180-192 + ProcessManager.ts:294-317):**
```typescript
// 3-hop indirection with mutable reference
const shutdownRef = { value: this.isShuttingDown };
const handler = createSignalHandler(() => this.shutdown(), shutdownRef);
process.on('SIGTERM', () => {
this.isShuttingDown = shutdownRef.value; // Sync back
handler('SIGTERM');
});
```
**Simplified approach:**
```typescript
private registerSignalHandlers(): void {
const handler = async (signal: string) => {
if (this.isShuttingDown) {
logger.warn('SYSTEM', `Received ${signal} but shutdown already in progress`);
return;
}
this.isShuttingDown = true;
logger.info('SYSTEM', `Received ${signal}, shutting down...`);
try {
await this.shutdown();
process.exit(0);
} catch (error) {
logger.error('SYSTEM', 'Error during shutdown', {}, error as Error);
process.exit(0);
}
};
process.on('SIGTERM', () => handler('SIGTERM'));
process.on('SIGINT', () => handler('SIGINT'));
}
```
**Decision needed:** Does `createSignalHandler` serve other callers? If yes, keep factory but simplify worker usage.
**Steps:**
1. Grep `createSignalHandler` usage across codebase
2. If only worker-service uses it, inline and simplify
3. If shared, simplify worker's usage while keeping factory
### 3.2 Unify Dual Initialization Tracking
**Current (lines 111, 129-130):**
```typescript
private initializationCompleteFlag: boolean = false;
private initializationComplete: Promise<void>;
```
**Recommendation:** Keep both but add clarifying comments:
- Promise: For async waiters (HTTP handlers)
- Flag: For sync checks (status endpoints)
**Alternative:** Use Promise with inspection pattern:
```typescript
private initializationComplete = false;
private initializationPromise: Promise<void>;
// Flag derived from promise state via finally() callback
```
**Steps:**
1. Add documentation comment explaining dual tracking purpose
2. Consider if flag can be derived from promise state instead
### 3.3 Reduce 5-Minute Timeout
**Location:** Lines 464-478 (approximate)
**Current:** `const timeoutMs = 300000; // 5 minutes`
**Recommendation:** Reduce to 30-60 seconds for HTTP handler, keep 5min for MCP init
**Caution:** MCP initialization can legitimately be slow (ChromaDB, model loading). May need different timeouts per use case.
**Steps:**
1. Find exact line for context inject timeout
2. Verify this is separate from MCP init timeout
3. Reduce HTTP handler timeout to 30-60 seconds
4. Keep MCP init timeout at 5 minutes
---
## Phase 4: Deferred / Low Priority
These items are noted but NOT part of this cleanup:
| Issue | Reason to Defer |
|-------|-----------------|
| Exit code 0 always | Documented Windows Terminal workaround - intentional |
| Re-export for circular import | Works correctly, architectural fix is separate work |
| Fallback agent verification | Behavioral change, needs feature design |
| MCP version hardcoding | Low impact, separate version management issue |
| Empty capabilities | Documentation issue only |
| Unsafe `as Error` casts | Common TS pattern, low risk |
---
## Phase 5: Verification
### 5.1 Build Verification
```bash
npm run build
```
Expected: No errors
### 5.2 Test Suite
```bash
npm test
```
Expected: All tests pass
### 5.3 Grep for Anti-patterns
```bash
# Verify dead code removed
grep -r "runInteractiveSetup" src/
# Verify unused imports removed
grep "import \* as fs from 'fs'" src/services/worker-service.ts
grep "import { spawn }" src/services/worker-service.ts
```
Expected: No matches
### 5.4 Runtime Check
```bash
npm run build-and-sync
# Start worker and verify basic operation
```
---
## Summary
| Phase | Items | Estimated Reduction |
|-------|-------|---------------------|
| Phase 1 | Dead code + unused imports | ~210 lines |
| Phase 2 | Low-risk simplifications | ~5 lines + clarity |
| Phase 3 | Medium-risk improvements | ~30 lines |
| Total | | ~245 lines (~30% reduction) |
**Execution Order:** Phase 1 → Phase 2 → Phase 3 → Phase 5 (verification after each)
-516
View File
@@ -1,516 +0,0 @@
# Fix CLAUDE.md Worktree Bug - Implementation Plan
## Problem Statement
CLAUDE.md files are being written to the wrong directory when using git worktrees. The worker service writes files relative to its own `process.cwd()` instead of the project's working directory (`cwd`) from the observation.
**Reproduction scenario:**
1. Start Claude Code in `budapest` worktree → worker starts with `cwd=budapest`
2. Run Claude Code in `~/Scripts/claude-mem/` (main repo)
3. Observations created with relative file paths (e.g., `src/utils/foo.ts`)
4. `updateFolderClaudeMdFiles` writes to `budapest/src/utils/CLAUDE.md` instead of main repo
## Root Cause Analysis
The `cwd` (project root path) IS captured and stored:
- `SessionRoutes.ts:309,403` - receives `cwd` from hooks
- `PendingMessageStore.ts:70` - stores `cwd` in database
- `SDKAgent.ts:295` - passes `cwd` to prompt builder
But `cwd` is NOT passed to file writing:
- `ResponseProcessor.ts:222-225` - calls `updateFolderClaudeMdFiles(allFilePaths, session.project, port)` without `cwd`
- `claude-md-utils.ts:219` - uses `path.dirname(filePath)` which produces relative paths
- Relative paths resolve against worker's `process.cwd()`, not project root
---
## Phase 0: Documentation & API Inventory
### Allowed APIs (from codebase analysis)
**File: `src/utils/claude-md-utils.ts`**
```typescript
export async function updateFolderClaudeMdFiles(
filePaths: string[],
project: string,
port: number
): Promise<void>
```
**File: `src/sdk/parser.ts`**
```typescript
export interface ParsedObservation {
type: string;
title: string | null;
subtitle: string | null;
facts: string[];
narrative: string | null;
concepts: string[];
files_read: string[];
files_modified: string[];
// NOTE: Does NOT include cwd
}
```
**File: `src/services/worker-types.ts`**
```typescript
export interface PendingMessage {
type: 'observation' | 'summarize';
tool_name?: string;
tool_input?: unknown;
tool_response?: unknown;
prompt_number?: number;
cwd?: string; // <-- Source of project root
last_assistant_message?: string;
}
```
**File: `src/shared/paths.ts`** - Path utilities
```typescript
import path from 'path';
// Standard pattern: path.join(baseDir, relativePath)
```
### Anti-Patterns to Avoid
1. **DO NOT** add `cwd` to `ParsedObservation` - it comes from message, not agent response
2. **DO NOT** use `process.cwd()` for project-specific paths
3. **DO NOT** assume file paths are absolute - they are relative from agent response
4. **DO NOT** modify the parser - file paths come from agent XML output
---
## Phase 1: Add `projectRoot` Parameter to `updateFolderClaudeMdFiles`
### What to implement
Modify the function signature to accept an optional `projectRoot` parameter for resolving relative paths to absolute paths.
### Files to modify
**File: `src/utils/claude-md-utils.ts`**
**Location: Lines 206-210 (function signature)**
Current:
```typescript
export async function updateFolderClaudeMdFiles(
filePaths: string[],
project: string,
port: number
): Promise<void>
```
New:
```typescript
export async function updateFolderClaudeMdFiles(
filePaths: string[],
project: string,
port: number,
projectRoot?: string
): Promise<void>
```
**Location: Lines 215-228 (folder extraction logic)**
Current:
```typescript
const folderPaths = new Set<string>();
for (const filePath of filePaths) {
if (!filePath || filePath === '') continue;
const folderPath = path.dirname(filePath);
if (folderPath && folderPath !== '.' && folderPath !== '/') {
if (isProjectRoot(folderPath)) {
logger.debug('FOLDER_INDEX', 'Skipping project root CLAUDE.md', { folderPath });
continue;
}
folderPaths.add(folderPath);
}
}
```
New:
```typescript
const folderPaths = new Set<string>();
for (const filePath of filePaths) {
if (!filePath || filePath === '') continue;
// Resolve relative paths to absolute using projectRoot
let absoluteFilePath = filePath;
if (projectRoot && !path.isAbsolute(filePath)) {
absoluteFilePath = path.join(projectRoot, filePath);
}
const folderPath = path.dirname(absoluteFilePath);
if (folderPath && folderPath !== '.' && folderPath !== '/') {
if (isProjectRoot(folderPath)) {
logger.debug('FOLDER_INDEX', 'Skipping project root CLAUDE.md', { folderPath });
continue;
}
folderPaths.add(folderPath);
}
}
```
### Documentation references
- Pattern for `path.isAbsolute()`: Standard Node.js path module
- Pattern for `path.join(base, relative)`: Used throughout `src/shared/paths.ts`
### Verification checklist
1. [ ] `grep -n "updateFolderClaudeMdFiles" src/utils/claude-md-utils.ts` shows new signature
2. [ ] `grep -n "path.isAbsolute" src/utils/claude-md-utils.ts` confirms new check added
3. [ ] `grep -n "projectRoot" src/utils/claude-md-utils.ts` shows parameter usage
4. [ ] Existing callers still compile (optional param is backward compatible)
### Anti-pattern guards
- **DO NOT** make `projectRoot` required - breaks existing callers
- **DO NOT** use `process.cwd()` as default - defeats purpose of fix
- **DO NOT** modify the API endpoint format - path resolution is caller's responsibility
---
## Phase 2: Pass `cwd` from Message to `updateFolderClaudeMdFiles`
### What to implement
Extract `cwd` from the original messages being processed and pass it to `updateFolderClaudeMdFiles`.
### Challenge
The `syncAndBroadcastObservations` function receives `ParsedObservation[]` which does NOT include `cwd`. The `cwd` is in the original `PendingMessage` but is consumed during prompt generation.
### Solution
Add `projectRoot` parameter to `syncAndBroadcastObservations` and `processAgentResponse`, sourced from `session` or passed through from message processing.
### Files to modify
**File: `src/services/worker/agents/ResponseProcessor.ts`**
**Step 1: Update `processAgentResponse` signature (lines 46-55)**
Current:
```typescript
export async function processAgentResponse(
text: string,
session: ActiveSession,
dbManager: DatabaseManager,
sessionManager: SessionManager,
worker: WorkerRef | undefined,
discoveryTokens: number,
originalTimestamp: number | null,
agentName: string
): Promise<void>
```
New:
```typescript
export async function processAgentResponse(
text: string,
session: ActiveSession,
dbManager: DatabaseManager,
sessionManager: SessionManager,
worker: WorkerRef | undefined,
discoveryTokens: number,
originalTimestamp: number | null,
agentName: string,
projectRoot?: string
): Promise<void>
```
**Step 2: Pass `projectRoot` to `syncAndBroadcastObservations` (line 101-109)**
Current:
```typescript
await syncAndBroadcastObservations(
observations,
result,
session,
dbManager,
worker,
discoveryTokens,
agentName
);
```
New:
```typescript
await syncAndBroadcastObservations(
observations,
result,
session,
dbManager,
worker,
discoveryTokens,
agentName,
projectRoot
);
```
**Step 3: Update `syncAndBroadcastObservations` signature (lines 153-161)**
Current:
```typescript
async function syncAndBroadcastObservations(
observations: ParsedObservation[],
result: StorageResult,
session: ActiveSession,
dbManager: DatabaseManager,
worker: WorkerRef | undefined,
discoveryTokens: number,
agentName: string
): Promise<void>
```
New:
```typescript
async function syncAndBroadcastObservations(
observations: ParsedObservation[],
result: StorageResult,
session: ActiveSession,
dbManager: DatabaseManager,
worker: WorkerRef | undefined,
discoveryTokens: number,
agentName: string,
projectRoot?: string
): Promise<void>
```
**Step 4: Update `updateFolderClaudeMdFiles` call (lines 222-229)**
Current:
```typescript
if (allFilePaths.length > 0) {
updateFolderClaudeMdFiles(
allFilePaths,
session.project,
getWorkerPort()
).catch(error => {
logger.warn('FOLDER_INDEX', 'CLAUDE.md update failed (non-critical)', { project: session.project }, error as Error);
});
}
```
New:
```typescript
if (allFilePaths.length > 0) {
updateFolderClaudeMdFiles(
allFilePaths,
session.project,
getWorkerPort(),
projectRoot
).catch(error => {
logger.warn('FOLDER_INDEX', 'CLAUDE.md update failed (non-critical)', { project: session.project }, error as Error);
});
}
```
### Verification checklist
1. [ ] `grep -n "projectRoot" src/services/worker/agents/ResponseProcessor.ts` shows parameter throughout
2. [ ] `grep -n "processAgentResponse" src/services/worker/*.ts` to find all callers
3. [ ] TypeScript compiles without errors
### Anti-pattern guards
- **DO NOT** extract `cwd` from `ParsedObservation` - it doesn't have one
- **DO NOT** store `cwd` on session globally - messages may come from different cwds (edge case)
---
## Phase 3: Update Agent Callers to Pass `cwd`
### What to implement
Update SDKAgent, GeminiAgent, and OpenRouterAgent to pass `message.cwd` to `processAgentResponse`.
### Files to modify
**File: `src/services/worker/SDKAgent.ts`**
Find the `processAgentResponse` call and add the `projectRoot` parameter from `message.cwd`.
**Pattern to follow (from SDKAgent.ts:289-296):**
```typescript
const obsPrompt = buildObservationPrompt({
id: 0,
tool_name: message.tool_name!,
tool_input: JSON.stringify(message.tool_input),
tool_output: JSON.stringify(message.tool_response),
created_at_epoch: Date.now(),
cwd: message.cwd // <-- This is available
});
```
**Challenge:** `processAgentResponse` is called after the SDK response, not in the message loop. Need to track `lastCwd` from messages.
**Solution:** Store `lastCwd` from messages being processed and pass to `processAgentResponse`.
**File: `src/services/worker/GeminiAgent.ts`** - Same pattern
**File: `src/services/worker/OpenRouterAgent.ts`** - Same pattern
### Implementation pattern for each agent
Add tracking variable:
```typescript
let lastCwd: string | undefined;
```
In message loop, capture cwd:
```typescript
if (message.cwd) {
lastCwd = message.cwd;
}
```
In `processAgentResponse` call:
```typescript
await processAgentResponse(
responseText,
session,
this.dbManager,
this.sessionManager,
worker,
discoveryTokens,
originalTimestamp,
'SDK', // or 'Gemini' or 'OpenRouter'
lastCwd
);
```
### Verification checklist
1. [ ] `grep -n "lastCwd" src/services/worker/SDKAgent.ts` shows tracking
2. [ ] `grep -n "lastCwd" src/services/worker/GeminiAgent.ts` shows tracking
3. [ ] `grep -n "lastCwd" src/services/worker/OpenRouterAgent.ts` shows tracking
4. [ ] `grep -n "processAgentResponse.*lastCwd" src/services/worker/` shows all calls updated
### Anti-pattern guards
- **DO NOT** use `session.cwd` - sessions can have messages from multiple cwds
- **DO NOT** default to `process.cwd()` - defeats the fix
---
## Phase 4: Update Tests
### What to implement
Update existing tests and add new tests for the `projectRoot` functionality.
### Files to modify
**File: `tests/utils/claude-md-utils.test.ts`**
Add test cases for:
1. Relative paths with `projectRoot` resolve correctly
2. Absolute paths ignore `projectRoot`
3. Missing `projectRoot` maintains backward compatibility
### Test pattern to copy
From `tests/utils/claude-md-utils.test.ts:245-266` (folder deduplication test):
```typescript
it('should deduplicate folders from multiple files', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ content: [{ text: mockApiResponse }] })
});
await updateFolderClaudeMdFiles(
['/project/src/utils/file1.ts', '/project/src/utils/file2.ts'],
'test-project',
37777
);
// Should only call API once for the deduplicated folder
expect(mockFetch).toHaveBeenCalledTimes(1);
});
```
### New test to add
```typescript
it('should resolve relative paths using projectRoot', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ content: [{ text: mockApiResponse }] })
});
await updateFolderClaudeMdFiles(
['src/utils/file.ts'], // relative path
'test-project',
37777,
'/home/user/my-project' // projectRoot
);
// Should write to absolute path /home/user/my-project/src/utils/CLAUDE.md
expect(mockWriteClaudeMd).toHaveBeenCalledWith(
'/home/user/my-project/src/utils',
expect.any(String)
);
});
```
### Verification checklist
1. [ ] `bun test tests/utils/claude-md-utils.test.ts` passes
2. [ ] New test case for `projectRoot` exists and passes
---
## Phase 5: Final Verification
### Verification commands
```bash
# 1. Confirm new parameter exists
grep -n "projectRoot" src/utils/claude-md-utils.ts
grep -n "projectRoot" src/services/worker/agents/ResponseProcessor.ts
grep -n "lastCwd" src/services/worker/SDKAgent.ts
# 2. Confirm path.isAbsolute check added
grep -n "path.isAbsolute" src/utils/claude-md-utils.ts
# 3. Confirm all agents updated
grep -n "processAgentResponse.*lastCwd" src/services/worker/*.ts
# 4. Run tests
bun test tests/utils/claude-md-utils.test.ts
# 5. Build and verify no TypeScript errors
npm run build
```
### Anti-pattern grep checks
```bash
# Should NOT find process.cwd() in updateFolderClaudeMdFiles path logic
grep -n "process.cwd" src/utils/claude-md-utils.ts
# Should NOT find cwd in ParsedObservation interface
grep -A 10 "interface ParsedObservation" src/sdk/parser.ts | grep cwd
```
### Manual testing
1. Start worker in one directory
2. Run Claude Code in a different directory (worktree)
3. Make a code change that creates an observation
4. Verify CLAUDE.md is written to the correct project directory
---
## Summary of Changes
| File | Change |
|------|--------|
| `src/utils/claude-md-utils.ts` | Add `projectRoot` param, resolve relative paths |
| `src/services/worker/agents/ResponseProcessor.ts` | Pass `projectRoot` through call chain |
| `src/services/worker/SDKAgent.ts` | Track `lastCwd`, pass to `processAgentResponse` |
| `src/services/worker/GeminiAgent.ts` | Track `lastCwd`, pass to `processAgentResponse` |
| `src/services/worker/OpenRouterAgent.ts` | Track `lastCwd`, pass to `processAgentResponse` |
| `tests/utils/claude-md-utils.test.ts` | Add tests for `projectRoot` behavior |
-266
View File
@@ -1,266 +0,0 @@
# Plan: Fix Empty CLAUDE.md File Generation
## Problem Statement
Currently the CLAUDE.md generator creates files with wasteful content:
1. **Empty files with "No recent activity"** - Files are created even when there are zero observations for a folder
2. **Redundant HTML comment** - "<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->" is unnecessary since the `<claude-mem-context>` tag already conveys this information
These issues create noisy, wasteful context that loads automatically and provides no value.
## Phase 0: Documentation Discovery
### Allowed APIs (from code analysis)
- `formatTimelineForClaudeMd(timelineText: string): string` - src/utils/claude-md-utils.ts:139
- `formatObservationsForClaudeMd(observations, folderPath): string` - scripts/regenerate-claude-md.ts:238
- `writeClaudeMdToFolder(folderPath, newContent): void` - src/utils/claude-md-utils.ts:94
- `updateFolderClaudeMdFiles(filePaths, project, port, projectRoot): Promise<void>` - src/utils/claude-md-utils.ts:257
- `replaceTaggedContent(existingContent, newContent): string` - src/utils/claude-md-utils.ts:64
### Key Locations
| File | Lines | Purpose |
|------|-------|---------|
| `src/utils/claude-md-utils.ts` | 139-235 | Main formatting function |
| `src/utils/claude-md-utils.ts` | 143 | HTML comment generation |
| `src/utils/claude-md-utils.ts` | 209-211 | "No recent activity" handling |
| `src/utils/claude-md-utils.ts` | 322-323 | Write decision point |
| `scripts/regenerate-claude-md.ts` | 238-286 | Regeneration script formatting |
| `scripts/regenerate-claude-md.ts` | 242 | HTML comment generation (duplicate) |
| `scripts/regenerate-claude-md.ts` | 245-247 | "No recent activity" handling |
| `scripts/regenerate-claude-md.ts` | 452-453 | Write decision point |
| `tests/utils/claude-md-utils.test.ts` | 96-109 | Tests for "No recent activity" behavior |
### Anti-patterns to avoid
- Do NOT add new configuration options for this behavior - just fix it
- Do NOT add logging for skipped files (unnecessary noise)
---
## Phase 1: Modify formatTimelineForClaudeMd to Return Empty on No Observations
### Task 1.1: Update formatTimelineForClaudeMd return behavior
**File:** `src/utils/claude-md-utils.ts`
**Lines:** 139-235
**Changes:**
1. Remove HTML comment line at line 143
2. Change the empty observations case (lines 209-211) to return an empty string instead of "No recent activity"
**Before (lines 141-144):**
```typescript
lines.push('# Recent Activity');
lines.push('');
lines.push('<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->');
lines.push('');
```
**After:**
```typescript
lines.push('# Recent Activity');
lines.push('');
```
**Before (lines 209-212):**
```typescript
if (observations.length === 0) {
lines.push('*No recent activity*');
return lines.join('\n');
}
```
**After:**
```typescript
if (observations.length === 0) {
return '';
}
```
### Verification
- Run `bun test tests/utils/claude-md-utils.test.ts`
- Tests at lines 96-109 will FAIL (expected - they test for "No recent activity")
- Update tests to expect empty string for empty input
---
## Phase 2: Update updateFolderClaudeMdFiles to Skip Empty Content
### Task 2.1: Add empty content check before writing
**File:** `src/utils/claude-md-utils.ts`
**Lines:** 322-323
**Changes:**
After formatting, check if result is empty and skip writing if so.
**Before (lines 321-325):**
```typescript
const formatted = formatTimelineForClaudeMd(result.content[0].text);
writeClaudeMdToFolder(folderPath, formatted);
logger.debug('FOLDER_INDEX', 'Updated CLAUDE.md', { folderPath });
```
**After:**
```typescript
const formatted = formatTimelineForClaudeMd(result.content[0].text);
if (!formatted) {
logger.debug('FOLDER_INDEX', 'No observations for folder, skipping', { folderPath });
continue;
}
writeClaudeMdToFolder(folderPath, formatted);
logger.debug('FOLDER_INDEX', 'Updated CLAUDE.md', { folderPath });
```
### Verification
- Grep for files containing "No recent activity": should find none after running
---
## Phase 3: Update Regeneration Script
### Task 3.1: Remove HTML comment from formatObservationsForClaudeMd
**File:** `scripts/regenerate-claude-md.ts`
**Lines:** 238-286
**Changes:**
1. Remove HTML comment line at line 242
2. Change empty observations case (lines 245-247) to return empty string
**Before (lines 240-244):**
```typescript
lines.push('# Recent Activity');
lines.push('');
lines.push('<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->');
lines.push('');
```
**After:**
```typescript
lines.push('# Recent Activity');
lines.push('');
```
**Before (lines 245-248):**
```typescript
if (observations.length === 0) {
lines.push('*No recent activity*');
return lines.join('\n');
}
```
**After:**
```typescript
if (observations.length === 0) {
return '';
}
```
### Task 3.2: Update regenerateFolder to handle empty formatted content
**File:** `scripts/regenerate-claude-md.ts`
**Lines:** 432-459
The script already skips folders with no observations (lines 443-444), so this change is already compatible. The `formatObservationsForClaudeMd` returning empty string doesn't change behavior since observations are checked before calling it.
### Verification
- Run `bun scripts/regenerate-claude-md.ts --dry-run` in the project
- Should NOT show any folders with 0 observations
---
## Phase 4: Update Tests
### Task 4.1: Update tests for new empty behavior
**File:** `tests/utils/claude-md-utils.test.ts`
**Lines:** 96-109
**Changes:**
Update the two tests that expect "No recent activity" to expect empty string instead.
**Before (lines 96-101):**
```typescript
it('should return "No recent activity" for empty input', () => {
const result = formatTimelineForClaudeMd('');
expect(result).toContain('# Recent Activity');
expect(result).toContain('*No recent activity*');
});
```
**After:**
```typescript
it('should return empty string for empty input', () => {
const result = formatTimelineForClaudeMd('');
expect(result).toBe('');
});
```
**Before (lines 103-109):**
```typescript
it('should return "No recent activity" when no table rows exist', () => {
const input = 'Just some plain text without table rows';
const result = formatTimelineForClaudeMd(input);
expect(result).toContain('*No recent activity*');
});
```
**After:**
```typescript
it('should return empty string when no table rows exist', () => {
const input = 'Just some plain text without table rows';
const result = formatTimelineForClaudeMd(input);
expect(result).toBe('');
});
```
### Task 4.2: Remove HTML comment assertions from any other tests
Search for tests that assert on "auto-generated" comment and update accordingly.
### Verification
- Run full test suite: `bun test`
- All tests should pass
---
## Phase 5: Cleanup Existing Empty Files
### Task 5.1: Run cleanup to remove existing empty CLAUDE.md files
**Command:**
```bash
bun scripts/regenerate-claude-md.ts --clean
```
This will:
- Find all CLAUDE.md files with `<claude-mem-context>` tags
- Strip the tagged section
- Delete files that become empty after stripping
- Preserve files that have user content outside the tags
### Verification
- `grep -r "No recent activity" . --include="CLAUDE.md"` should return no results
- `grep -r "auto-generated by claude-mem" . --include="CLAUDE.md"` should return no results
---
## Summary of Changes
| File | Change |
|------|--------|
| `src/utils/claude-md-utils.ts:143` | Remove HTML comment line |
| `src/utils/claude-md-utils.ts:209-211` | Return empty string instead of "No recent activity" |
| `src/utils/claude-md-utils.ts:322` | Skip writing if formatted content is empty |
| `scripts/regenerate-claude-md.ts:242` | Remove HTML comment line |
| `scripts/regenerate-claude-md.ts:245-247` | Return empty string instead of "No recent activity" |
| `tests/utils/claude-md-utils.test.ts:96-109` | Update tests to expect empty string |
## Final Verification Checklist
- [ ] `bun test` passes
- [ ] No "No recent activity" CLAUDE.md files exist
- [ ] No "auto-generated" comments in CLAUDE.md files
- [ ] Build succeeds: `npm run build-and-sync`
- [ ] New observations correctly generate CLAUDE.md files with content
- [ ] Folders without observations get no CLAUDE.md file created
@@ -1,252 +0,0 @@
# Plan: Fix Stale Session Resume Crash
## Problem Summary
The worker crashes repeatedly with "Claude Code process exited with code 1" when attempting to resume into a stale/non-existent SDK session.
**Root Cause:** In `SDKAgent.ts:94`, the resume parameter is passed whenever `memorySessionId` exists in the database, regardless of whether this is an INIT prompt or CONTINUATION prompt. When a worker restarts or re-initializes a session, it loads a stale `memorySessionId` from a previous SDK session and tries to resume into a session that no longer exists in Claude's context.
**Evidence from logs:**
```
[17:30:21.773] Starting SDK query {
hasRealMemorySessionId=true, ← DB has old memorySessionId
resume_parameter=5439891b-..., ← Trying to resume with it
lastPromptNumber=1 ← But this is a NEW SDK session!
}
[17:30:24.450] Generator failed {error=Claude Code process exited with code 1}
```
---
## Phase 0: Documentation Discovery (COMPLETED)
### Allowed APIs (from subagent research)
**V1 SDK API (currently used):**
```typescript
// From @anthropic-ai/claude-agent-sdk
function query(options: {
prompt: string | AsyncIterable<SDKUserMessage>;
options: {
model: string;
resume?: string; // SESSION ID - only use for CONTINUATION
disallowedTools?: string[];
abortController?: AbortController;
pathToClaudeCodeExecutable?: string;
}
}): AsyncIterable<SDKMessage>
```
**Resume Parameter Rules (from docs/context/agent-sdk-v2-preview.md and SESSION_ID_ARCHITECTURE.md):**
- `resume` should only be used when continuing an existing SDK conversation
- For INIT prompts (first prompt in a fresh SDK session), no resume parameter should be passed
- Session ID is captured from first SDK message and stored for subsequent prompts
### Anti-Patterns to Avoid
- Passing `resume` parameter with INIT prompts (causes crash)
- Using `contentSessionId` for resume (contaminates user session)
- Assuming memorySessionId validity without checking prompt context
---
## Phase 1: Fix the Resume Parameter Logic
### What to Implement
Modify `src/services/worker/SDKAgent.ts` line 94 to check BOTH conditions:
1. `hasRealMemorySessionId` - memorySessionId exists and is non-null
2. `session.lastPromptNumber > 1` - this is a CONTINUATION, not an INIT prompt
### Current Code (line 89-99):
```typescript
const queryResult = query({
prompt: messageGenerator,
options: {
model: modelId,
// Resume with captured memorySessionId (null on first prompt, real ID on subsequent)
...(hasRealMemorySessionId && { resume: session.memorySessionId }),
disallowedTools,
abortController: session.abortController,
pathToClaudeCodeExecutable: claudePath
}
});
```
### Fixed Code:
```typescript
const queryResult = query({
prompt: messageGenerator,
options: {
model: modelId,
// Only resume if BOTH: (1) we have a memorySessionId AND (2) this isn't the first prompt
// On worker restart, memorySessionId may exist from a previous SDK session but we
// need to start fresh since the SDK context was lost
...(hasRealMemorySessionId && session.lastPromptNumber > 1 && { resume: session.memorySessionId }),
disallowedTools,
abortController: session.abortController,
pathToClaudeCodeExecutable: claudePath
}
});
```
### Also Update the Comment at Line 66-68:
```typescript
// CRITICAL: Only resume if:
// 1. memorySessionId exists (was captured from a previous SDK response)
// 2. lastPromptNumber > 1 (this is a continuation within the same SDK session)
// On worker restart or crash recovery, memorySessionId may exist from a previous
// SDK session but we must NOT resume because the SDK context was lost.
// NEVER use contentSessionId for resume - that would inject messages into the user's transcript!
```
### Verification Checklist
- [ ] `grep "hasRealMemorySessionId && session.lastPromptNumber > 1" src/services/worker/SDKAgent.ts` returns the fix
- [ ] Build succeeds: `npm run build`
- [ ] No TypeScript errors
---
## Phase 2: Add Logging for Debugging
### What to Implement
Enhance the alignment log at line 81-85 to clearly indicate when resume is skipped due to INIT prompt:
```typescript
// Debug-level alignment logs for detailed tracing
if (session.lastPromptNumber > 1) {
const willResume = hasRealMemorySessionId;
logger.debug('SDK', `[ALIGNMENT] Resume Decision | contentSessionId=${session.contentSessionId} | memorySessionId=${session.memorySessionId} | prompt#=${session.lastPromptNumber} | hasRealMemorySessionId=${hasRealMemorySessionId} | willResume=${willResume} | resumeWith=${willResume ? session.memorySessionId : 'NONE'}`);
} else {
// INIT prompt - never resume even if memorySessionId exists (stale from previous session)
const hasStaleMemoryId = hasRealMemorySessionId;
logger.debug('SDK', `[ALIGNMENT] First Prompt (INIT) | contentSessionId=${session.contentSessionId} | prompt#=${session.lastPromptNumber} | hasStaleMemoryId=${hasStaleMemoryId} | action=START_FRESH | Will capture new memorySessionId from SDK response`);
if (hasStaleMemoryId) {
logger.warn('SDK', `Skipping resume for INIT prompt despite existing memorySessionId=${session.memorySessionId} - SDK context was lost (worker restart or crash recovery)`);
}
}
```
### Verification Checklist
- [ ] Build succeeds: `npm run build`
- [ ] Log message appears when running with stale session scenario
---
## Phase 3: Add Unit Tests
### What to Implement
Create tests in `tests/sdk-agent-resume.test.ts` following patterns from `tests/session_id_usage_validation.test.ts`:
```typescript
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
describe('SDKAgent Resume Parameter Logic', () => {
describe('hasRealMemorySessionId check', () => {
it('should NOT pass resume parameter when lastPromptNumber === 1 even if memorySessionId exists', () => {
// Scenario: Worker restart with stale memorySessionId
const session = {
memorySessionId: 'stale-session-id-from-previous-run',
lastPromptNumber: 1, // INIT prompt
};
const hasRealMemorySessionId = !!session.memorySessionId;
const shouldResume = hasRealMemorySessionId && session.lastPromptNumber > 1;
expect(hasRealMemorySessionId).toBe(true); // memorySessionId exists
expect(shouldResume).toBe(false); // but should NOT resume
});
it('should pass resume parameter when lastPromptNumber > 1 AND memorySessionId exists', () => {
// Scenario: Normal continuation within same SDK session
const session = {
memorySessionId: 'valid-session-id',
lastPromptNumber: 2, // CONTINUATION prompt
};
const hasRealMemorySessionId = !!session.memorySessionId;
const shouldResume = hasRealMemorySessionId && session.lastPromptNumber > 1;
expect(hasRealMemorySessionId).toBe(true);
expect(shouldResume).toBe(true);
});
it('should NOT pass resume parameter when memorySessionId is null', () => {
// Scenario: Fresh session, no captured ID yet
const session = {
memorySessionId: null,
lastPromptNumber: 1,
};
const hasRealMemorySessionId = !!session.memorySessionId;
const shouldResume = hasRealMemorySessionId && session.lastPromptNumber > 1;
expect(hasRealMemorySessionId).toBe(false);
expect(shouldResume).toBe(false);
});
});
});
```
### Documentation Reference
- Pattern: `tests/session_id_usage_validation.test.ts` lines 1-50 for test structure
- Mock pattern: `tests/worker/agents/response-processor.test.ts` for session mocking
### Verification Checklist
- [ ] Tests pass: `bun test tests/sdk-agent-resume.test.ts`
- [ ] Test file follows project conventions
---
## Phase 4: Build and Deploy
### What to Implement
1. Build the plugin: `npm run build-and-sync`
2. Verify worker restarts with fix applied
### Verification Checklist
- [ ] `npm run build-and-sync` succeeds
- [ ] Worker health check passes: `curl http://localhost:37777/api/health`
- [ ] No "Claude Code process exited with code 1" errors in logs after restart
---
## Phase 5: Final Verification
### Verification Commands
```bash
# 1. Verify fix is in place
grep -n "hasRealMemorySessionId && session.lastPromptNumber > 1" src/services/worker/SDKAgent.ts
# 2. Verify no crashes in recent logs
tail -100 ~/.claude-mem/logs/claude-mem-$(date +%Y-%m-%d).log | grep -c "exited with code 1"
# 3. Run tests
bun test tests/sdk-agent-resume.test.ts
# 4. Check for anti-patterns (should return 0 results)
grep -n "hasRealMemorySessionId && { resume" src/services/worker/SDKAgent.ts
```
### Success Criteria
- [ ] Fix in place at SDKAgent.ts:94
- [ ] Zero "exited with code 1" errors related to stale resume
- [ ] All tests pass
- [ ] Worker stable for 10+ minutes without crash loop
---
## Files to Modify
1. `src/services/worker/SDKAgent.ts` - Fix resume logic (Phase 1 & 2)
2. `tests/sdk-agent-resume.test.ts` - New test file (Phase 3)
## Estimated Complexity
- **Phase 1**: Low - Single line change with updated condition
- **Phase 2**: Low - Enhanced logging
- **Phase 3**: Medium - New test file following existing patterns
- **Phase 4-5**: Low - Standard build/verify process
-298
View File
@@ -1,298 +0,0 @@
# Folder CLAUDE.md Generator
## CORE DIRECTIVE (NON-NEGOTIABLE)
**EXTEND THE EXISTING CURSOR RULES TIMELINE GENERATION SYSTEM TO ALSO WRITE CLAUDE.MD FILES**
- DO NOT create new services
- DO NOT create new orchestrators
- DO NOT create new HTTP routes
- DO NOT create new database query functions
- EXTEND existing functions to add folder-level output
---
## Approved Directives (From Planning Conversation)
### Trigger Mechanism
- Observation save triggers folder CLAUDE.md regeneration **INLINE**
- NO batching
- NO debouncing
- NO Set-based queuing
- NO session-end hook
- Synchronous: `observation.save()` → update folder CLAUDE.md files → done
### Tag Strategy
- Wrap ONLY auto-generated content with `<claude-mem-context>` tags
- Everything outside tags is untouched (user's manual content preserved)
- If tags are deleted, just regenerate them
- NO backup system
- NO manual content markers
### Git Behavior
- CLAUDE.md files SHOULD be committed (intentional)
- `<claude-mem-context>` tag is searchable fingerprint for GitHub analytics
- NO .gitignore for these files
### Phasing
- **Phase 1**: CLAUDE.md generation only (THIS PLAN)
- **Phase 2**: IDE symlinks (FUTURE)
### REJECTED
- Cross-folder linking — NO
- Semantic grouping — deferred enhancement only
- Team sync — future phase
### DEFERRED
- Priority weighting by observation type
- IDE-specific template refinements
---
## Phase 0: Documentation Discovery (COMPLETED)
### Existing APIs to USE (Not Rebuild)
| Function | Location | Purpose |
|----------|----------|---------|
| `findByFile(filePath, options)` | `src/services/sqlite/SessionSearch.ts:342` | Query observations by folder prefix (already supports LIKE wildcards) |
| `updateCursorContextForProject()` | `src/services/integrations/CursorHooksInstaller.ts:98` | Write context files after observation save |
| `writeContextFile()` | `src/utils/cursor-utils.ts:97` | Atomic file write with temp file + rename |
| `extractFirstFile()` | `src/shared/timeline-formatting.ts` | Extract file paths from JSON arrays |
| `groupByDate()` | `src/shared/timeline-formatting.ts` | Group items chronologically |
| `formatTime()`, `formatDate()` | `src/shared/timeline-formatting.ts` | Time formatting |
### Existing Integration Points
| Location | What Happens | Extension Point |
|----------|--------------|-----------------|
| `ResponseProcessor.ts:266` | Calls `updateCursorContextForProject()` after summary save | Add folder CLAUDE.md update here |
| `CursorHooksInstaller.ts:98` | `updateCursorContextForProject()` fetches context and writes file | Add sibling function for folder updates |
### Anti-Patterns to AVOID
- Creating `FolderIndexOrchestrator.ts` — NO
- Creating `FolderTimelineCompiler.ts` — NO
- Creating `FolderDiscovery.ts` — NO
- Creating `ClaudeMdGenerator.ts` — NO
- Creating `FolderIndexRoutes.ts` — NO
- Adding new HTTP endpoints — NO
- Adding new settings in `SettingsDefaultsManager.ts` — NO (use sensible defaults inline)
---
## Phase 1: Extend CursorHooksInstaller
### What to Implement
Add ONE new function to `src/services/integrations/CursorHooksInstaller.ts`:
```typescript
/**
* Update CLAUDE.md files for folders touched by an observation.
* Called inline after observation save, similar to updateCursorContextForProject.
*/
export async function updateFolderClaudeMd(
workspacePath: string,
filesModified: string[],
filesRead: string[],
project: string,
port: number
): Promise<void>
```
### Implementation Pattern (Copy From)
Follow the EXACT pattern of `updateCursorContextForProject()` at line 98:
1. Extract unique folder paths from filesModified and filesRead
2. For each folder, fetch timeline via existing `/api/search/file?files=<folderPath>` endpoint
3. Format as simple timeline (reuse existing formatters)
4. Write to `<folder>/CLAUDE.md` preserving content outside `<claude-mem-context>` tags
### Tag Preservation Logic
```typescript
function replaceTaggedContent(existingContent: string, newContent: string): string {
const startTag = '<claude-mem-context>';
const endTag = '</claude-mem-context>';
// If no existing content, wrap new content in tags
if (!existingContent) {
return `${startTag}\n${newContent}\n${endTag}`;
}
// If existing has tags, replace only tagged section
const startIdx = existingContent.indexOf(startTag);
const endIdx = existingContent.indexOf(endTag);
if (startIdx !== -1 && endIdx !== -1) {
return existingContent.substring(0, startIdx) +
`${startTag}\n${newContent}\n${endTag}` +
existingContent.substring(endIdx + endTag.length);
}
// If no tags exist, append tagged content at end
return existingContent + `\n\n${startTag}\n${newContent}\n${endTag}`;
}
```
### Verification Checklist
- [ ] Function added to CursorHooksInstaller.ts
- [ ] Uses existing `findByFile` endpoint (no new database queries)
- [ ] Preserves content outside `<claude-mem-context>` tags
- [ ] Atomic writes (temp file + rename)
- [ ] Build passes: `npm run build`
---
## Phase 2: Hook Into ResponseProcessor
### What to Implement
Add call to `updateFolderClaudeMd()` in `src/services/worker/agents/ResponseProcessor.ts`, right after the existing `updateCursorContextForProject()` call at line 266.
### Code Location
In `syncAndBroadcastSummary()` function, after line 269:
```typescript
// EXISTING: Update Cursor context file for registered projects (fire-and-forget)
updateCursorContextForProject(session.project, getWorkerPort()).catch(error => {
logger.warn('CURSOR', 'Context update failed (non-critical)', { project: session.project }, error as Error);
});
// NEW: Update folder CLAUDE.md files for touched folders (fire-and-forget)
// Extract file paths from the saved observations
updateFolderClaudeMd(
workspacePath, // From registry lookup
filesModified, // From observations
filesRead, // From observations
session.project,
getWorkerPort()
).catch(error => {
logger.warn('FOLDER_INDEX', 'CLAUDE.md update failed (non-critical)', { project: session.project }, error as Error);
});
```
### Data Flow
1. `processAgentResponse()` saves observations → gets back `observationIds`
2. Fetch observation records to get `files_read` and `files_modified`
3. Pass to `updateFolderClaudeMd()`
### Verification Checklist
- [ ] Call added to ResponseProcessor.ts
- [ ] Fire-and-forget pattern (non-blocking, errors logged)
- [ ] Uses existing observation data (no new queries)
- [ ] Build passes: `npm run build`
---
## Phase 3: Timeline Formatting
### What to Implement
Create a minimal timeline formatter for CLAUDE.md output. This can be:
1. A simple function in CursorHooksInstaller.ts, OR
2. Reuse existing `ResultFormatter.formatSearchResults()` from `src/services/worker/search/ResultFormatter.ts`
### Output Format
```markdown
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
<claude-mem-context>
### 2026-01-04
| Time | Type | Title |
|------|------|-------|
| 4:30pm | feature | Added folder index support |
| 3:15pm | bugfix | Fixed file path handling |
### 2026-01-03
| Time | Type | Title |
|------|------|-------|
| 11:00am | refactor | Cleaned up cursor utils |
</claude-mem-context>
```
### Key Points
- Compact format (time, type emoji, title only)
- Grouped by date
- Limited to last N days or observations (sensible default: 10)
- NO token counts
- NO file columns (redundant - we're IN the folder)
### Verification Checklist
- [ ] Formatter produces clean markdown
- [ ] Output is concise (not verbose)
- [ ] Grouped by date
- [ ] Build passes: `npm run build`
---
## Phase 4: Verification
### Functional Tests
1. **Manual Test**:
- Start worker: `npm run dev`
- Create a test observation touching `src/services/sqlite/`
- Verify `src/services/sqlite/CLAUDE.md` is created/updated
- Verify `<claude-mem-context>` tags are present
- Verify manual content outside tags is preserved
2. **Build Check**:
```bash
npm run build
```
3. **Grep for Anti-Patterns**:
```bash
# Should find NOTHING
grep -r "FolderIndexOrchestrator" src/
grep -r "FolderTimelineCompiler" src/
grep -r "FolderDiscovery" src/
grep -r "ClaudeMdGenerator" src/
grep -r "FolderIndexRoutes" src/
```
4. **Grep for Correct Implementation**:
```bash
# Should find the new function
grep -r "updateFolderClaudeMd" src/
```
### Tag Preservation Test
1. Create `src/test-folder/CLAUDE.md` with manual content:
```markdown
# My Notes
This is manual content I wrote.
```
2. Trigger observation save touching files in `src/test-folder/`
3. Verify result:
```markdown
# My Notes
This is manual content I wrote.
<claude-mem-context>
### 2026-01-04
| Time | Type | Title |
...
</claude-mem-context>
```
---
## Summary
This is a **~100 line change** spread across 2 files:
1. `CursorHooksInstaller.ts` — Add `updateFolderClaudeMd()` function (~60 lines)
2. `ResponseProcessor.ts` — Add call to the new function (~10 lines)
NO new files. NO new services. NO new routes. Just extending existing patterns.
-378
View File
@@ -1,378 +0,0 @@
# Folder CLAUDE.md Refactor - Extract to Shared Utils
## CORE DIRECTIVE
**DECOUPLE FOLDER CLAUDE.MD WRITING FROM CURSOR INTEGRATION**
The current implementation incorrectly couples folder-level CLAUDE.md generation to Cursor-specific registry lookups. The file paths from observations are already absolute - no workspace registry lookup is needed.
---
## Phase 0: Documentation Discovery (COMPLETED)
### Current Implementation Location
| Function | Location | Lines | Purpose |
|----------|----------|-------|---------|
| `updateFolderClaudeMd` | CursorHooksInstaller.ts | 128-199 | Orchestrates folder CLAUDE.md updates |
| `formatTimelineForClaudeMd` | CursorHooksInstaller.ts | 221-295 | Parses API response to markdown |
| `replaceTaggedContent` | CursorHooksInstaller.ts | 300-321 | Preserves user content outside tags |
| `writeFolderClaudeMd` | CursorHooksInstaller.ts | 326-353 | Atomic file write |
### Integration Point
**File:** `src/services/worker/agents/ResponseProcessor.ts:274-298`
Current (problematic) code:
```typescript
const registry = readCursorRegistry();
const registryEntry = registry[session.project];
if (registryEntry && (filesModified.length > 0 || filesRead.length > 0)) {
updateFolderClaudeMd(
registryEntry.workspacePath, // <-- PROBLEM: Needs Cursor registry
filesModified,
filesRead,
session.project,
getWorkerPort()
).catch(error => { ... });
}
```
### The Problem
1. `filesModified` and `filesRead` already contain **absolute paths**
2. We don't need `workspacePath` - just extract folder from file path directly
3. Cursor registry is only populated when Cursor hooks are installed
4. This makes folder CLAUDE.md a Cursor-only feature (unintended)
### Project Utils Pattern
**From `src/utils/cursor-utils.ts:97-122`:**
- Pure functions with paths as parameters
- Atomic write pattern: temp file + rename
- `mkdirSync(dir, { recursive: true })` for directory creation
### Related Utils
**`src/utils/tag-stripping.ts`** - Handles *stripping* tags (input filtering)
- `stripMemoryTagsFromJson()` - removes `<claude-mem-context>` content
- `stripMemoryTagsFromPrompt()` - removes `<private>` content
Our `replaceTaggedContent` handles *preserving/replacing* (output writing) - complementary, not duplicative.
---
## Phase 1: Create Shared Utils File
### What to Implement
Create `src/utils/claude-md-utils.ts` with extracted and simplified functions.
### File Structure
```typescript
/**
* CLAUDE.md File Utilities
*
* Shared utilities for writing folder-level CLAUDE.md files with
* auto-generated context sections. Preserves user content outside
* <claude-mem-context> tags.
*/
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'fs';
import path from 'path';
import { logger } from './logger.js';
/**
* Replace tagged content in existing file, preserving content outside tags.
*
* Handles three cases:
* 1. No existing content → wraps new content in tags
* 2. Has existing tags → replaces only tagged section
* 3. No tags in existing content → appends tagged content at end
*/
export function replaceTaggedContent(existingContent: string, newContent: string): string {
// Copy from CursorHooksInstaller.ts:300-321
}
/**
* Write CLAUDE.md file to folder with atomic writes.
* Creates directory structure if needed.
*
* @param folderPath - Absolute path to the folder
* @param newContent - Content to write inside tags
*/
export function writeClaudeMdToFolder(folderPath: string, newContent: string): void {
// Simplified from writeFolderClaudeMd - no workspacePath needed
// Copy atomic write pattern from CursorHooksInstaller.ts:326-353
}
/**
* Format timeline text from API response to compact CLAUDE.md format.
*
* @param timelineText - Raw API response text
* @returns Formatted markdown with date headers and compact table
*/
export function formatTimelineForClaudeMd(timelineText: string): string {
// Copy from CursorHooksInstaller.ts:221-295
}
```
### Key Simplification
**OLD `writeFolderClaudeMd` signature:**
```typescript
async function writeFolderClaudeMd(
workspacePath: string, // <-- REMOVE
folderPath: string,
newContent: string
): Promise<void>
```
**NEW `writeClaudeMdToFolder` signature:**
```typescript
export function writeClaudeMdToFolder(
folderPath: string, // Must be absolute path
newContent: string
): void // Sync is fine, atomic anyway
```
### Verification Checklist
- [ ] File created at `src/utils/claude-md-utils.ts`
- [ ] `replaceTaggedContent` exported and handles all 3 cases
- [ ] `writeClaudeMdToFolder` exported with atomic writes
- [ ] `formatTimelineForClaudeMd` exported
- [ ] Build passes: `npm run build`
---
## Phase 2: Create Folder Index Service Function
### What to Implement
Create a new orchestrating function that replaces `updateFolderClaudeMd`. This should NOT be in CursorHooksInstaller - it's a general feature.
**Option A:** Add to `src/utils/claude-md-utils.ts` (keeps it simple)
**Option B:** Create `src/services/folder-index-service.ts` (follows service pattern)
Recommend **Option A** for simplicity - it's just one function.
### New Function
```typescript
/**
* Update CLAUDE.md files for folders containing the given files.
* Fetches timeline from worker API and writes formatted content.
*
* @param filePaths - Array of absolute file paths (modified or read)
* @param project - Project identifier for API query
* @param port - Worker API port
*/
export async function updateFolderClaudeMdFiles(
filePaths: string[],
project: string,
port: number
): Promise<void> {
// Extract unique folder paths from file paths
const folderPaths = new Set<string>();
for (const filePath of filePaths) {
if (!filePath || filePath === '') continue;
const folderPath = path.dirname(filePath);
if (folderPath && folderPath !== '.' && folderPath !== '/') {
folderPaths.add(folderPath);
}
}
if (folderPaths.size === 0) return;
logger.debug('FOLDER_INDEX', 'Updating CLAUDE.md files', {
project,
folderCount: folderPaths.size
});
// Process each folder
for (const folderPath of folderPaths) {
try {
// Fetch timeline via existing API
const response = await fetch(
`http://127.0.0.1:${port}/api/search/by-file?filePath=${encodeURIComponent(folderPath)}&limit=10&project=${encodeURIComponent(project)}`
);
if (!response.ok) {
logger.warn('FOLDER_INDEX', 'Failed to fetch timeline', { folderPath, status: response.status });
continue;
}
const result = await response.json();
if (!result.content?.[0]?.text) {
logger.debug('FOLDER_INDEX', 'No content for folder', { folderPath });
continue;
}
const formatted = formatTimelineForClaudeMd(result.content[0].text);
writeClaudeMdToFolder(folderPath, formatted);
logger.debug('FOLDER_INDEX', 'Updated CLAUDE.md', { folderPath });
} catch (error) {
logger.warn('FOLDER_INDEX', 'Failed to update CLAUDE.md', { folderPath }, error as Error);
}
}
}
```
### Verification Checklist
- [ ] `updateFolderClaudeMdFiles` function added
- [ ] Takes only `filePaths`, `project`, `port` (no workspacePath)
- [ ] Extracts folder paths from absolute file paths
- [ ] Uses `writeClaudeMdToFolder` for atomic writes
- [ ] Build passes: `npm run build`
---
## Phase 3: Update ResponseProcessor Integration
### What to Implement
Simplify the call site in `src/services/worker/agents/ResponseProcessor.ts`.
### Current Code (lines 274-298)
```typescript
// Update folder CLAUDE.md files for touched folders (fire-and-forget)
const filesModified: string[] = [];
const filesRead: string[] = [];
for (const obs of observations) {
filesModified.push(...(obs.files_modified || []));
filesRead.push(...(obs.files_read || []));
}
// Get workspace path from project registry
const registry = readCursorRegistry();
const registryEntry = registry[session.project];
if (registryEntry && (filesModified.length > 0 || filesRead.length > 0)) {
updateFolderClaudeMd(
registryEntry.workspacePath,
filesModified,
filesRead,
session.project,
getWorkerPort()
).catch(error => {
logger.warn('FOLDER_INDEX', 'CLAUDE.md update failed (non-critical)', { project: session.project }, error as Error);
});
}
```
### New Code
```typescript
// Update folder CLAUDE.md files for touched folders (fire-and-forget)
const allFilePaths: string[] = [];
for (const obs of observations) {
allFilePaths.push(...(obs.files_modified || []));
allFilePaths.push(...(obs.files_read || []));
}
if (allFilePaths.length > 0) {
updateFolderClaudeMdFiles(
allFilePaths,
session.project,
getWorkerPort()
).catch(error => {
logger.warn('FOLDER_INDEX', 'CLAUDE.md update failed (non-critical)', { project: session.project }, error as Error);
});
}
```
### Import Changes
**Remove:**
```typescript
import { updateFolderClaudeMd, readCursorRegistry } from '../../integrations/CursorHooksInstaller.js';
```
**Add:**
```typescript
import { updateFolderClaudeMdFiles } from '../../../utils/claude-md-utils.js';
```
**Keep (if still needed for Cursor context):**
```typescript
import { updateCursorContextForProject } from '../../worker-service.js';
```
### Verification Checklist
- [ ] Import updated to use `claude-md-utils.ts`
- [ ] `readCursorRegistry` import removed (if no longer needed)
- [ ] Call site simplified - no registry lookup
- [ ] Fire-and-forget pattern preserved
- [ ] Build passes: `npm run build`
---
## Phase 4: Clean Up CursorHooksInstaller
### What to Implement
Remove the extracted functions from `src/services/integrations/CursorHooksInstaller.ts`.
### Functions to Remove
- `updateFolderClaudeMd` (lines 128-199)
- `formatTimelineForClaudeMd` (lines 221-295)
- `replaceTaggedContent` (lines 300-321)
- `writeFolderClaudeMd` (lines 326-353)
### Verification Checklist
- [ ] All 4 functions removed from CursorHooksInstaller.ts
- [ ] No dangling references to removed functions
- [ ] CursorHooksInstaller still exports what it needs for Cursor integration
- [ ] Build passes: `npm run build`
- [ ] Grep shows no references to old function locations
---
## Phase 5: Verification
### Build Check
```bash
npm run build
```
### Anti-Pattern Grep (should find NOTHING in CursorHooksInstaller)
```bash
grep -n "updateFolderClaudeMd\|formatTimelineForClaudeMd\|replaceTaggedContent\|writeFolderClaudeMd" src/services/integrations/CursorHooksInstaller.ts
```
### Correct Location Grep (should find in claude-md-utils)
```bash
grep -rn "updateFolderClaudeMdFiles\|writeClaudeMdToFolder\|formatTimelineForClaudeMd" src/utils/
```
### Integration Check
```bash
grep -n "updateFolderClaudeMdFiles" src/services/worker/agents/ResponseProcessor.ts
```
### No Cursor Registry Dependency
```bash
grep -n "readCursorRegistry" src/services/worker/agents/ResponseProcessor.ts
# Should return nothing (or only for Cursor context, not folder index)
```
---
## Summary
**~150 lines moved** from CursorHooksInstaller.ts to claude-md-utils.ts with simplification:
| Before | After |
|--------|-------|
| 4 functions in CursorHooksInstaller | 4 functions in claude-md-utils |
| Requires Cursor registry lookup | Works with absolute paths directly |
| `updateFolderClaudeMd(workspacePath, ...)` | `updateFolderClaudeMdFiles(filePaths, ...)` |
| Coupled to Cursor integration | Independent utility |
**Files Changed:**
1. `src/utils/claude-md-utils.ts` - NEW (create)
2. `src/services/worker/agents/ResponseProcessor.ts` - UPDATE (simplify call site)
3. `src/services/integrations/CursorHooksInstaller.ts` - UPDATE (remove extracted functions)
@@ -1,186 +0,0 @@
# Plan: Change Folder CLAUDE.md to Timeline Format
## Goal
Replace the simple table format in folder-level CLAUDE.md files with the timeline format used by search results.
## Current vs Target Format
### Current Format (Simple)
```markdown
# Recent Activity
### Recent
| Time | Type | Title |
|------|------|-------|
| 6:33pm | feature | Multiple CLAUDE.md files generated |
| 6:32pm | feature | CLAUDE.md file successfully generated |
```
### Target Format (Timeline)
```markdown
# Recent Activity
### Jan 4, 2026
**src/services/worker/agents/ResponseProcessor.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37110 | 6:35 PM | 🔴 | Folder CLAUDE.md updates moved from summary | ~85 |
| #37109 | " | ✅ | ResponseProcessor.ts modified | ~92 |
**General**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37108 | 6:33 PM | 🟣 | Multiple CLAUDE.md files generated | ~78 |
```
## Key Changes
1. **Group by date** - Use `### Jan 4, 2026` instead of `### Recent`
2. **Group by file within each date** - Add `**filename**` headers
3. **Expand columns** - Add ID and Read columns: `| ID | Time | T | Title | Read |`
4. **Use type emojis** - Use `🔴` `🟣` `✅` etc. instead of text
5. **Show ditto marks** - Use `"` for repeated times
---
## Phase 1: Refactor formatTimelineForClaudeMd
**File:** `src/utils/claude-md-utils.ts`
**Tasks:**
1. Add imports from shared utilities:
```typescript
import { formatDate, formatTime, extractFirstFile, estimateTokens, groupByDate } from '../shared/timeline-formatting.js';
import { ModeManager } from '../services/domain/ModeManager.js';
```
2. Replace `formatTimelineForClaudeMd()` (lines 78-151) with new implementation that:
- Parses API response to extract full observation data (id, time, type emoji, title, files)
- Groups observations by date using `groupByDate()`
- Within each date, groups by file using a Map
- Renders file sections with `**filename**` headers
- Uses search table format: `| ID | Time | T | Title | Read |`
- Uses ditto marks for repeated times
**Pattern to Copy From:** `src/services/worker/search/ResultFormatter.ts` lines 56-108
**Key APIs:**
- `groupByDate(items, getDate)` - from `src/shared/timeline-formatting.ts:104-127`
- `formatTime(epoch)` - from `src/shared/timeline-formatting.ts:46-53`
- `formatDate(epoch)` - from `src/shared/timeline-formatting.ts:59-66`
- `extractFirstFile(filesModified, cwd)` - from `src/shared/timeline-formatting.ts:81-84`
- `estimateTokens(text)` - from `src/shared/timeline-formatting.ts:89-92`
- `ModeManager.getInstance().getTypeIcon(type)` - from `src/services/domain/ModeManager.ts`
**Verification:**
1. Run `npm run build` - no errors
2. Restart worker: `npm run worker:restart`
3. Make a test edit to trigger observation
4. Check generated CLAUDE.md files for new format
---
## Phase 2: Parse Full Observation Data from API
**Context:** The current regex parsing extracts only time, type emoji, and title. Need to also extract:
- Observation ID (for `#123` column)
- File path (from files_modified in API response, for grouping)
- Token estimate (for `Read` column)
**Challenge:** The current API returns formatted text, not structured data. We need to:
1. Parse the existing text format more thoroughly, OR
2. Use a different API endpoint that returns JSON
**Decision Point:** Check what data the `/api/search/by-file` endpoint returns. If it returns structured JSON with observations, use that. Otherwise, enhance parsing.
**Investigation Required:**
- Read `src/services/worker/http/routes/SearchRoutes.ts` to see by-file response format
- Determine if we can access raw observation data or just formatted text
**Verification:**
- Confirm API response structure
- Update parsing to extract all needed fields
---
## Phase 3: Integrate File-Based Grouping
**File:** `src/utils/claude-md-utils.ts`
**Tasks:**
1. Create helper to group by file:
```typescript
function groupByFile(observations: ParsedObservation[]): Map<string, ParsedObservation[]> {
const byFile = new Map<string, ParsedObservation[]>();
for (const obs of observations) {
const file = obs.file || 'General';
if (!byFile.has(file)) byFile.set(file, []);
byFile.get(file)!.push(obs);
}
return byFile;
}
```
2. Render with file sections:
```typescript
for (const [file, fileObs] of resultsByFile) {
lines.push(`**${file}**`);
lines.push(`| ID | Time | T | Title | Read |`);
lines.push(`|----|------|---|-------|------|`);
// render rows with ditto marks
}
```
**Pattern to Copy From:** `ResultFormatter.formatSearchResults()` lines 60-108
**Verification:**
- Generated CLAUDE.md shows file grouping
- Files are displayed as relative paths when possible
---
## Phase 4: Final Verification
**Checklist:**
1. **Build passes:** `npm run build`
2. **Worker restarts cleanly:** `npm run worker:restart`
3. **Format matches target:**
- Date headers: `### Jan 4, 2026`
- File sections: `**filename**`
- Table columns: `| ID | Time | T | Title | Read |`
- Type emojis: `🔴` `🟣` `` not text
- Ditto marks: `"` for repeated times
4. **Anti-pattern checks:**
- No hardcoded type maps (use ModeManager)
- No invented APIs
- Reuses existing formatters from shared utils
5. **Graceful degradation:** Empty results still show `*No recent activity*`
---
## Files to Modify
| File | Change |
|------|--------|
| `src/utils/claude-md-utils.ts` | Replace `formatTimelineForClaudeMd()` with timeline format |
## Files to Read (Patterns to Copy)
| File | Pattern |
|------|---------|
| `src/services/worker/search/ResultFormatter.ts:56-108` | Date/file grouping logic |
| `src/shared/timeline-formatting.ts` | All formatting utilities |
| `src/services/domain/ModeManager.ts` | Type icon lookup |
## Anti-Patterns to Avoid
- ❌ Creating new hardcoded type→emoji maps (use ModeManager)
- ❌ Parsing dates manually (use shared formatters)
- ❌ Skipping the existing groupByDate utility
- ❌ Not handling ditto marks for repeated times
@@ -1,356 +0,0 @@
# Execution Plan: Intentional Patterns Validation Actions
**Created:** 2026-01-13
**Source:** `docs/reports/intentional-patterns-validation.md`
**Target:** `src/services/worker-service.ts` and related files
---
## Phase 0: Documentation Discovery (COMPLETED)
### Evidence Gathered
**Files Analyzed:**
- `docs/reports/intentional-patterns-validation.md` - Pattern verdicts and recommendations
- `docs/reports/nonsense-logic.md` - Original 23 issues identified
- `.claude/plans/cleanup-worker-service-nonsense-logic.md` - Existing cleanup plan
- `src/services/worker-service.ts` (813 lines) - Current state
**Current State:**
- File has been reduced from 1445 lines to 813 lines in prior refactoring
- `runInteractiveSetup` still exists at line 439 (~200 lines of dead code)
- Re-export at line 78: `export { updateCursorContextForProject };`
- MCP version hardcoded "1.0.0" at line 159
- Fallback agents set at lines 144-146 without verification
- Unused imports: `fs`, `spawn`, `homedir`, `readline` at lines 13-17
**Allowed APIs (from validation report):**
- Exit code 0 pattern: **KEEP** (documented Windows Terminal workaround)
- `as Error` casts: **KEEP** (documented project policy)
- Dual init tracking: **KEEP** (serves async + sync callers)
- Signal handler ref pattern: **KEEP** (standard JS mutable state sharing)
- Empty MCP capabilities: **KEEP** (correct per MCP spec)
**Actions Required:**
| Pattern | Action | Priority |
|---------|--------|----------|
| Re-export for circular import | Remove (no actual circular dep) | LOW |
| Fallback agent without check | Add availability verification | HIGH |
| MCP version hardcoded | Update to use package.json | LOW |
| Dead code `runInteractiveSetup` | Delete (~200 lines) | HIGH |
| Unused imports | Delete | LOW |
---
## Phase 1: Delete Dead Code (HIGH PRIORITY)
### 1.1 Delete `runInteractiveSetup` Function
**What:** Delete lines 435-639 (approximately 200 lines)
**File:** `src/services/worker-service.ts`
**Location confirmed:** Line 439 starts `async function runInteractiveSetup(): Promise<number>`
**Steps:**
1. Read worker-service.ts lines 435-650 to find exact boundaries
2. Delete the section comment and entire function
3. Run build to verify no compile errors
**Verification:**
```bash
grep -n "runInteractiveSetup" src/services/worker-service.ts
# Expected: No output (function deleted)
npm run build
# Expected: No errors
```
### 1.2 Remove Unused Imports
**What:** Delete imports only used by dead code
**Lines to delete:** 13-17 (check each)
**Current imports to remove:**
```typescript
import * as fs from 'fs'; // Line 13 - UNUSED (namespace never accessed)
import { spawn } from 'child_process'; // Line 14 - UNUSED (MCP uses StdioClientTransport)
import { homedir } from 'os'; // Line 15 - Only in dead code
import * as readline from 'readline'; // Line 17 - Only in dead code
```
**Keep:**
```typescript
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'fs'; // Line 16 - CHECK
```
**Steps:**
1. After deleting `runInteractiveSetup`, grep each import
2. Delete any with zero usages
3. Run build to verify
**Verification:**
```bash
grep -n "^import \* as fs" src/services/worker-service.ts
grep -n "import { spawn }" src/services/worker-service.ts
# Expected: No output
npm run build
```
### 1.3 Remove Unused CursorHooksInstaller Imports
**After deleting dead code, check:**
```typescript
import {
updateCursorContextForProject, // KEEP (re-exported)
handleCursorCommand, // KEEP (used in main)
detectClaudeCode, // DELETE (only in dead code)
findCursorHooksDir, // DELETE (only in dead code)
installCursorHooks, // DELETE (only in dead code)
configureCursorMcp // DELETE (only in dead code)
} from './integrations/CursorHooksInstaller.js';
```
**Verification:**
```bash
grep "detectClaudeCode\|findCursorHooksDir\|installCursorHooks\|configureCursorMcp" src/services/worker-service.ts
# Expected: Only import line (which gets trimmed)
```
---
## Phase 2: Fix Fallback Agent Oversight (HIGH PRIORITY)
### 2.1 Add SDKAgent Availability Check
**Problem:** Lines 144-146 set Claude SDK as fallback without verifying it's configured
```typescript
this.geminiAgent.setFallbackAgent(this.sdkAgent);
this.openRouterAgent.setFallbackAgent(this.sdkAgent);
```
**Risk:** User chooses Gemini because they lack Claude credentials → transient Gemini error → fallback to Claude SDK → cascading failure
**Solution Options:**
**Option A: Add isConfigured() method to SDKAgent**
1. Add method to SDKAgent that checks for valid Claude SDK credentials
2. Only set fallback if `sdkAgent.isConfigured()` returns true
3. Log warning when fallback unavailable
**Pattern to follow (from SDKAgent.ts constructor):**
```typescript
// Check if Claude SDK can be initialized
public isConfigured(): boolean {
// Claude SDK uses subprocess, check if claude command exists
try {
// Check for ANTHROPIC_API_KEY or claude CLI availability
return !!process.env.ANTHROPIC_API_KEY || this.checkClaudeCliAvailable();
} catch {
return false;
}
}
```
**Option B: Document limitation (minimal fix)**
Add comment explaining the risk:
```typescript
// NOTE: Fallback to Claude SDK may fail if user lacks Claude credentials
// Consider adding availability check in future (Issue #XXX)
this.geminiAgent.setFallbackAgent(this.sdkAgent);
```
**Recommended: Option A**
**Steps:**
1. Read SDKAgent.ts to understand initialization pattern
2. Add `isConfigured()` method that checks Claude CLI/credentials
3. Update worker-service.ts to conditionally set fallback
4. Add warning log when fallback unavailable
5. Run tests
**Verification:**
```bash
grep -n "isConfigured" src/services/worker/SDKAgent.ts
# Expected: Method definition
grep -n "setFallbackAgent" src/services/worker-service.ts
# Expected: Conditional calls with isConfigured check
npm test
```
---
## Phase 3: Remove Unnecessary Re-Export (LOW PRIORITY)
### 3.1 Fix Misleading Re-Export
**Current (worker-service.ts:77-78):**
```typescript
// Re-export updateCursorContextForProject for SDK agents
export { updateCursorContextForProject };
```
**Issue:** Comment implies avoiding circular import, but investigation found NO circular dependency exists.
**Import chain:**
```
CursorHooksInstaller.ts (defines) → worker-service.ts (imports, re-exports) → ResponseProcessor.ts (imports)
```
**ResponseProcessor.ts could import directly from CursorHooksInstaller.ts**
**Options:**
1. **Remove re-export entirely** - Update ResponseProcessor.ts to import from CursorHooksInstaller directly
2. **Fix comment** - Update to reflect actual reason (API surface simplification)
**Recommended: Option 1 (cleaner)**
**Steps:**
1. Update `src/services/worker/agents/ResponseProcessor.ts`:
- Change: `import { updateCursorContextForProject } from '../../worker-service.js';`
- To: `import { updateCursorContextForProject } from '../../integrations/CursorHooksInstaller.js';`
2. Delete re-export from worker-service.ts (lines 77-78)
3. Run build to verify
**Verification:**
```bash
grep -n "export { updateCursorContextForProject" src/services/worker-service.ts
# Expected: No output
grep -n "updateCursorContextForProject" src/services/worker/agents/ResponseProcessor.ts
# Expected: Import from CursorHooksInstaller
npm run build
```
---
## Phase 4: Update MCP Version (LOW PRIORITY)
### 4.1 Use Package Version for MCP Client
**Current (worker-service.ts:157-160):**
```typescript
this.mcpClient = new Client({
name: 'worker-search-proxy',
version: '1.0.0' // Hardcoded, should match package.json (9.0.4)
}, { capabilities: {} });
```
**Also affects (from report):**
- `src/services/sync/ChromaSync.ts:126-131`
- MCP server (separate file)
**Pattern to follow:**
```typescript
import { version } from '../../package.json' assert { type: 'json' };
this.mcpClient = new Client({
name: 'worker-search-proxy',
version: version
}, { capabilities: {} });
```
**Alternative (if JSON import not supported):**
```typescript
import { readFileSync } from 'fs';
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf-8'));
this.mcpClient = new Client({
name: 'worker-search-proxy',
version: pkg.version
}, { capabilities: {} });
```
**Steps:**
1. Check if JSON import assertion works in project
2. Update worker-service.ts MCP client initialization
3. Update ChromaSync.ts similarly
4. Run build to verify
**Verification:**
```bash
grep -n "version: '1.0.0'" src/services/worker-service.ts src/services/sync/ChromaSync.ts
# Expected: No output
npm run build
```
### 4.2 Add MCP Capabilities Comment
**Current:**
```typescript
}, { capabilities: {} });
```
**Add clarifying comment:**
```typescript
}, {
// MCP spec: Clients accept all server capabilities; no declaration needed
capabilities: {}
});
```
---
## Phase 5: Verification
### 5.1 Build Check
```bash
npm run build
```
**Expected:** No TypeScript errors
### 5.2 Test Suite
```bash
npm test
```
**Expected:** All tests pass
### 5.3 Grep for Anti-Patterns
```bash
# Verify dead code removed
grep -r "runInteractiveSetup" src/
# Expected: No matches
# Verify unused imports removed
grep "import \* as fs from 'fs'" src/services/worker-service.ts
# Expected: No match
# Verify re-export removed
grep "export { updateCursorContextForProject" src/services/worker-service.ts
# Expected: No match
# Verify fallback has check
grep -A2 "setFallbackAgent" src/services/worker-service.ts
# Expected: Conditional with isConfigured check
```
### 5.4 Runtime Check
```bash
npm run build-and-sync
# Manually verify worker starts and basic operations work
```
---
## Summary
| Phase | Description | Lines Changed | Priority |
|-------|-------------|---------------|----------|
| Phase 1 | Delete dead code + imports | ~200 deleted | HIGH |
| Phase 2 | Add fallback verification | ~10 added | HIGH |
| Phase 3 | Remove re-export | ~5 changed | LOW |
| Phase 4 | Update MCP version | ~3 changed | LOW |
| Phase 5 | Verification | N/A | N/A |
**Execution Order:** Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5
**Note:** Each phase should be followed by verification (build + test) before proceeding.
---
## Patterns Confirmed KEEP (No Action)
These patterns were validated as intentional:
1. **Exit code 0 always** - Windows Terminal tab accumulation workaround (commit 222a73da)
2. **`as Error` casts** - Documented project policy with anti-pattern detection
3. **Dual init tracking** - Promise for async, flag for sync callers
4. **Signal handler ref pattern** - Standard JS mutable state sharing
5. **Empty MCP capabilities** - Correct per MCP client spec
-144
View File
@@ -1,144 +0,0 @@
# Plan: Address PR #610 Review Issues
## Overview
This plan addresses the issues identified in the PR review for PR #610 "fix: Update hooks for Claude Code 2.1.0/1 - SessionStart no longer shows user messages".
## Phase 0: Verification and Discovery
### 0.1 Verify Test Failure
- **File**: `tests/hook-constants.test.ts`
- **Issue**: Lines 61-63 test for `HOOK_EXIT_CODES.USER_MESSAGE_ONLY` which was removed
- **Verification**: Run `bun test tests/hook-constants.test.ts` to confirm failure
### 0.2 Verify No Code References USER_MESSAGE_ONLY
- **Finding**: Grep found references only in:
- `tests/hook-constants.test.ts` (test file - needs fix)
- `src/services/CLAUDE.md` (memory context - auto-generated, not code)
- `plugin/scripts/CLAUDE.md` (memory context - auto-generated, not code)
- **Conclusion**: Only the test file needs updating; CLAUDE.md files are memory records
### 0.3 Verify CLAUDE.md Files Are Legitimate
- **Clarification**: The PR reviewer mentioned "user-specific CLAUDE.md files starting with ~/"
- **Finding**: All CLAUDE.md files in the commit are within the repository (`docs/`, `src/`, `plugin/`)
- **Conclusion**: These are legitimate in-repo context files, not user-specific paths
---
## Phase 1: Fix Test File (REQUIRED)
### Task 1.1: Remove USER_MESSAGE_ONLY Test
**File**: `tests/hook-constants.test.ts`
**Action**: Delete lines 61-63 that test for the removed constant
```typescript
// DELETE THESE LINES:
it('should define USER_MESSAGE_ONLY exit code', () => {
expect(HOOK_EXIT_CODES.USER_MESSAGE_ONLY).toBe(3);
});
```
### Task 1.2: Add Test for BLOCKING_ERROR
**File**: `tests/hook-constants.test.ts`
**Action**: Add test for the new `BLOCKING_ERROR` constant (exit code 2) that replaced it
```typescript
// ADD THIS TEST:
it('should define BLOCKING_ERROR exit code', () => {
expect(HOOK_EXIT_CODES.BLOCKING_ERROR).toBe(2);
});
```
### Verification
- Run `bun test tests/hook-constants.test.ts`
- Expect: All tests pass
---
## Phase 2: Documentation Consistency (NICE TO HAVE)
### Issue
Three similar notes about Claude Code 2.1.0 have slightly different wording:
1. `docs/public/architecture/hooks.mdx:254`:
> "SessionStart hooks no longer display any user-visible messages. Context is still injected via `hookSpecificOutput.additionalContext` but users don't see startup output in the UI."
2. `docs/public/hooks-architecture.mdx:31`:
> "SessionStart hooks no longer display any user-visible messages. Context is silently injected via `hookSpecificOutput.additionalContext`."
3. `docs/public/hooks-architecture.mdx:441`:
> "SessionStart hooks output is never displayed to users. Context is injected silently via `hookSpecificOutput.additionalContext`."
### Task 2.1: Standardize Note Wording
**Action**: Use consistent wording across all three locations
**Standard text**:
```
As of Claude Code 2.1.0 (ultrathink update), SessionStart hooks no longer display user-visible messages. Context is silently injected via `hookSpecificOutput.additionalContext`.
```
### Files to Update
1. `docs/public/architecture/hooks.mdx:253-255` - Update Note block
2. `docs/public/hooks-architecture.mdx:30-32` - Update Note block
3. `docs/public/hooks-architecture.mdx:440-442` - Update Note block
### Verification
- Grep for the standard text in all three files
- Visual review of documentation
---
## Phase 3: Code Quality Improvements (OPTIONAL)
### Issue 3.1: Hardcoded Promotional Message
**File**: `src/hooks/context-hook.ts:66-68`
**Current code**:
```typescript
const enhancedContext = `${text}
Access 300k tokens of past research & decisions for just 19,008t. Use MCP search tools to access memories by ID.`;
```
### Options
1. **Leave as-is**: The token count is a rough estimate and doesn't need to be exact
2. **Make configurable**: Add to settings (over-engineering for this use case)
3. **Remove hardcoded numbers**: Use relative language instead
### Recommendation
Leave as-is for now. The token counts are marketing copy, not critical functionality. Creating a PR just for this adds unnecessary complexity.
---
## Phase 4: Final Verification
### 4.1 Run Full Test Suite
```bash
bun test
```
### 4.2 Build Verification
```bash
npm run build
```
### 4.3 Grep Verification
```bash
grep -r "USER_MESSAGE_ONLY" src/ --include="*.ts" --include="*.js"
```
Expected: No results (CLAUDE.md files excluded as they're memory records)
---
## Summary
| Phase | Priority | Effort | Description |
|-------|----------|--------|-------------|
| 1 | REQUIRED | 5 min | Fix test file - remove USER_MESSAGE_ONLY test, add BLOCKING_ERROR test |
| 2 | Nice to have | 10 min | Standardize documentation note wording |
| 3 | Skip | - | Hardcoded token counts are fine as-is |
| 4 | REQUIRED | 5 min | Run tests and build to verify |
## Expected Outcome
- All tests pass
- Build succeeds
- No code references to removed USER_MESSAGE_ONLY constant
- Documentation uses consistent wording (if Phase 2 is done)
-223
View File
@@ -1,223 +0,0 @@
# Plan: PR #628 Polish Items
**PR**: #628 - Windows Terminal Tab Accumulation & Windows 11 Compatibility
**Status**: APPROVED by 3 reviewers with minor suggestions
**Branch**: `feature/no-more-hook-files`
---
## Phase 0: Documentation Discovery (Completed by Orchestrator)
### Allowed APIs and Patterns
**Exit Code Constants** - `src/shared/hook-constants.ts:18-23`:
```typescript
export const HOOK_EXIT_CODES = {
SUCCESS: 0,
FAILURE: 1,
BLOCKING_ERROR: 2,
} as const;
```
**Timeout Constants** - `src/shared/hook-constants.ts:1-8`:
```typescript
export const HOOK_TIMEOUTS = {
DEFAULT: 300000,
HEALTH_CHECK: 30000,
WORKER_STARTUP_WAIT: 1000,
WORKER_STARTUP_RETRIES: 300,
PRE_RESTART_SETTLE_DELAY: 2000,
WINDOWS_MULTIPLIER: 1.5
} as const;
```
**Platform Timeout Function** - `src/services/infrastructure/ProcessManager.ts:70-73`:
```typescript
export function getPlatformTimeout(baseMs: number): number {
const WINDOWS_MULTIPLIER = 2.0;
return process.platform === 'win32' ? Math.round(baseMs * WINDOWS_MULTIPLIER) : baseMs;
}
```
**Migration Guide Pattern** - `docs/public/architecture/pm2-to-bun-migration.mdx`:
- Uses MDX format with frontmatter
- Starts with `<Note>` for historical context
- Uses `<AccordionGroup>` for before/after comparisons
- Includes executive summary, key benefits, migration impact sections
**Exit Code Documentation** - `private/context/claude-code/exit-codes.md`:
- Defines exit code 0, 2, and other behaviors
- Per-hook event behavior table
### Files to Modify
| File | Change | Lines |
|------|--------|-------|
| `src/services/infrastructure/ProcessManager.ts` | Add POWERSHELL_TIMEOUT constant, reduce from 60000 to 10000 | 93, 123, 175, 241 |
| `src/shared/hook-constants.ts` | Add POWERSHELL_TIMEOUT constant | After line 8 |
| `CLAUDE.md` | Document exit code strategy | Architecture section |
### Anti-Patterns to Avoid
- DO NOT invent new exit code values (only 0, 1, 2 exist)
- DO NOT change Windows multiplier (1.5x in hooks, 2.0x in ProcessManager - they serve different purposes)
- DO NOT add upper bound PID validation (not in existing pattern, reviewers marked as "nice to have")
- DO NOT create migration guide for Cursor (shell scripts still exist in cursor-hooks/, not removed)
---
## Phase 1: Extract PowerShell Timeout Constant
### What to Implement
Add a `POWERSHELL_TIMEOUT` constant to centralize the magic number `60000` and reduce to `10000` (10 seconds) as recommended by reviewers.
### Documentation References
1. Copy constant pattern from `src/shared/hook-constants.ts:1-8`
2. Copy usage pattern from `src/services/infrastructure/ProcessManager.ts:93`
### Implementation Steps
1. **Add constant to hook-constants.ts** after line 8:
```typescript
POWERSHELL_COMMAND: 10000, // PowerShell process enumeration (10s - typically completes in <1s)
```
2. **Import and use in ProcessManager.ts**:
- Import `HOOK_TIMEOUTS` from `../../shared/hook-constants.js`
- Replace `{ timeout: 60000 }` with `{ timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND }` at lines 93, 123, 175, 241
### Verification Checklist
- [ ] `grep -n "60000" src/services/infrastructure/ProcessManager.ts` returns 0 matches
- [ ] `grep -n "POWERSHELL_COMMAND" src/services/infrastructure/ProcessManager.ts` returns 4 matches
- [ ] `npm run build` succeeds
- [ ] `npm test` passes (22/22 PowerShell tests still pass)
### Anti-Pattern Guards
- DO NOT use `getPlatformTimeout()` for PowerShell commands (they already run only on Windows)
- DO NOT change timeout values in other files (only ProcessManager.ts uses PowerShell)
---
## Phase 2: Document Exit Code Strategy in CLAUDE.md
### What to Implement
Add an "Exit Code Strategy" section to the main CLAUDE.md to explain the graceful exit philosophy adopted in this PR.
### Documentation References
1. Copy exit code definitions from `private/context/claude-code/exit-codes.md`
2. Follow format of existing CLAUDE.md sections
### Implementation Steps
1. **Add section after "File Locations"** in `/Users/alexnewman/Scripts/claude-mem/CLAUDE.md`:
```markdown
## Exit Code Strategy
Claude-mem hooks use specific exit codes per Claude Code's hook contract:
- **Exit 0**: Success or graceful shutdown (Windows Terminal closes tabs)
- **Exit 1**: Non-blocking error (stderr shown to user, continues)
- **Exit 2**: Blocking error (stderr fed to Claude for processing)
**Philosophy**: Worker/hook errors exit with code 0 to prevent Windows Terminal tab accumulation. The wrapper/plugin layer handles restart logic. ERROR-level logging is maintained for diagnostics.
See `private/context/claude-code/exit-codes.md` for full hook behavior matrix.
```
### Verification Checklist
- [ ] `grep -n "Exit Code Strategy" CLAUDE.md` returns 1 match
- [ ] Section appears after "File Locations" section
- [ ] No duplicate sections added
### Anti-Pattern Guards
- DO NOT copy the full exit-codes.md table (keep it brief, reference the source)
- DO NOT change actual exit code behavior in code files
---
## Phase 3: Update Tests for New Timeout Constant
### What to Implement
Add test coverage for the new `POWERSHELL_COMMAND` timeout constant.
### Documentation References
1. Copy test pattern from `tests/hook-constants.test.ts:26-48`
### Implementation Steps
1. **Add test to hook-constants.test.ts** after line 42:
```typescript
test('POWERSHELL_COMMAND timeout is 10000ms', () => {
expect(HOOK_TIMEOUTS.POWERSHELL_COMMAND).toBe(10000);
});
```
### Verification Checklist
- [ ] `npm test -- tests/hook-constants.test.ts` passes
- [ ] New test appears in test output
- [ ] All 22 PowerShell parsing tests still pass
### Anti-Pattern Guards
- DO NOT modify PowerShell parsing tests (they test parsing, not timeouts)
- DO NOT add integration tests for actual PowerShell execution (out of scope)
---
## Phase 4: Final Verification
### Verification Checklist
1. **Build passes**: `npm run build`
2. **All tests pass**: `npm test`
3. **No magic numbers remain**: `grep -rn "60000" src/services/infrastructure/ProcessManager.ts` returns 0
4. **Exit code documentation exists**: `grep -n "Exit Code Strategy" CLAUDE.md` returns 1
5. **Constant is used**: `grep -rn "POWERSHELL_COMMAND" src/` returns multiple matches
### Anti-Pattern Grep Checks
- [ ] `grep -rn "timeout: 60000" src/` returns 0 matches (no hardcoded 60s timeouts in ProcessManager)
- [ ] `grep -rn "process.exit(3)" src/` returns 0 matches (exit code 3 not used)
### Commit Message Template
```
polish: extract PowerShell timeout constant and document exit code strategy
- Extract magic number 60000ms to HOOK_TIMEOUTS.POWERSHELL_COMMAND (10000ms)
- Reduce PowerShell timeout from 60s to 10s per review feedback
- Document exit code strategy in CLAUDE.md
- Add test coverage for new constant
Addresses review feedback from PR #628
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
```
---
## Summary
| Phase | Description | Files Changed | Verification |
|-------|-------------|---------------|--------------|
| 0 | Documentation Discovery | N/A | Patterns identified |
| 1 | Extract PowerShell timeout | hook-constants.ts, ProcessManager.ts | grep + build + test |
| 2 | Document exit strategy | CLAUDE.md | grep |
| 3 | Add test coverage | hook-constants.test.ts | npm test |
| 4 | Final verification | N/A | All checks pass |
**Estimated Changes**: ~20 lines added/modified across 4 files
**Risk Level**: Low (constants extraction, documentation only)
**Breaking Changes**: None
-394
View File
@@ -1,394 +0,0 @@
# Plan: Remove Worker Start Calls - In-Process Architecture
## Problem Statement
Current architecture has problematic spawn patterns:
1. `hooks.json` calls `worker-service.cjs start` which spawns a daemon
2. Spawning is buggy on Windows - **HARD RULE: NO SPAWN**
3. `user-message` hook is deprecated
4. `smart-install` was supposed to chain: `smart-install && stop && context`
## Target Architecture
**NO SPAWN - Worker runs in-process within hook command**
```
SessionStart:
smart-install && stop && context
```
Flow:
1. `smart-install` - Install dependencies if needed
2. `stop` - Kill any existing worker (clean slate)
3. `context` - Hook starts worker IN-PROCESS, becomes the worker
**Key insight:** The first hook that needs the worker **becomes** the worker. No spawn, no daemon. The hook process IS the worker process.
---
## Current vs Target hooks.json
### Current (BROKEN)
```json
"SessionStart": [
{ "hooks": [
{ "command": "node smart-install.js" },
{ "command": "bun worker-service.cjs start" }, // REMOVE - spawn
{ "command": "bun worker-service.cjs hook ... context" },
{ "command": "bun worker-service.cjs hook ... user-message" } // REMOVE - deprecated
]}
]
```
### Target
```json
"SessionStart": [
{ "hooks": [
{ "command": "node smart-install.js && bun worker-service.cjs stop && bun worker-service.cjs hook claude-code context" }
]}
]
```
---
## Files Involved
| File | Changes |
|------|---------|
| `plugin/hooks/hooks.json` | Restructure to chained commands, remove start/user-message |
| `src/services/worker-service.ts` | `hook` case: start worker in-process if not running |
| `src/cli/handlers/*.ts` | May need adjustment for in-process execution |
| `src/shared/worker-utils.ts` | `ensureWorkerRunning()` → adapt for in-process |
---
## Phase 0: Documentation Discovery
### Available APIs
**From `src/services/infrastructure/HealthMonitor.ts`:**
- `isPortInUse(port): Promise<boolean>`
- `waitForHealth(port, timeoutMs): Promise<boolean>`
- `httpShutdown(port): Promise<void>`
**From `src/services/worker-service.ts`:**
- `WorkerService` class - the actual worker
- `stop` command - shuts down worker via HTTP
- `--daemon` case - starts WorkerService (currently only used after spawn)
**BANNED (spawn patterns):**
- ~~`spawnDaemon()`~~ - NO SPAWN
- ~~`fork()`~~ - NO SPAWN
- ~~`spawn()` with detached~~ - NO SPAWN
### Anti-Patterns
- **NO SPAWN** - Hard rule, Windows buggy
- No `restart` command - removed for same reason
- No detached processes
---
## Phase 1: Modify `hook` Case for In-Process Worker
### Location
`src/services/worker-service.ts:564-576`
### Current Code
```typescript
case 'hook': {
const platform = process.argv[3];
const event = process.argv[4];
if (!platform || !event) {
console.error('Usage: claude-mem hook <platform> <event>');
process.exit(1);
}
const { hookCommand } = await import('../cli/hook-command.js');
await hookCommand(platform, event);
break;
}
```
### Target Code
```typescript
case 'hook': {
const platform = process.argv[3];
const event = process.argv[4];
if (!platform || !event) {
console.error('Usage: claude-mem hook <platform> <event>');
process.exit(1);
}
// Check if worker already running (port in use = valid, another process has it)
const portInUse = await isPortInUse(port);
if (portInUse) {
// Port in use - either healthy worker or something else
// Proceed with hook via HTTP to existing worker
const { hookCommand } = await import('../cli/hook-command.js');
await hookCommand(platform, event);
break;
}
// Port free - start worker IN THIS PROCESS (no spawn!)
logger.info('SYSTEM', 'Starting worker in-process for hook');
const worker = new WorkerService();
// Start worker (non-blocking, returns when server listening)
await worker.start();
// Now execute hook logic - worker is running in this process
// Can call handler directly (in-process) or via HTTP to self
const { hookCommand } = await import('../cli/hook-command.js');
await hookCommand(platform, event);
// DON'T exit - this process IS the worker now
// Worker stays alive serving requests
break;
}
```
### Key Behavior
- If port in use → hook runs via HTTP to existing worker, then exits
- If port free → start worker in-process, run hook, process stays alive as worker
### Verification
- [ ] Stop worker, run hook command → should start worker and stay alive
- [ ] Worker already running, run hook command → should complete and exit
- [ ] `lsof -i :37777` shows hook process IS the worker
---
## Phase 2: Update hooks.json - Chained Commands
### Location
`plugin/hooks/hooks.json`
### Target Structure
```json
{
"description": "Claude-mem memory system hooks",
"hooks": {
"SessionStart": [
{
"matcher": "startup|clear|compact",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js\" && bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" stop && bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code context",
"timeout": 300
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code session-init",
"timeout": 60
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code observation",
"timeout": 120
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code summarize",
"timeout": 120
}
]
}
]
}
}
```
### Changes Summary
1. SessionStart: Chain `smart-install && stop && context` in single command
2. Remove `user-message` hook (deprecated)
3. Remove all separate `start` commands
4. Other hooks unchanged (just hook command, auto-starts if needed)
### Verification
- [ ] JSON valid: `cat plugin/hooks/hooks.json | jq .`
- [ ] No `start` command: `grep -c '"start"' plugin/hooks/hooks.json` = 0
- [ ] No `user-message`: `grep -c 'user-message' plugin/hooks/hooks.json` = 0
---
## Phase 3: Handle "Port In Use" Gracefully
### Scenario
Another process has port 37777 (not our worker). Hook should handle gracefully.
### Current Behavior
`ensureWorkerRunning()` polls for 15 seconds, then throws error.
### Target Behavior
If port in use but not healthy (not our worker):
- Hook is "valid" - don't block Claude Code
- Return graceful response (empty context, etc.)
- Log warning for debugging
### Location
`src/shared/worker-utils.ts:117-141`
### Changes
```typescript
export async function ensureWorkerRunning(): Promise<boolean> {
const port = getWorkerPort();
// Quick health check (2 seconds max)
try {
if (await isWorkerHealthy()) {
await checkWorkerVersion();
return true; // Worker healthy
}
} catch (e) {
// Not healthy
}
// Port might be in use by something else
// Return false but don't throw - let caller decide
logger.warn('SYSTEM', 'Worker not healthy, hook will proceed gracefully');
return false;
}
```
### Handler Updates
Update handlers to handle `ensureWorkerRunning()` returning false:
```typescript
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
// Return graceful empty response
return { output: '', exitCode: HOOK_EXIT_CODES.SUCCESS };
}
```
### Verification
- [ ] Start non-worker process on 37777, run hook → completes gracefully
- [ ] No 15-second hang when port blocked
---
## Phase 4: Remove Deprecated Code
### Remove `user-message` Handler (if unused elsewhere)
- [ ] Check if `user-message.ts` is used anywhere else
- [ ] Remove from `src/cli/handlers/index.ts` if safe
- [ ] Consider keeping file but removing from hooks.json only
### Remove `start` Command (optional)
The `start` command in worker-service.ts can stay for manual use:
```bash
bun worker-service.cjs start # Manual start if needed
```
But it should NOT be called from hooks.json.
### Verification
- [ ] `npm run build` succeeds
- [ ] No references to removed handlers in hooks.json
---
## Phase 5: Update Handler `ensureWorkerRunning()` Calls
### Context
Each handler currently calls `ensureWorkerRunning()` which polls for 15 seconds.
With in-process architecture:
- If hook started worker in-process → worker is THIS process, no HTTP needed
- If worker already running → HTTP to existing worker
### Decision
**Keep handler calls** but modify `ensureWorkerRunning()` to:
1. Return quickly if port is in use (assume valid)
2. Return true if in-process worker (detect via global flag?)
3. Graceful false return instead of throwing
### Files
- `src/cli/handlers/context.ts:15`
- `src/cli/handlers/session-init.ts:15`
- `src/cli/handlers/observation.ts:14`
- `src/cli/handlers/summarize.ts:17`
- `src/cli/handlers/file-edit.ts:15`
### Verification
- [ ] Handlers don't hang on port-in-use scenarios
- [ ] In-process worker scenario works
---
## Phase 6: Final Verification
### Tests
- [ ] `bun test` - All tests pass
- [ ] `npm run build-and-sync` - Build succeeds
### Manual Tests
**Test 1: Clean Start**
```bash
bun plugin/scripts/worker-service.cjs stop
# Start new Claude Code session
# Verify: context hook starts worker in-process
# Verify: lsof -i :37777 shows the hook process
```
**Test 2: Worker Already Running**
```bash
bun plugin/scripts/worker-service.cjs stop
bun plugin/scripts/worker-service.cjs hook claude-code context &
# Wait for worker to start
bun plugin/scripts/worker-service.cjs hook claude-code observation
# Verify: observation hook exits after completing (doesn't stay alive)
```
**Test 3: Port Blocked**
```bash
bun plugin/scripts/worker-service.cjs stop
nc -l 37777 & # Block port with netcat
bun plugin/scripts/worker-service.cjs hook claude-code context
# Verify: completes gracefully, doesn't hang
kill %1 # Clean up netcat
```
**Test 4: Full Session**
```bash
# Start fresh Claude Code session
# Do some work (creates observations)
# End session (Ctrl+C or /exit)
# Verify: summarize hook ran, observations saved
```
---
## Risk Assessment
| Risk | Mitigation |
|------|------------|
| Hook stays alive forever | Expected - it's the worker now |
| Multiple hooks compete for port | First one wins, others use HTTP |
| Graceful shutdown on session end | Stop command in chain handles this |
| Windows compatibility | No spawn = no Windows issues |
## Rollback Plan
If issues arise:
1. Restore hooks.json with separate start commands
2. Revert worker-service.ts hook case changes
3. No database changes to rollback
@@ -1,196 +0,0 @@
# Plan: Integrate Workflow Agents and Commands into Claude-Mem
## Executive Summary
This plan integrates the `/make-plan` and `/do` orchestration workflow from `~/.claude/commands/` into the claude-mem plugin as project-level development tools.
## Dependency Analysis
### Commands to Copy (from `~/.claude/commands/`)
| File | Purpose | Dependencies |
|------|---------|--------------|
| `make-plan.md` | Orchestrator for LLM-friendly phased planning | Uses Task tool with subagents |
| `do.md` | Orchestrator for executing plans via subagents | Uses Task tool with subagents |
| `anti-pattern-czar.md` | Error handling anti-pattern detection/fixing | Uses Read, Edit, Bash tools |
### Specialized Agents Referenced
The `/make-plan` and `/do` commands reference these **conceptual agent roles** (not actual agent files):
| Agent Role | Referenced In | Description |
|------------|---------------|-------------|
| "Documentation Discovery" | make-plan.md | Fact-gathering from docs/examples |
| "Verification" | make-plan.md, do.md | Verify implementation matches plan |
| "Implementation" | do.md | Execute implementation tasks |
| "Anti-pattern" | do.md | Grep for known bad patterns |
| "Code Quality" | do.md | Review code changes |
| "Commit" | do.md | Commit after verification passes |
| "Branch/Sync" | do.md | Push and prepare phase handoffs |
**Key Finding**: These are **role descriptions**, not separate agent files. The Task tool's `general-purpose` subagent_type executes all roles. The commands define *what* each role should do, not separate agent implementations.
### Existing Project Assets
Located in `.claude/`:
- `agents/github-morning-reporter.md` - Already in project
- `skills/version-bump/SKILL.md` - Already in project
- No existing commands directory
---
## Phase 0: Documentation Discovery (Complete)
### Sources Consulted
1. `/Users/alexnewman/.claude/commands/make-plan.md` (62 lines)
2. `/Users/alexnewman/.claude/commands/do.md` (39 lines)
3. `/Users/alexnewman/.claude/commands/anti-pattern-czar.md` (122 lines)
4. `/Users/alexnewman/.claude/settings.json` (36 lines)
5. `.claude/skills/CLAUDE.md` (30 lines)
6. `.claude/agents/github-morning-reporter.md` (102 lines)
### Allowed APIs/Patterns
- **Commands**: `.claude/commands/*.md` files with `#$ARGUMENTS` placeholder for user input
- **Skills**: `.claude/skills/<name>/SKILL.md` with YAML frontmatter (name, description)
- **Agents**: `.claude/agents/*.md` with YAML frontmatter (name, description, model)
### Anti-Patterns to Avoid
- Skills require YAML frontmatter; commands do not
- Commands use `#$ARGUMENTS` for input; skills/agents receive prompts differently
- Don't create separate agent files for role descriptions - the Task tool handles routing
---
## Phase 1: Create Commands Directory
### What to Implement
1. Create `.claude/commands/` directory
2. Copy `make-plan.md` from `~/.claude/commands/make-plan.md`
3. Copy `do.md` from `~/.claude/commands/do.md`
4. Copy `anti-pattern-czar.md` from `~/.claude/commands/anti-pattern-czar.md`
### Documentation References
- Pattern: `~/.claude/commands/*.md` (source files)
- Existing example: `.claude/skills/version-bump/SKILL.md` for claude-mem project tools
### Verification Checklist
```bash
# Verify files exist
ls -la .claude/commands/
# Verify content matches source
diff ~/.claude/commands/make-plan.md .claude/commands/make-plan.md
diff ~/.claude/commands/do.md .claude/commands/do.md
diff ~/.claude/commands/anti-pattern-czar.md .claude/commands/anti-pattern-czar.md
# Verify #$ARGUMENTS placeholder exists
grep '\$ARGUMENTS' .claude/commands/*.md
```
### Anti-Pattern Guards
- Do NOT add YAML frontmatter to commands (they don't need it)
- Do NOT modify the source content (copy verbatim)
---
## Phase 2: Create CLAUDE.md Documentation
### What to Implement
Create `.claude/commands/CLAUDE.md` documenting the commands directory (following pattern from `.claude/skills/CLAUDE.md`)
### Content Template
```markdown
# Project-Level Commands
This directory contains slash commands **for developing and maintaining the claude-mem project itself**.
## Commands in This Directory
### /make-plan
Orchestrator for creating LLM-friendly implementation plans in phases. Deploys subagents for documentation discovery and fact gathering.
**Usage**: `/make-plan <task description>`
### /do
Orchestrator for executing plans via subagents. Deploys specialized subagents for implementation, verification, and code quality review.
**Usage**: `/do <plan-file-path or inline plan>`
### /anti-pattern-czar
Interactive workflow for detecting and fixing error handling anti-patterns using the automated scanner.
**Usage**: `/anti-pattern-czar`
## Adding New Commands
Commands are markdown files with `#$ARGUMENTS` placeholder for user input.
```
### Verification Checklist
```bash
# Verify file exists
cat .claude/commands/CLAUDE.md
```
---
## Phase 3: Update Settings (if needed)
### What to Implement
Check if `.claude/settings.json` needs any permission updates for the new commands.
### Verification Checklist
```bash
# Check current settings
cat .claude/settings.json
# Verify commands work by listing them
# (After Claude Code restart, commands should appear in slash-command list)
```
### Anti-Pattern Guards
- Do NOT add skill permissions for commands (they're different)
- Commands don't require explicit permissions
---
## Phase 4: Final Verification
### Verification Checklist
1. All three command files exist in `.claude/commands/`
2. Content matches source files exactly (byte-for-byte if possible)
3. CLAUDE.md documentation exists
4. Git status shows new files ready for commit
```bash
# Full verification
ls -la .claude/commands/
wc -l .claude/commands/*.md
git status
```
### Commit Message Template
```
feat: add /make-plan, /do, and /anti-pattern-czar workflow commands
Add project-level orchestration commands for claude-mem development:
- /make-plan: Create LLM-friendly implementation plans in phases
- /do: Execute plans via coordinated subagents
- /anti-pattern-czar: Detect and fix error handling anti-patterns
These commands enable structured, agent-driven development workflows.
```
---
## Summary
**Files to Create**:
1. `.claude/commands/make-plan.md` (copy from ~/.claude/commands/)
2. `.claude/commands/do.md` (copy from ~/.claude/commands/)
3. `.claude/commands/anti-pattern-czar.md` (copy from ~/.claude/commands/)
4. `.claude/commands/CLAUDE.md` (new documentation)
**No Agent Files Needed**: The "agents" referenced in make-plan.md and do.md are role descriptions, not separate files. The Task tool's built-in subagent types handle execution.
**Confidence**: High - analysis complete with full source file reads.
Submodule .claude/worktrees/amazing-matsumoto deleted from 8e4d109db0
Submodule .claude/worktrees/animated-installer deleted from 1b68c55763
Submodule .claude/worktrees/hardcore-black deleted from c27314f896
Submodule .claude/worktrees/kind-noyce deleted from 1b68c55763
+2
View File
@@ -11,6 +11,8 @@ dist/
.claude/settings.local.json
.claude/agents/
.claude/skills/
.claude/plans/
.claude/worktrees/
plugin/data/
plugin/data.backup/
package-lock.json