diff --git a/.cursor/commands/orchestrate.md b/.cursor/commands/orchestrate.md index 30ac2b8..3a629ec 100644 --- a/.cursor/commands/orchestrate.md +++ b/.cursor/commands/orchestrate.md @@ -17,7 +17,7 @@ planner -> tdd-guide -> code-reviewer -> security-reviewer ### bugfix Bug investigation and fix workflow: ``` -explorer -> tdd-guide -> code-reviewer +planner -> tdd-guide -> code-reviewer ``` ### refactor diff --git a/.cursor/commands/plan.md b/.cursor/commands/plan.md index 3acf686..b7e9905 100644 --- a/.cursor/commands/plan.md +++ b/.cursor/commands/plan.md @@ -104,7 +104,7 @@ If you want changes, respond with: After planning: - Use `/tdd` to implement with test-driven development -- Use `/build-and-fix` if build errors occur +- Use `/build-fix` if build errors occur - Use `/code-review` to review completed implementation ## Related Agents diff --git a/.cursor/commands/python-review.md b/.cursor/commands/python-review.md index ba594b2..1d72978 100644 --- a/.cursor/commands/python-review.md +++ b/.cursor/commands/python-review.md @@ -171,7 +171,7 @@ Run: `black app/routes/user.py app/services/auth.py` ## Integration with Other Commands -- Use `/python-test` first to ensure tests pass +- Use `/tdd` first to ensure tests pass - Use `/code-review` for non-Python specific concerns - Use `/python-review` before committing - Use `/build-fix` if static analysis tools fail diff --git a/.cursor/commands/tdd.md b/.cursor/commands/tdd.md index 02bdb2d..3f7b02b 100644 --- a/.cursor/commands/tdd.md +++ b/.cursor/commands/tdd.md @@ -313,7 +313,7 @@ Never skip the RED phase. Never write code before tests. - Use `/plan` first to understand what to build - Use `/tdd` to implement with tests -- Use `/build-and-fix` if build errors occur +- Use `/build-fix` if build errors occur - Use `/code-review` to review implementation - Use `/test-coverage` to verify coverage diff --git a/commands/orchestrate.md b/commands/orchestrate.md index 30ac2b8..3a629ec 100644 --- a/commands/orchestrate.md +++ b/commands/orchestrate.md @@ -17,7 +17,7 @@ planner -> tdd-guide -> code-reviewer -> security-reviewer ### bugfix Bug investigation and fix workflow: ``` -explorer -> tdd-guide -> code-reviewer +planner -> tdd-guide -> code-reviewer ``` ### refactor diff --git a/commands/plan.md b/commands/plan.md index 3acf686..b7e9905 100644 --- a/commands/plan.md +++ b/commands/plan.md @@ -104,7 +104,7 @@ If you want changes, respond with: After planning: - Use `/tdd` to implement with test-driven development -- Use `/build-and-fix` if build errors occur +- Use `/build-fix` if build errors occur - Use `/code-review` to review completed implementation ## Related Agents diff --git a/commands/python-review.md b/commands/python-review.md index ba594b2..1d72978 100644 --- a/commands/python-review.md +++ b/commands/python-review.md @@ -171,7 +171,7 @@ Run: `black app/routes/user.py app/services/auth.py` ## Integration with Other Commands -- Use `/python-test` first to ensure tests pass +- Use `/tdd` first to ensure tests pass - Use `/code-review` for non-Python specific concerns - Use `/python-review` before committing - Use `/build-fix` if static analysis tools fail diff --git a/commands/tdd.md b/commands/tdd.md index 02bdb2d..3f7b02b 100644 --- a/commands/tdd.md +++ b/commands/tdd.md @@ -313,7 +313,7 @@ Never skip the RED phase. Never write code before tests. - Use `/plan` first to understand what to build - Use `/tdd` to implement with tests -- Use `/build-and-fix` if build errors occur +- Use `/build-fix` if build errors occur - Use `/code-review` to review implementation - Use `/test-coverage` to verify coverage diff --git a/docs/zh-CN/commands/orchestrate.md b/docs/zh-CN/commands/orchestrate.md index 21124d8..5677670 100644 --- a/docs/zh-CN/commands/orchestrate.md +++ b/docs/zh-CN/commands/orchestrate.md @@ -21,7 +21,7 @@ planner -> tdd-guide -> code-reviewer -> security-reviewer 错误调查与修复工作流: ``` -explorer -> tdd-guide -> code-reviewer +planner -> tdd-guide -> code-reviewer ``` ### refactor diff --git a/docs/zh-CN/commands/plan.md b/docs/zh-CN/commands/plan.md index 5dec96f..1734f73 100644 --- a/docs/zh-CN/commands/plan.md +++ b/docs/zh-CN/commands/plan.md @@ -107,7 +107,7 @@ Agent (planner): 计划之后: * 使用 `/tdd` 以测试驱动开发的方式实施 -* 如果出现构建错误,使用 `/build-and-fix` +* 如果出现构建错误,使用 `/build-fix` * 使用 `/code-review` 审查已完成的实施 ## 相关代理 diff --git a/docs/zh-CN/commands/python-review.md b/docs/zh-CN/commands/python-review.md index bf7b7a2..82df0db 100644 --- a/docs/zh-CN/commands/python-review.md +++ b/docs/zh-CN/commands/python-review.md @@ -189,7 +189,7 @@ with open("config.json") as f: # Good ## Integration with Other Commands -- Use `/python-test` first to ensure tests pass +- Use `/tdd` first to ensure tests pass - Use `/code-review` for non-Python specific concerns - Use `/python-review` before committing - Use `/build-fix` if static analysis tools fail diff --git a/docs/zh-CN/commands/tdd.md b/docs/zh-CN/commands/tdd.md index 548743d..8a4cf1c 100644 --- a/docs/zh-CN/commands/tdd.md +++ b/docs/zh-CN/commands/tdd.md @@ -315,7 +315,7 @@ Never skip the RED phase. Never write code before tests. - Use `/plan` first to understand what to build - Use `/tdd` to implement with tests -- Use `/build-and-fix` if build errors occur +- Use `/build-fix` if build errors occur - Use `/code-review` to review implementation - Use `/test-coverage` to verify coverage diff --git a/docs/zh-TW/commands/orchestrate.md b/docs/zh-TW/commands/orchestrate.md index 1065ee6..f4ddb9f 100644 --- a/docs/zh-TW/commands/orchestrate.md +++ b/docs/zh-TW/commands/orchestrate.md @@ -17,7 +17,7 @@ planner -> tdd-guide -> code-reviewer -> security-reviewer ### bugfix Bug 調查和修復工作流程: ``` -explorer -> tdd-guide -> code-reviewer +planner -> tdd-guide -> code-reviewer ``` ### refactor diff --git a/package.json b/package.json index a1c1937..0094081 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ecc-universal", - "version": "1.0.0", + "version": "1.4.1", "description": "Complete collection of battle-tested Claude Code configs — agents, skills, hooks, commands, and rules evolved over 10+ months of intensive daily use by an Anthropic hackathon winner", "keywords": [ "claude-code", diff --git a/scripts/hooks/session-end.js b/scripts/hooks/session-end.js index 40553e4..e4afe24 100644 --- a/scripts/hooks/session-end.js +++ b/scripts/hooks/session-end.js @@ -153,8 +153,9 @@ async function main() { if (summary) { const existing = readFile(sessionFile); if (existing && existing.includes('[Session context goes here]')) { + // Use a flexible regex that tolerates CRLF, extra whitespace, and minor template variations const updatedContent = existing.replace( - /## Current State\n\n\[Session context goes here\]\n\n### Completed\n- \[ \]\n\n### In Progress\n- \[ \]\n\n### Notes for Next Session\n-\n\n### Context to Load\n```\n\[relevant files\]\n```/, + /## Current State\s*\n\s*\[Session context goes here\][\s\S]*?### Context to Load\s*\n```\s*\n\[relevant files\]\s*\n```/, buildSummarySection(summary) ); writeFile(sessionFile, updatedContent); diff --git a/scripts/hooks/suggest-compact.js b/scripts/hooks/suggest-compact.js index 71f5da5..3cfbcef 100644 --- a/scripts/hooks/suggest-compact.js +++ b/scripts/hooks/suggest-compact.js @@ -25,7 +25,7 @@ async function main() { // Track tool call count (increment in a temp file) // Use a session-specific counter file based on session ID from environment // or parent PID as fallback - const sessionId = process.env.CLAUDE_SESSION_ID || String(process.ppid) || 'default'; + const sessionId = process.env.CLAUDE_SESSION_ID || 'default'; const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`); const rawThreshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10); const threshold = Number.isFinite(rawThreshold) && rawThreshold > 0 && rawThreshold <= 10000 diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index b830ebd..b1a488e 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -216,6 +216,96 @@ async function runTests() { fs.unlinkSync(counterFile); })) passed++; else failed++; + if (await asyncTest('does not suggest below threshold', async () => { + const sessionId = 'test-below-' + Date.now(); + const counterFile = path.join(os.tmpdir(), `claude-tool-count-${sessionId}`); + + fs.writeFileSync(counterFile, '10'); + + const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', { + CLAUDE_SESSION_ID: sessionId, + COMPACT_THRESHOLD: '50' + }); + + assert.ok( + !result.stderr.includes('tool calls'), + 'Should not suggest compact below threshold' + ); + + fs.unlinkSync(counterFile); + })) passed++; else failed++; + + if (await asyncTest('suggests at regular intervals after threshold', async () => { + const sessionId = 'test-interval-' + Date.now(); + const counterFile = path.join(os.tmpdir(), `claude-tool-count-${sessionId}`); + + // Set counter to 74 (next will be 75, which is >50 and 75%25==0) + fs.writeFileSync(counterFile, '74'); + + const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', { + CLAUDE_SESSION_ID: sessionId, + COMPACT_THRESHOLD: '50' + }); + + assert.ok( + result.stderr.includes('75 tool calls'), + 'Should suggest at 25-call intervals after threshold' + ); + + fs.unlinkSync(counterFile); + })) passed++; else failed++; + + if (await asyncTest('handles corrupted counter file', async () => { + const sessionId = 'test-corrupt-' + Date.now(); + const counterFile = path.join(os.tmpdir(), `claude-tool-count-${sessionId}`); + + fs.writeFileSync(counterFile, 'not-a-number'); + + const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', { + CLAUDE_SESSION_ID: sessionId + }); + + assert.strictEqual(result.code, 0, 'Should handle corrupted counter gracefully'); + + // Counter should be reset to 1 + const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); + assert.strictEqual(newCount, 1, 'Should reset counter to 1 on corrupt data'); + + fs.unlinkSync(counterFile); + })) passed++; else failed++; + + if (await asyncTest('uses default session ID when no env var', async () => { + const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', { + CLAUDE_SESSION_ID: '' // Empty, should use 'default' + }); + + assert.strictEqual(result.code, 0, 'Should work with default session ID'); + + // Cleanup the default counter file + const counterFile = path.join(os.tmpdir(), 'claude-tool-count-default'); + if (fs.existsSync(counterFile)) fs.unlinkSync(counterFile); + })) passed++; else failed++; + + if (await asyncTest('validates threshold bounds', async () => { + const sessionId = 'test-bounds-' + Date.now(); + const counterFile = path.join(os.tmpdir(), `claude-tool-count-${sessionId}`); + + // Invalid threshold should fall back to 50 + fs.writeFileSync(counterFile, '49'); + + const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', { + CLAUDE_SESSION_ID: sessionId, + COMPACT_THRESHOLD: '-5' // Invalid: negative + }); + + assert.ok( + result.stderr.includes('50 tool calls'), + 'Should use default threshold (50) for invalid value' + ); + + fs.unlinkSync(counterFile); + })) passed++; else failed++; + // evaluate-session.js tests console.log('\nevaluate-session.js:'); diff --git a/tests/lib/session-manager.test.js b/tests/lib/session-manager.test.js index 211575c..a4cbe9c 100644 --- a/tests/lib/session-manager.test.js +++ b/tests/lib/session-manager.test.js @@ -328,6 +328,148 @@ src/main.ts assert.strictEqual(title, 'Untitled Session'); })) passed++; else failed++; + // getAllSessions tests + console.log('\ngetAllSessions:'); + + // Override HOME to a temp dir for isolated getAllSessions/getSessionById tests + const tmpHome = path.join(os.tmpdir(), `ecc-session-mgr-test-${Date.now()}`); + const tmpSessionsDir = path.join(tmpHome, '.claude', 'sessions'); + fs.mkdirSync(tmpSessionsDir, { recursive: true }); + const origHome = process.env.HOME; + + // Create test session files with controlled modification times + const testSessions = [ + { name: '2026-01-15-abcd1234-session.tmp', content: '# Session 1' }, + { name: '2026-01-20-efgh5678-session.tmp', content: '# Session 2' }, + { name: '2026-02-01-ijkl9012-session.tmp', content: '# Session 3' }, + { name: '2026-02-01-mnop3456-session.tmp', content: '# Session 4' }, + { name: '2026-02-10-session.tmp', content: '# Old format session' }, + ]; + for (let i = 0; i < testSessions.length; i++) { + const filePath = path.join(tmpSessionsDir, testSessions[i].name); + fs.writeFileSync(filePath, testSessions[i].content); + // Stagger modification times so sort order is deterministic + const mtime = new Date(Date.now() - (testSessions.length - i) * 60000); + fs.utimesSync(filePath, mtime, mtime); + } + + process.env.HOME = tmpHome; + + if (test('getAllSessions returns all sessions', () => { + const result = sessionManager.getAllSessions({ limit: 100 }); + assert.strictEqual(result.total, 5); + assert.strictEqual(result.sessions.length, 5); + assert.strictEqual(result.hasMore, false); + })) passed++; else failed++; + + if (test('getAllSessions paginates correctly', () => { + const page1 = sessionManager.getAllSessions({ limit: 2, offset: 0 }); + assert.strictEqual(page1.sessions.length, 2); + assert.strictEqual(page1.hasMore, true); + assert.strictEqual(page1.total, 5); + + const page2 = sessionManager.getAllSessions({ limit: 2, offset: 2 }); + assert.strictEqual(page2.sessions.length, 2); + assert.strictEqual(page2.hasMore, true); + + const page3 = sessionManager.getAllSessions({ limit: 2, offset: 4 }); + assert.strictEqual(page3.sessions.length, 1); + assert.strictEqual(page3.hasMore, false); + })) passed++; else failed++; + + if (test('getAllSessions filters by date', () => { + const result = sessionManager.getAllSessions({ date: '2026-02-01', limit: 100 }); + assert.strictEqual(result.total, 2); + assert.ok(result.sessions.every(s => s.date === '2026-02-01')); + })) passed++; else failed++; + + if (test('getAllSessions filters by search (short ID)', () => { + const result = sessionManager.getAllSessions({ search: 'abcd', limit: 100 }); + assert.strictEqual(result.total, 1); + assert.strictEqual(result.sessions[0].shortId, 'abcd1234'); + })) passed++; else failed++; + + if (test('getAllSessions returns sorted by newest first', () => { + const result = sessionManager.getAllSessions({ limit: 100 }); + for (let i = 1; i < result.sessions.length; i++) { + assert.ok( + result.sessions[i - 1].modifiedTime >= result.sessions[i].modifiedTime, + 'Sessions should be sorted newest first' + ); + } + })) passed++; else failed++; + + if (test('getAllSessions handles offset beyond total', () => { + const result = sessionManager.getAllSessions({ offset: 999, limit: 10 }); + assert.strictEqual(result.sessions.length, 0); + assert.strictEqual(result.total, 5); + assert.strictEqual(result.hasMore, false); + })) passed++; else failed++; + + if (test('getAllSessions returns empty for non-existent date', () => { + const result = sessionManager.getAllSessions({ date: '2099-12-31', limit: 100 }); + assert.strictEqual(result.total, 0); + assert.strictEqual(result.sessions.length, 0); + })) passed++; else failed++; + + if (test('getAllSessions ignores non-.tmp files', () => { + fs.writeFileSync(path.join(tmpSessionsDir, 'notes.txt'), 'not a session'); + fs.writeFileSync(path.join(tmpSessionsDir, 'compaction-log.txt'), 'log'); + const result = sessionManager.getAllSessions({ limit: 100 }); + assert.strictEqual(result.total, 5, 'Should only count .tmp session files'); + })) passed++; else failed++; + + // getSessionById tests + console.log('\ngetSessionById:'); + + if (test('getSessionById finds by short ID prefix', () => { + const result = sessionManager.getSessionById('abcd1234'); + assert.ok(result, 'Should find session by exact short ID'); + assert.strictEqual(result.shortId, 'abcd1234'); + })) passed++; else failed++; + + if (test('getSessionById finds by short ID prefix match', () => { + const result = sessionManager.getSessionById('abcd'); + assert.ok(result, 'Should find session by short ID prefix'); + assert.strictEqual(result.shortId, 'abcd1234'); + })) passed++; else failed++; + + if (test('getSessionById finds by full filename', () => { + const result = sessionManager.getSessionById('2026-01-15-abcd1234-session.tmp'); + assert.ok(result, 'Should find session by full filename'); + assert.strictEqual(result.shortId, 'abcd1234'); + })) passed++; else failed++; + + if (test('getSessionById finds by filename without .tmp', () => { + const result = sessionManager.getSessionById('2026-01-15-abcd1234-session'); + assert.ok(result, 'Should find session by filename without extension'); + })) passed++; else failed++; + + if (test('getSessionById returns null for non-existent ID', () => { + const result = sessionManager.getSessionById('zzzzzzzz'); + assert.strictEqual(result, null); + })) passed++; else failed++; + + if (test('getSessionById includes content when requested', () => { + const result = sessionManager.getSessionById('abcd1234', true); + assert.ok(result, 'Should find session'); + assert.ok(result.content, 'Should include content'); + assert.ok(result.content.includes('Session 1'), 'Content should match'); + })) passed++; else failed++; + + if (test('getSessionById finds old format (no short ID)', () => { + const result = sessionManager.getSessionById('2026-02-10-session'); + assert.ok(result, 'Should find old-format session by filename'); + })) passed++; else failed++; + + // Cleanup + process.env.HOME = origHome; + try { + fs.rmSync(tmpHome, { recursive: true, force: true }); + } catch { + // best-effort + } + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js index cbeef9a..7b975ce 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -397,6 +397,155 @@ function runTests() { assert.strictEqual(result.success, false); })) passed++; else failed++; + // output() and log() tests + console.log('\noutput() and log():'); + + if (test('output() writes string to stdout', () => { + // Capture stdout by temporarily replacing console.log + let captured = null; + const origLog = console.log; + console.log = (v) => { captured = v; }; + try { + utils.output('hello'); + assert.strictEqual(captured, 'hello'); + } finally { + console.log = origLog; + } + })) passed++; else failed++; + + if (test('output() JSON-stringifies objects', () => { + let captured = null; + const origLog = console.log; + console.log = (v) => { captured = v; }; + try { + utils.output({ key: 'value', num: 42 }); + assert.strictEqual(captured, '{"key":"value","num":42}'); + } finally { + console.log = origLog; + } + })) passed++; else failed++; + + if (test('output() JSON-stringifies null (typeof null === "object")', () => { + let captured = null; + const origLog = console.log; + console.log = (v) => { captured = v; }; + try { + utils.output(null); + // typeof null === 'object' in JS, so it goes through JSON.stringify + assert.strictEqual(captured, 'null'); + } finally { + console.log = origLog; + } + })) passed++; else failed++; + + if (test('output() handles arrays as objects', () => { + let captured = null; + const origLog = console.log; + console.log = (v) => { captured = v; }; + try { + utils.output([1, 2, 3]); + assert.strictEqual(captured, '[1,2,3]'); + } finally { + console.log = origLog; + } + })) passed++; else failed++; + + if (test('log() writes to stderr', () => { + let captured = null; + const origError = console.error; + console.error = (v) => { captured = v; }; + try { + utils.log('test message'); + assert.strictEqual(captured, 'test message'); + } finally { + console.error = origError; + } + })) passed++; else failed++; + + // isGitRepo() tests + console.log('\nisGitRepo():'); + + if (test('isGitRepo returns true in a git repo', () => { + // We're running from within the ECC repo, so this should be true + assert.strictEqual(utils.isGitRepo(), true); + })) passed++; else failed++; + + // getGitModifiedFiles() tests + console.log('\ngetGitModifiedFiles():'); + + if (test('getGitModifiedFiles returns an array', () => { + const files = utils.getGitModifiedFiles(); + assert.ok(Array.isArray(files)); + })) passed++; else failed++; + + if (test('getGitModifiedFiles filters by regex patterns', () => { + const files = utils.getGitModifiedFiles(['\\.NONEXISTENT_EXTENSION$']); + assert.ok(Array.isArray(files)); + assert.strictEqual(files.length, 0); + })) passed++; else failed++; + + if (test('getGitModifiedFiles skips invalid patterns', () => { + // Mix of valid and invalid patterns — should not throw + const files = utils.getGitModifiedFiles(['(unclosed', '\\.js$', '[invalid']); + assert.ok(Array.isArray(files)); + })) passed++; else failed++; + + if (test('getGitModifiedFiles skips non-string patterns', () => { + const files = utils.getGitModifiedFiles([null, undefined, 42, '', '\\.js$']); + assert.ok(Array.isArray(files)); + })) passed++; else failed++; + + // getLearnedSkillsDir() test + console.log('\ngetLearnedSkillsDir():'); + + if (test('getLearnedSkillsDir returns path under Claude dir', () => { + const dir = utils.getLearnedSkillsDir(); + assert.ok(dir.includes('.claude')); + assert.ok(dir.includes('skills')); + assert.ok(dir.includes('learned')); + })) passed++; else failed++; + + // findFiles with regex special characters in pattern + console.log('\nfindFiles (regex chars):'); + + if (test('findFiles handles regex special chars in pattern', () => { + const testDir = path.join(utils.getTempDir(), `utils-test-regex-${Date.now()}`); + try { + fs.mkdirSync(testDir); + // Create files with regex-special characters in names + fs.writeFileSync(path.join(testDir, 'file(1).txt'), 'content'); + fs.writeFileSync(path.join(testDir, 'file+2.txt'), 'content'); + fs.writeFileSync(path.join(testDir, 'file[3].txt'), 'content'); + + // These patterns should match literally, not as regex metacharacters + const parens = utils.findFiles(testDir, 'file(1).txt'); + assert.strictEqual(parens.length, 1, 'Should match file(1).txt literally'); + + const plus = utils.findFiles(testDir, 'file+2.txt'); + assert.strictEqual(plus.length, 1, 'Should match file+2.txt literally'); + + const brackets = utils.findFiles(testDir, 'file[3].txt'); + assert.strictEqual(brackets.length, 1, 'Should match file[3].txt literally'); + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + + if (test('findFiles wildcard still works with special chars', () => { + const testDir = path.join(utils.getTempDir(), `utils-test-glob-${Date.now()}`); + try { + fs.mkdirSync(testDir); + fs.writeFileSync(path.join(testDir, 'app(v2).js'), 'content'); + fs.writeFileSync(path.join(testDir, 'app(v3).ts'), 'content'); + + const jsFiles = utils.findFiles(testDir, '*.js'); + assert.strictEqual(jsFiles.length, 1); + assert.ok(jsFiles[0].path.endsWith('app(v2).js')); + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + // Summary console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`);