Compare commits

..

65 Commits

Author SHA1 Message Date
Affaan Mustafa
db27ba1eb2 chore: update README stats and add Codex platform support
- Stars: 42K+ -> 50K+, forks: 5K+ -> 6K+, contributors: 24 -> 30
- Skills: 43 -> 44 (search-first), commands: 31 -> 32 (learn-eval)
- Add Codex to supported platforms in FAQ
- Add search-first skill and learn-eval command to directory listing
- Update OpenCode feature parity table counts
2026-02-23 06:56:00 -08:00
Affaan Mustafa
3c833d8922 Merge pull request #265 from shimo4228/feat/skills/skill-stocktake
feat(skills): add skill-stocktake skill
2026-02-23 06:55:38 -08:00
Affaan Mustafa
156b89ed30 Merge pull request #268 from pangerlkr/patch-6
feat: add scripts/codemaps/generate.ts — fixes #247
2026-02-23 06:55:35 -08:00
Pangerkumzuk Longkumer
41ce1a52e5 feat: add scripts/codemaps/generate.ts codemap generator Fixes #247 - The generate.ts script referenced in agents/doc-updater.md was missing from the repository. This adds the actual implementation. The script: - Recursively walks the src directory (skipping node_modules, dist, etc.) - Classifies files into 5 areas: frontend, backend, database, integrations, workers - Generates docs/CODEMAPS/INDEX.md + one .md per area - Uses the codemap format defined in doc-updater.md - Supports optional srcDir argument: npx tsx scripts/codemaps/generate.ts [srcDir]
This script scans the current working directory and generates architectural codemap documentation in the specified output directory. It classifies files into areas such as frontend, backend, database, integrations, and workers, and creates markdown files for each area along with an index.
2026-02-22 16:19:16 +05:30
Jongchan
6f94c2e28f fix(search-first): add missing skill name frontmatter (#266)
fix: add missing name frontmatter for search-first skill
2026-02-21 16:04:39 -08:00
Tatsuya Shimomoto
91b7ccf56f feat(skills): add skill-stocktake skill 2026-02-22 04:29:40 +09:00
Affaan Mustafa
7daa830da9 Merge pull request #263 from shimo4228/feat/commands/learn-eval
feat(commands): add learn-eval command
2026-02-20 21:17:57 -08:00
Affaan Mustafa
7e57d1b831 Merge pull request #262 from shimo4228/feat/skills/search-first
feat(skills): add search-first skill
2026-02-20 21:17:54 -08:00
Tatsuya Shimomoto
ff47dace11 feat(commands): add learn-eval command 2026-02-21 12:10:39 +09:00
Tatsuya Shimomoto
c9dc53e862 feat(skills): add search-first skill 2026-02-21 12:10:25 +09:00
Affaan Mustafa
c8f54481b8 chore: update Sonnet model references from 4.5 to 4.6
chore: update Sonnet model references from 4.5 to 4.6
2026-02-20 10:59:12 -08:00
Affaan Mustafa
294fc4aad8 fix: CI/Test for issue #226 (hook override bug)
Fixed CI / Test for (issue#226)
2026-02-20 10:59:10 -08:00
yptse123
81aa8a72c3 chore: update Sonnet model references from 4.5 to 4.6
Update MODEL_SONNET constant and all documentation references
to reflect the new claude-sonnet-4-6 model version.
2026-02-20 18:08:10 +08:00
Affaan Mustafa
0e9f613fd1 Revert "feat(ecc): prune plugin 43→12 items, promote 7 rules to .claude/rules/ (#245)"
This reverts commit 1bd68ff534.
2026-02-20 01:11:30 -08:00
park-kyungchan
1bd68ff534 feat(ecc): prune plugin 43→12 items, promote 7 rules to .claude/rules/ (#245)
ECC community plugin pruning: removed 530+ non-essential files
(.cursor/, .opencode/, docs/ja-JP, docs/zh-CN, docs/zh-TW,
language-specific skills/agents/rules). Retained 4 agents,
3 commands, 5 skills. Promoted 13 rule files (8 common + 5
typescript) to .claude/rules/ for CC native loading. Extracted
reusable patterns to EXTRACTED-PATTERNS.md.
2026-02-19 22:34:51 -08:00
Affaan Mustafa
24047351c2 Merge pull request #251 from gangqian68/main
docs: add CLAUDE.md for Claude Code guidance
2026-02-19 04:56:49 -08:00
qian gang
66959c1dca docs: add CLAUDE.md for Claude Code guidance
Add project-level CLAUDE.md with test commands, architecture
overview, key commands, and contribution guidelines.
2026-02-19 16:50:08 +08:00
Pangerkumzuk Longkumer
5a0f6e9e1e Merge pull request #12 from pangerlkr/copilot/fix-ci-test-failures-again
Fix Windows CI test failures - platform-specific test adjustments
2026-02-19 06:43:40 +05:30
copilot-swe-agent[bot]
cf61ef7539 Fix Windows CI test failures - add platform checks and USERPROFILE support
Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com>
2026-02-18 15:10:32 +00:00
copilot-swe-agent[bot]
07e23e3e64 Initial plan 2026-02-18 15:00:14 +00:00
Pangerkumzuk Longkumer
8fc49ba0e8 Merge pull request #11 from pangerlkr/copilot/fix-ci-test-failures
Fix session-manager tests failing in CI due to missing test isolation
2026-02-18 20:29:40 +05:30
copilot-swe-agent[bot]
b90448aef6 Clarify cleanup comment scope in session-manager tests
Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com>
2026-02-18 14:05:46 +00:00
copilot-swe-agent[bot]
caab908be8 Fix session-manager test environment for Rounds 95-98
Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com>
2026-02-18 14:02:11 +00:00
copilot-swe-agent[bot]
7021d1f6cf Initial plan 2026-02-18 13:51:40 +00:00
Pangerkumzuk Longkumer
3ad211b01b Merge pull request #10 from pangerlkr/copilot/fix-matrix-test-failures
Fix platform-specific hook blocking tests for CI matrix
2026-02-18 19:21:21 +05:30
copilot-swe-agent[bot]
f61c9b0caf Fix integration/hooks tests to handle Windows platform differences
Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com>
2026-02-18 13:42:05 +00:00
copilot-swe-agent[bot]
b682ac7d79 Initial plan 2026-02-18 13:36:31 +00:00
Pangerkumzuk Longkumer
e1fca6e84d Merge branch 'affaan-m:main' into main 2026-02-18 18:33:04 +05:30
Pangerkumzuk Longkumer
07530ace5f Merge pull request #9 from pangerlkr/claude/fix-agentshield-security-scan
Fix test failures and remove broken AgentShield workflow
2026-02-18 13:42:01 +05:30
anthropic-code-agent[bot]
00464b6f60 Fix failing workflows: trim action in getCommandPattern and remove broken AgentShield scan
Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com>
2026-02-18 08:06:25 +00:00
anthropic-code-agent[bot]
0c78a7c779 Initial plan 2026-02-18 08:00:23 +00:00
Pangerkumzuk Longkumer
fca997001e Merge pull request #7 from pangerlkr/copilot/fix-workflow-failures
Fix stdin size limit enforcement in hook scripts
2026-02-18 13:16:01 +05:30
copilot-swe-agent[bot]
1eca3c9130 Fix stdin overflow bug in hook scripts - truncate chunks to stay within MAX_STDIN limit
Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com>
2026-02-18 07:40:12 +00:00
copilot-swe-agent[bot]
defcdc356e Initial plan 2026-02-18 07:28:13 +00:00
Pangerkumzuk Longkumer
b548ce47c9 Merge pull request #6 from pangerlkr/copilot/fix-workflow-actions
Fix copilot-setup-steps.yml YAML structure and address review feedback
2026-02-18 12:56:29 +05:30
copilot-swe-agent[bot]
90e6a8c63b Fix copilot-setup-steps.yml and address PR review comments
Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com>
2026-02-18 07:22:05 +00:00
copilot-swe-agent[bot]
c68f7efcdc Initial plan 2026-02-18 07:16:12 +00:00
Pangerkumzuk Longkumer
aa805d5240 Merge pull request #5 from pangerlkr/claude/fix-workflow-actions
Fix ESLint errors in test files and package manager
2026-02-18 12:42:38 +05:30
anthropic-code-agent[bot]
c5ca3c698c Fix ESLint errors in test files and package-manager.js
Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com>
2026-02-18 07:04:29 +00:00
anthropic-code-agent[bot]
7e928572c7 Initial plan 2026-02-18 06:58:35 +00:00
Pangerkumzuk Longkumer
0bf47bbb41 Update print statement from 'Hfix: update package manager tests and add summaryello' to 'Goodbye' 2026-02-18 07:29:16 +05:30
Pangerkumzuk Longkumer
2ad888ca82 Refactor console log formatting in tests 2026-02-18 07:21:58 +05:30
Pangerkumzuk Longkumer
8966282e48 fix: add comments to empty catch blocks (no-empty ESLint) 2026-02-18 07:18:40 +05:30
Pangerkumzuk Longkumer
3d97985559 fix: remove unused execFileSync import (no-unused-vars ESLint) 2026-02-18 07:15:53 +05:30
Pangerkumzuk Longkumer
d54124afad fix: remove useless escape characters in regex patterns (no-useless-escape ESLint) 2026-02-18 07:14:32 +05:30
Affaan Mustafa
0b11849f1e chore: update skill count from 37 to 43, add 5 new skills to directory listing
New community skills: content-hash-cache-pattern, cost-aware-llm-pipeline,
regex-vs-llm-structured-text, swift-actor-persistence, swift-protocol-di-testing
2026-02-16 20:04:57 -08:00
Affaan Mustafa
2c26d2d67c fix: add missing process.exit(0) to early return in post-edit-console-warn hook 2026-02-16 20:03:12 -08:00
Pangerkumzuk Longkumer
fdda6cbcd9 Merge branch 'main' into main 2026-02-17 07:00:12 +05:30
Pangerkumzuk Longkumer
08ee723e85 Merge pull request #3 from pangerlkr/copilot/fix-markdownlint-errors-again
Fix markdownlint errors: MD038, MD058, MD025, MD034
2026-02-16 19:23:01 +05:30
Pangerkumzuk Longkumer
f11347a708 Merge pull request #4 from pangerlkr/copilot/fix-markdownlint-errors-another-one
Fix markdownlint errors (MD038, MD058, MD025, MD034)
2026-02-16 19:20:56 +05:30
copilot-swe-agent[bot]
586637f94c Revert unrelated package-lock.json changes
Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com>
2026-02-16 03:01:15 +00:00
copilot-swe-agent[bot]
2b6ff6b55e Initial plan for markdownlint error fixes
Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com>
2026-02-16 02:58:12 +00:00
copilot-swe-agent[bot]
2be6e09501 Initial plan 2026-02-16 02:55:40 +00:00
copilot-swe-agent[bot]
b1d47b22ea Initial plan 2026-02-16 02:37:38 +00:00
Pangerkumzuk Longkumer
9dd4f4409b Merge pull request #2 from pangerlkr/copilot/fix-markdownlint-errors
Fix markdownlint CI failures (MD038, MD058, MD025, MD034)
2026-02-16 07:58:23 +05:30
copilot-swe-agent[bot]
c5de2a7bf7 Remove misleading comments about trailing spaces
Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com>
2026-02-16 02:22:49 +00:00
copilot-swe-agent[bot]
af24c617bb Fix all markdownlint errors (MD038, MD058, MD025, MD034)
Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com>
2026-02-16 02:22:14 +00:00
copilot-swe-agent[bot]
2ca903d4c5 Initial plan 2026-02-16 02:20:28 +00:00
Pangerkumzuk Longkumer
4d98d9f125 Add Go environment setup step to workflow
Added a step to set up the Go environment in GitHub Actions workflow.
2026-02-16 07:10:39 +05:30
Pangerkumzuk Longkumer
9db98673d0 Merge branch 'affaan-m:main' into main 2026-02-09 17:12:53 +05:30
Pangerkumzuk Longkumer
fab2e05ae7 Merge branch 'affaan-m:main' into main 2026-02-02 10:53:41 +05:30
Graceme Kamei
8d65c6d429 Merge branch 'affaan-m:main' into main 2026-01-30 19:32:07 +05:30
Panger Lkr
9b2233b5bc Merge branch 'affaan-m:main' into main 2026-01-27 10:15:34 +05:30
Panger Lkr
5a26daf392 Merge pull request #1 from pangerlkr/pangerlkr-patch-1
feat: add cloud infrastructure security skill
2026-01-23 23:14:43 +05:30
Panger Lkr
438d082e30 feat: add cloud infrastructure security skill
Add comprehensive cloud and infrastructure security skill covering:
- IAM & access control (least privilege, MFA)
- Secrets management & rotation
- Network security (VPC, firewalls)
- Logging & monitoring setup
- CI/CD pipeline security
- Cloudflare/CDN security
- Backup & disaster recovery
- Pre-deployment checklist

Complements existing security-review skill with cloud-specific guidance.
2026-01-23 22:50:59 +05:30
34 changed files with 1674 additions and 350 deletions

View File

@@ -8,7 +8,7 @@ Pre-translated configurations for [Cursor IDE](https://cursor.com), part of the
|----------|-------|-------------|
| Rules | 27 | Coding standards, security, testing, patterns (common + TypeScript/Python/Go) |
| Agents | 13 | Specialized AI agents (planner, architect, code-reviewer, tdd-guide, etc.) |
| Skills | 37 | Agent skills for backend, frontend, security, TDD, and more |
| Skills | 43 | Agent skills for backend, frontend, security, TDD, and more |
| Commands | 31 | Slash commands for planning, reviewing, testing, and deployment |
| MCP Config | 1 | Pre-configured MCP servers (GitHub, Supabase, Vercel, Railway, etc.) |

View File

@@ -12,7 +12,7 @@ alwaysApply: true
- Pair programming and code generation
- Worker agents in multi-agent systems
**Sonnet 4.5** (Best coding model):
**Sonnet 4.6** (Best coding model):
- Main development work
- Orchestrating multi-agent workflows
- Complex coding tasks

View File

@@ -0,0 +1,18 @@
steps:
- name: Setup Go environment
uses: actions/setup-go@v6.2.0
with:
# The Go version to download (if necessary) and use. Supports semver spec and ranges. Be sure to enclose this option in single quotation marks.
go-version: # optional
# Path to the go.mod, go.work, .go-version, or .tool-versions file.
go-version-file: # optional
# Set this option to true if you want the action to always check for the latest available version that satisfies the version spec
check-latest: # optional
# Used to pull Go distributions from go-versions. Since there's a default, this is typically not supplied by the user. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting.
token: # optional, default is ${{ github.server_url == 'https://github.com' && github.token || '' }}
# Used to specify whether caching is needed. Set to true, if you'd like to enable caching.
cache: # optional, default is true
# Used to specify the path to a dependency file - go.sum
cache-dependency-path: # optional
# Target architecture for Go to use. Examples: x86, x64. Will use system architecture by default.
architecture: # optional

View File

@@ -1,34 +0,0 @@
name: AgentShield Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
# Prevent duplicate runs
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Minimal permissions
permissions:
contents: read
jobs:
agentshield:
name: AgentShield Scan
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run AgentShield Security Scan
uses: affaan-m/agentshield@v1
with:
path: '.'
min-severity: 'medium'
format: 'terminal'
fail-on-findings: 'false'

60
CLAUDE.md Normal file
View File

@@ -0,0 +1,60 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a **Claude Code plugin** - a collection of production-ready agents, skills, hooks, commands, rules, and MCP configurations. The project provides battle-tested workflows for software development using Claude Code.
## Running Tests
```bash
# Run all tests
node tests/run-all.js
# Run individual test files
node tests/lib/utils.test.js
node tests/lib/package-manager.test.js
node tests/hooks/hooks.test.js
```
## Architecture
The project is organized into several core components:
- **agents/** - Specialized subagents for delegation (planner, code-reviewer, tdd-guide, etc.)
- **skills/** - Workflow definitions and domain knowledge (coding standards, patterns, testing)
- **commands/** - Slash commands invoked by users (/tdd, /plan, /e2e, etc.)
- **hooks/** - Trigger-based automations (session persistence, pre/post-tool hooks)
- **rules/** - Always-follow guidelines (security, coding style, testing requirements)
- **mcp-configs/** - MCP server configurations for external integrations
- **scripts/** - Cross-platform Node.js utilities for hooks and setup
- **tests/** - Test suite for scripts and utilities
## Key Commands
- `/tdd` - Test-driven development workflow
- `/plan` - Implementation planning
- `/e2e` - Generate and run E2E tests
- `/code-review` - Quality review
- `/build-fix` - Fix build errors
- `/learn` - Extract patterns from sessions
- `/skill-create` - Generate skills from git history
## Development Notes
- Package manager detection: npm, pnpm, yarn, bun (configurable via `CLAUDE_PACKAGE_MANAGER` env var or project config)
- Cross-platform: Windows, macOS, Linux support via Node.js scripts
- Agent format: Markdown with YAML frontmatter (name, description, tools, model)
- Skill format: Markdown with clear sections for when to use, how it works, examples
- Hook format: JSON with matcher conditions and command/notification hooks
## Contributing
Follow the formats in CONTRIBUTING.md:
- Agents: Markdown with frontmatter (name, description, tools, model)
- Skills: Clear sections (When to Use, How It Works, Examples)
- Commands: Markdown with description frontmatter
- Hooks: JSON with matcher and hooks array
File naming: lowercase with hyphens (e.g., `python-reviewer.md`, `tdd-workflow.md`)

View File

@@ -13,7 +13,7 @@
![Java](https://img.shields.io/badge/-Java-ED8B00?logo=openjdk&logoColor=white)
![Markdown](https://img.shields.io/badge/-Markdown-000000?logo=markdown&logoColor=white)
> **42K+ stars** | **5K+ forks** | **24 contributors** | **6 languages supported** | **Anthropic Hackathon Winner**
> **50K+ stars** | **6K+ forks** | **30 contributors** | **6 languages supported** | **Anthropic Hackathon Winner**
---
@@ -143,7 +143,7 @@ For manual install instructions see the README in the `rules/` folder.
/plugin list everything-claude-code@everything-claude-code
```
**That's it!** You now have access to 13 agents, 37 skills, and 31 commands.
**That's it!** You now have access to 13 agents, 44 skills, and 32 commands.
---
@@ -246,6 +246,12 @@ everything-claude-code/
| |-- deployment-patterns/ # CI/CD, Docker, health checks, rollbacks (NEW)
| |-- docker-patterns/ # Docker Compose, networking, volumes, container security (NEW)
| |-- e2e-testing/ # Playwright E2E patterns and Page Object Model (NEW)
| |-- content-hash-cache-pattern/ # SHA-256 content hash caching for file processing (NEW)
| |-- cost-aware-llm-pipeline/ # LLM cost optimization, model routing, budget tracking (NEW)
| |-- regex-vs-llm-structured-text/ # Decision framework: regex vs LLM for text parsing (NEW)
| |-- swift-actor-persistence/ # Thread-safe Swift data persistence with actors (NEW)
| |-- swift-protocol-di-testing/ # Protocol-based DI for testable Swift code (NEW)
| |-- search-first/ # Research-before-coding workflow (NEW)
|
|-- commands/ # Slash commands for quick execution
| |-- tdd.md # /tdd - Test-driven development
@@ -255,6 +261,7 @@ everything-claude-code/
| |-- build-fix.md # /build-fix - Fix build errors
| |-- refactor-clean.md # /refactor-clean - Dead code removal
| |-- learn.md # /learn - Extract patterns mid-session (Longform Guide)
| |-- learn-eval.md # /learn-eval - Extract, evaluate, and save patterns (NEW)
| |-- checkpoint.md # /checkpoint - Save verification state (Longform Guide)
| |-- verify.md # /verify - Run verification loop (Longform Guide)
| |-- setup-pm.md # /setup-pm - Configure package manager
@@ -693,11 +700,12 @@ Each component is fully independent.
</details>
<details>
<summary><b>Does this work with Cursor / OpenCode?</b></summary>
<summary><b>Does this work with Cursor / OpenCode / Codex?</b></summary>
Yes. ECC is cross-platform:
- **Cursor**: Pre-translated configs in `.cursor/`. See [Cursor IDE Support](#cursor-ide-support).
- **OpenCode**: Full plugin support in `.opencode/`. See [OpenCode Support](#-opencode-support).
- **Codex**: First-class support with adapter drift guards and SessionStart fallback. See PR [#257](https://github.com/affaan-m/everything-claude-code/pull/257).
- **Claude Code**: Native — this is the primary target.
</details>
@@ -802,8 +810,8 @@ The configuration is automatically detected from `.opencode/opencode.json`.
| Feature | Claude Code | OpenCode | Status |
|---------|-------------|----------|--------|
| Agents | ✅ 13 agents | ✅ 12 agents | **Claude Code leads** |
| Commands | ✅ 31 commands | ✅ 24 commands | **Claude Code leads** |
| Skills | ✅ 37 skills | ✅ 16 skills | **Claude Code leads** |
| Commands | ✅ 32 commands | ✅ 24 commands | **Claude Code leads** |
| Skills | ✅ 44 skills | ✅ 16 skills | **Claude Code leads** |
| Hooks | ✅ 3 phases | ✅ 20+ events | **OpenCode has more!** |
| Rules | ✅ 8 rules | ✅ 8 rules | **Full parity** |
| MCP Servers | ✅ Full | ✅ Full | **Full parity** |
@@ -823,7 +831,7 @@ OpenCode's plugin system is MORE sophisticated than Claude Code with 20+ event t
**Additional OpenCode events**: `file.edited`, `file.watcher.updated`, `message.updated`, `lsp.client.diagnostics`, `tui.toast.show`, and more.
### Available Commands (31)
### Available Commands (32)
| Command | Description |
|---------|-------------|
@@ -857,6 +865,7 @@ OpenCode's plugin system is MORE sophisticated than Claude Code with 20+ event t
| `/instinct-import` | Import instincts |
| `/instinct-export` | Export instincts |
| `/evolve` | Cluster instincts into skills |
| `/learn-eval` | Extract and evaluate patterns before saving |
| `/setup-pm` | Configure package manager |
### Plugin Installation

View File

@@ -95,7 +95,7 @@ cp -r everything-claude-code/rules/* ~/.claude/rules/
/plugin list everything-claude-code@everything-claude-code
```
**完成!** 你现在可以使用 13 个代理、37 个技能和 31 个命令。
**完成!** 你现在可以使用 13 个代理、43 个技能和 31 个命令。
---

91
commands/learn-eval.md Normal file
View File

@@ -0,0 +1,91 @@
---
description: Extract reusable patterns from the session, self-evaluate quality before saving, and determine the right save location (Global vs Project).
---
# /learn-eval - Extract, Evaluate, then Save
Extends `/learn` with a quality gate and save-location decision before writing any skill file.
## What to Extract
Look for:
1. **Error Resolution Patterns** — root cause + fix + reusability
2. **Debugging Techniques** — non-obvious steps, tool combinations
3. **Workarounds** — library quirks, API limitations, version-specific fixes
4. **Project-Specific Patterns** — conventions, architecture decisions, integration patterns
## Process
1. Review the session for extractable patterns
2. Identify the most valuable/reusable insight
3. **Determine save location:**
- Ask: "Would this pattern be useful in a different project?"
- **Global** (`~/.claude/skills/learned/`): Generic patterns usable across 2+ projects (bash compatibility, LLM API behavior, debugging techniques, etc.)
- **Project** (`.claude/skills/learned/` in current project): Project-specific knowledge (quirks of a particular config file, project-specific architecture decisions, etc.)
- When in doubt, choose Global (moving Global → Project is easier than the reverse)
4. Draft the skill file using this format:
```markdown
---
name: pattern-name
description: "Under 130 characters"
user-invocable: false
origin: auto-extracted
---
# [Descriptive Pattern Name]
**Extracted:** [Date]
**Context:** [Brief description of when this applies]
## Problem
[What problem this solves - be specific]
## Solution
[The pattern/technique/workaround - with code examples]
## When to Use
[Trigger conditions]
```
5. **Self-evaluate before saving** using this rubric:
| Dimension | 1 | 3 | 5 |
|-----------|---|---|---|
| Specificity | Abstract principles only, no code examples | Representative code example present | Rich examples covering all usage patterns |
| Actionability | Unclear what to do | Main steps are understandable | Immediately actionable, edge cases covered |
| Scope Fit | Too broad or too narrow | Mostly appropriate, some boundary ambiguity | Name, trigger, and content perfectly aligned |
| Non-redundancy | Nearly identical to another skill | Some overlap but unique perspective exists | Completely unique value |
| Coverage | Covers only a fraction of the target task | Main cases covered, common variants missing | Main cases, edge cases, and pitfalls covered |
- Score each dimension 15
- If any dimension scores 12, improve the draft and re-score until all dimensions are ≥ 3
- Show the user the scores table and the final draft
6. Ask user to confirm:
- Show: proposed save path + scores table + final draft
- Wait for explicit confirmation before writing
7. Save to the determined location
## Output Format for Step 5 (scores table)
| Dimension | Score | Rationale |
|-----------|-------|-----------|
| Specificity | N/5 | ... |
| Actionability | N/5 | ... |
| Scope Fit | N/5 | ... |
| Non-redundancy | N/5 | ... |
| Coverage | N/5 | ... |
| **Total** | **N/25** | |
## Notes
- Don't extract trivial fixes (typos, simple syntax errors)
- Don't extract one-time issues (specific API outages, etc.)
- Focus on patterns that will save time in future sessions
- Keep skills focused — one pattern per skill
- If Coverage score is low, add related variants before saving

View File

@@ -140,7 +140,7 @@ cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
/plugin list everything-claude-code@everything-claude-code
```
**完了です!** これで13のエージェント、37のスキル、31のコマンドにアクセスできます。
**完了です!** これで13のエージェント、43のスキル、31のコマンドにアクセスできます。
---

View File

@@ -13,7 +13,7 @@
"C": "Cyan - username, todos",
"T": "Gray - model name"
},
"output_example": "affoon:~/projects/myapp main* ctx:73% sonnet-4.5 14:30 todos:3",
"output_example": "affoon:~/projects/myapp main* ctx:73% sonnet-4.6 14:30 todos:3",
"usage": "Copy the statusLine object to your ~/.claude/settings.json"
}
}

16
package-lock.json generated
View File

@@ -1,14 +1,25 @@
{
"name": "everything-claude-code",
"name": "ecc-universal",
"version": "1.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ecc-universal",
"version": "1.4.1",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"ecc-install": "install.sh"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"eslint": "^9.39.2",
"globals": "^17.1.0",
"markdownlint-cli": "^0.47.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
@@ -294,6 +305,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -599,6 +611,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -1930,6 +1943,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},

View File

@@ -7,7 +7,7 @@
- Pair programming and code generation
- Worker agents in multi-agent systems
**Sonnet 4.5** (Best coding model):
**Sonnet 4.6** (Best coding model):
- Main development work
- Orchestrating multi-agent workflows
- Complex coding tasks

View File

@@ -0,0 +1,330 @@
#!/usr/bin/env node
/**
* scripts/codemaps/generate.ts
*
* Codemap Generator for everything-claude-code (ECC)
*
* Scans the current working directory and generates architectural
* codemap documentation under docs/CODEMAPS/ as specified by the
* doc-updater agent.
*
* Usage:
* npx tsx scripts/codemaps/generate.ts [srcDir]
*
* Output:
* docs/CODEMAPS/INDEX.md
* docs/CODEMAPS/frontend.md
* docs/CODEMAPS/backend.md
* docs/CODEMAPS/database.md
* docs/CODEMAPS/integrations.md
* docs/CODEMAPS/workers.md
*/
import fs from 'fs';
import path from 'path';
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
const ROOT = process.cwd();
const SRC_DIR = process.argv[2] ? path.resolve(process.argv[2]) : ROOT;
const OUTPUT_DIR = path.join(ROOT, 'docs', 'CODEMAPS');
const TODAY = new Date().toISOString().split('T')[0];
// Patterns used to classify files into codemap areas
const AREA_PATTERNS: Record<string, RegExp[]> = {
frontend: [
/\/(app|pages|components|hooks|contexts|ui|views|layouts|styles)\//i,
/\.(tsx|jsx|css|scss|sass|less|vue|svelte)$/i,
],
backend: [
/\/(api|routes|controllers|middleware|server|services|handlers)\//i,
/\.(route|controller|handler|middleware|service)\.(ts|js)$/i,
],
database: [
/\/(models|schemas|migrations|prisma|drizzle|db|database|repositories)\//i,
/\.(model|schema|migration|seed)\.(ts|js)$/i,
/prisma\/schema\.prisma$/,
/schema\.sql$/,
],
integrations: [
/\/(integrations?|third-party|external|plugins?|adapters?|connectors?)\//i,
/\.(integration|adapter|connector)\.(ts|js)$/i,
],
workers: [
/\/(workers?|jobs?|queues?|tasks?|cron|background)\//i,
/\.(worker|job|queue|task|cron)\.(ts|js)$/i,
],
};
// ---------------------------------------------------------------------------
// File System Helpers
// ---------------------------------------------------------------------------
/** Recursively collect all files under a directory, skipping common noise dirs. */
function walkDir(dir: string, results: string[] = []): string[] {
const SKIP = new Set([
'node_modules', '.git', '.next', '.nuxt', 'dist', 'build', 'out',
'.turbo', 'coverage', '.cache', '__pycache__', '.venv', 'venv',
]);
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return results;
}
for (const entry of entries) {
if (SKIP.has(entry.name)) continue;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkDir(fullPath, results);
} else if (entry.isFile()) {
results.push(fullPath);
}
}
return results;
}
/** Return path relative to ROOT, always using forward slashes. */
function rel(p: string): string {
return path.relative(ROOT, p).replace(/\\/g, '/');
}
// ---------------------------------------------------------------------------
// Analysis
// ---------------------------------------------------------------------------
interface AreaInfo {
name: string;
files: string[];
entryPoints: string[];
directories: string[];
}
function classifyFiles(allFiles: string[]): Record<string, AreaInfo> {
const areas: Record<string, AreaInfo> = {
frontend: { name: 'Frontend', files: [], entryPoints: [], directories: [] },
backend: { name: 'Backend/API', files: [], entryPoints: [], directories: [] },
database: { name: 'Database', files: [], entryPoints: [], directories: [] },
integrations: { name: 'Integrations', files: [], entryPoints: [], directories: [] },
workers: { name: 'Workers', files: [], entryPoints: [], directories: [] },
};
for (const file of allFiles) {
const relPath = rel(file);
for (const [area, patterns] of Object.entries(AREA_PATTERNS)) {
if (patterns.some((p) => p.test(relPath))) {
areas[area].files.push(relPath);
break;
}
}
}
// Derive unique directories and entry points per area
for (const area of Object.values(areas)) {
const dirs = new Set(area.files.map((f) => path.dirname(f)));
area.directories = [...dirs].sort();
area.entryPoints = area.files
.filter((f) => /index\.(ts|tsx|js|jsx)$/.test(f) || /main\.(ts|tsx|js|jsx)$/.test(f))
.slice(0, 10);
}
return areas;
}
/** Count lines in a file (returns 0 on error). */
function lineCount(p: string): number {
try {
const content = fs.readFileSync(p, 'utf8');
return content.split('\n').length;
} catch {
return 0;
}
}
/** Build a simple directory tree ASCII diagram (max 3 levels deep). */
function buildTree(dir: string, prefix = '', depth = 0): string {
if (depth > 2) return '';
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage']);
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return '';
}
const dirs = entries.filter((e) => e.isDirectory() && !SKIP.has(e.name));
const files = entries.filter((e) => e.isFile());
let result = '';
const items = [...dirs, ...files];
items.forEach((entry, i) => {
const isLast = i === items.length - 1;
const connector = isLast ? '└── ' : '├── ';
result += `${prefix}${connector}${entry.name}\n`;
if (entry.isDirectory()) {
const newPrefix = prefix + (isLast ? ' ' : '│ ');
result += buildTree(path.join(dir, entry.name), newPrefix, depth + 1);
}
});
return result;
}
// ---------------------------------------------------------------------------
// Markdown Generators
// ---------------------------------------------------------------------------
function generateAreaDoc(areaKey: string, area: AreaInfo, allFiles: string[]): string {
const fileCount = area.files.length;
const totalLines = area.files.reduce((sum, f) => sum + lineCount(path.join(ROOT, f)), 0);
const entrySection = area.entryPoints.length > 0
? area.entryPoints.map((e) => `- \`${e}\``).join('\n')
: '- *(no index/main entry points detected)*';
const dirSection = area.directories.slice(0, 20)
.map((d) => `- \`${d}/\``)
.join('\n') || '- *(no dedicated directories detected)*';
const fileSection = area.files.slice(0, 30)
.map((f) => `| \`${f}\` | ${lineCount(path.join(ROOT, f))} |`)
.join('\n');
const moreFiles = area.files.length > 30
? `\n*...and ${area.files.length - 30} more files*`
: '';
return `# ${area.name} Codemap
**Last Updated:** ${TODAY}
**Total Files:** ${fileCount}
**Total Lines:** ${totalLines}
## Entry Points
${entrySection}
## Architecture
\`\`\`
${area.name} Directory Structure
${dirSection.replace(/- `/g, '').replace(/`\/$/gm, '/')}
\`\`\`
## Key Modules
| File | Lines |
|------|-------|
${fileSection}${moreFiles}
## Data Flow
> Detected from file patterns. Review individual files for detailed data flow.
## External Dependencies
> Run \`npx jsdoc2md src/**/*.ts\` to extract JSDoc and identify external dependencies.
## Related Areas
- [INDEX](./INDEX.md) — Full overview
- [Frontend](./frontend.md)
- [Backend/API](./backend.md)
- [Database](./database.md)
- [Integrations](./integrations.md)
- [Workers](./workers.md)
`;
}
function generateIndex(areas: Record<string, AreaInfo>, allFiles: string[]): string {
const totalFiles = allFiles.length;
const areaRows = Object.entries(areas)
.map(([key, area]) => `| [${area.name}](./${key}.md) | ${area.files.length} files | ${area.directories.slice(0, 3).map((d) => `\`${d}\``).join(', ') || '—'} |`)
.join('\n');
const topLevelTree = buildTree(SRC_DIR);
return `# Codebase Overview — CODEMAPS Index
**Last Updated:** ${TODAY}
**Root:** \`${rel(SRC_DIR) || '.'}\`
**Total Files Scanned:** ${totalFiles}
## Areas
| Area | Size | Key Directories |
|------|------|-----------------|
${areaRows}
## Repository Structure
\`\`\`
${rel(SRC_DIR) || path.basename(SRC_DIR)}/
${topLevelTree}\`\`\`
## How to Regenerate
\`\`\`bash
npx tsx scripts/codemaps/generate.ts # Regenerate codemaps
npx madge --image graph.svg src/ # Dependency graph (requires graphviz)
npx jsdoc2md src/**/*.ts # Extract JSDoc
\`\`\`
## Related Documentation
- [Frontend](./frontend.md) — UI components, pages, hooks
- [Backend/API](./backend.md) — API routes, controllers, middleware
- [Database](./database.md) — Models, schemas, migrations
- [Integrations](./integrations.md) — External services & adapters
- [Workers](./workers.md) — Background jobs, queues, cron tasks
`;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
function main(): void {
console.log(`[generate.ts] Scanning: ${SRC_DIR}`);
console.log(`[generate.ts] Output: ${OUTPUT_DIR}`);
// Ensure output directory exists
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
// Walk the directory tree
const allFiles = walkDir(SRC_DIR);
console.log(`[generate.ts] Found ${allFiles.length} files`);
// Classify files into areas
const areas = classifyFiles(allFiles);
// Generate INDEX.md
const indexContent = generateIndex(areas, allFiles);
const indexPath = path.join(OUTPUT_DIR, 'INDEX.md');
fs.writeFileSync(indexPath, indexContent, 'utf8');
console.log(`[generate.ts] Written: ${rel(indexPath)}`);
// Generate per-area codemaps
for (const [key, area] of Object.entries(areas)) {
const content = generateAreaDoc(key, area, allFiles);
const outPath = path.join(OUTPUT_DIR, `${key}.md`);
fs.writeFileSync(outPath, content, 'utf8');
console.log(`[generate.ts] Written: ${rel(outPath)} (${area.files.length} files)`);
}
console.log('\n[generate.ts] Done! Codemaps written to docs/CODEMAPS/');
console.log('[generate.ts] Files generated:');
console.log(' docs/CODEMAPS/INDEX.md');
console.log(' docs/CODEMAPS/frontend.md');
console.log(' docs/CODEMAPS/backend.md');
console.log(' docs/CODEMAPS/database.md');
console.log(' docs/CODEMAPS/integrations.md');
console.log(' docs/CODEMAPS/workers.md');
}
main();

View File

@@ -32,7 +32,8 @@ process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (data.length < MAX_STDIN) {
data += chunk;
const remaining = MAX_STDIN - data.length;
data += chunk.substring(0, remaining);
}
});

View File

@@ -29,7 +29,8 @@ process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (stdinData.length < MAX_STDIN) {
stdinData += chunk;
const remaining = MAX_STDIN - stdinData.length;
stdinData += chunk.substring(0, remaining);
}
});

View File

@@ -17,7 +17,8 @@ process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (data.length < MAX_STDIN) {
data += chunk;
const remaining = MAX_STDIN - data.length;
data += chunk.substring(0, remaining);
}
});
@@ -28,7 +29,7 @@ process.stdin.on('end', () => {
if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) {
const content = readFile(filePath);
if (!content) { process.stdout.write(data); return; }
if (!content) { process.stdout.write(data); process.exit(0); }
const lines = content.split('\n');
const matches = [];

View File

@@ -17,7 +17,8 @@ process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (data.length < MAX_STDIN) {
data += chunk;
const remaining = MAX_STDIN - data.length;
data += chunk.substring(0, remaining);
}
});

View File

@@ -19,7 +19,8 @@ process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
if (data.length < MAX_STDIN) {
data += chunk;
const remaining = MAX_STDIN - data.length;
data += chunk.substring(0, remaining);
}
});

View File

@@ -109,7 +109,8 @@ process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (stdinData.length < MAX_STDIN) {
stdinData += chunk;
const remaining = MAX_STDIN - stdinData.length;
stdinData += chunk.substring(0, remaining);
}
});

View File

@@ -282,7 +282,7 @@ function setProjectPackageManager(pmName, projectDir = process.cwd()) {
// Allowed characters in script/binary names: alphanumeric, dash, underscore, dot, slash, @
// This prevents shell metacharacter injection while allowing scoped packages (e.g., @scope/pkg)
const SAFE_NAME_REGEX = /^[@a-zA-Z0-9_.\/-]+$/;
const SAFE_NAME_REGEX = /^[@a-zA-Z0-9_./-]+$/;
/**
* Get the command to run a script
@@ -316,7 +316,7 @@ function getRunCommand(script, options = {}) {
// Allowed characters in arguments: alphanumeric, whitespace, dashes, dots, slashes,
// equals, colons, commas, quotes, @. Rejects shell metacharacters like ; | & ` $ ( ) { } < > !
const SAFE_ARGS_REGEX = /^[@a-zA-Z0-9\s_.\/:=,'"*+-]+$/;
const SAFE_ARGS_REGEX = /^[@a-zA-Z0-9\s_./:=,'"*+-]+$/;
/**
* Get the command to execute a package binary
@@ -370,28 +370,31 @@ function escapeRegex(str) {
function getCommandPattern(action) {
const patterns = [];
if (action === 'dev') {
// Trim spaces from action to handle leading/trailing whitespace gracefully
const trimmedAction = action.trim();
if (trimmedAction === 'dev') {
patterns.push(
'npm run dev',
'pnpm( run)? dev',
'yarn dev',
'bun run dev'
);
} else if (action === 'install') {
} else if (trimmedAction === 'install') {
patterns.push(
'npm install',
'pnpm install',
'yarn( install)?',
'bun install'
);
} else if (action === 'test') {
} else if (trimmedAction === 'test') {
patterns.push(
'npm test',
'pnpm test',
'yarn test',
'bun test'
);
} else if (action === 'build') {
} else if (trimmedAction === 'build') {
patterns.push(
'npm run build',
'pnpm( run)? build',
@@ -400,7 +403,7 @@ function getCommandPattern(action) {
);
} else {
// Generic run command — escape regex metacharacters in action
const escaped = escapeRegex(action);
const escaped = escapeRegex(trimmedAction);
patterns.push(
`npm run ${escaped}`,
`pnpm( run)? ${escaped}`,

View File

@@ -21,7 +21,7 @@ Patterns for controlling LLM API costs while maintaining quality. Combines model
Automatically select cheaper models for simple tasks, reserving expensive models for complex ones.
```python
MODEL_SONNET = "claude-sonnet-4-5-20250929"
MODEL_SONNET = "claude-sonnet-4-6"
MODEL_HAIKU = "claude-haiku-4-5-20251001"
_SONNET_TEXT_THRESHOLD = 10_000 # chars
@@ -155,7 +155,7 @@ def process(text: str, config: Config, tracker: CostTracker) -> tuple[Result, Co
| Model | Input ($/1M tokens) | Output ($/1M tokens) | Relative Cost |
|-------|---------------------|----------------------|---------------|
| Haiku 4.5 | $0.80 | $4.00 | 1x |
| Sonnet 4.5 | $3.00 | $15.00 | ~4x |
| Sonnet 4.6 | $3.00 | $15.00 | ~4x |
| Opus 4.5 | $15.00 | $75.00 | ~19x |
## Best Practices

View File

@@ -0,0 +1,159 @@
---
name: search-first
description: Research-before-coding workflow. Search for existing tools, libraries, and patterns before writing custom code. Invokes the researcher agent.
---
# /search-first — Research Before You Code
Systematizes the "search for existing solutions before implementing" workflow.
## Trigger
Use this skill when:
- Starting a new feature that likely has existing solutions
- Adding a dependency or integration
- The user asks "add X functionality" and you're about to write code
- Before creating a new utility, helper, or abstraction
## Workflow
```
┌─────────────────────────────────────────────┐
│ 1. NEED ANALYSIS │
│ Define what functionality is needed │
│ Identify language/framework constraints │
├─────────────────────────────────────────────┤
│ 2. PARALLEL SEARCH (researcher agent) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ npm / │ │ MCP / │ │ GitHub / │ │
│ │ PyPI │ │ Skills │ │ Web │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────┤
│ 3. EVALUATE │
│ Score candidates (functionality, maint, │
│ community, docs, license, deps) │
├─────────────────────────────────────────────┤
│ 4. DECIDE │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ │
│ │ Adopt │ │ Extend │ │ Build │ │
│ │ as-is │ │ /Wrap │ │ Custom │ │
│ └─────────┘ └──────────┘ └─────────┘ │
├─────────────────────────────────────────────┤
│ 5. IMPLEMENT │
│ Install package / Configure MCP / │
│ Write minimal custom code │
└─────────────────────────────────────────────┘
```
## Decision Matrix
| Signal | Action |
|--------|--------|
| Exact match, well-maintained, MIT/Apache | **Adopt** — install and use directly |
| Partial match, good foundation | **Extend** — install + write thin wrapper |
| Multiple weak matches | **Compose** — combine 2-3 small packages |
| Nothing suitable found | **Build** — write custom, but informed by research |
## How to Use
### Quick Mode (inline)
Before writing a utility or adding functionality, mentally run through:
1. Is this a common problem? → Search npm/PyPI
2. Is there an MCP for this? → Check `~/.claude/settings.json` and search
3. Is there a skill for this? → Check `~/.claude/skills/`
4. Is there a GitHub template? → Search GitHub
### Full Mode (agent)
For non-trivial functionality, launch the researcher agent:
```
Task(subagent_type="general-purpose", prompt="
Research existing tools for: [DESCRIPTION]
Language/framework: [LANG]
Constraints: [ANY]
Search: npm/PyPI, MCP servers, Claude Code skills, GitHub
Return: Structured comparison with recommendation
")
```
## Search Shortcuts by Category
### Development Tooling
- Linting → `eslint`, `ruff`, `textlint`, `markdownlint`
- Formatting → `prettier`, `black`, `gofmt`
- Testing → `jest`, `pytest`, `go test`
- Pre-commit → `husky`, `lint-staged`, `pre-commit`
### AI/LLM Integration
- Claude SDK → Context7 for latest docs
- Prompt management → Check MCP servers
- Document processing → `unstructured`, `pdfplumber`, `mammoth`
### Data & APIs
- HTTP clients → `httpx` (Python), `ky`/`got` (Node)
- Validation → `zod` (TS), `pydantic` (Python)
- Database → Check for MCP servers first
### Content & Publishing
- Markdown processing → `remark`, `unified`, `markdown-it`
- Image optimization → `sharp`, `imagemin`
## Integration Points
### With planner agent
The planner should invoke researcher before Phase 1 (Architecture Review):
- Researcher identifies available tools
- Planner incorporates them into the implementation plan
- Avoids "reinventing the wheel" in the plan
### With architect agent
The architect should consult researcher for:
- Technology stack decisions
- Integration pattern discovery
- Existing reference architectures
### With iterative-retrieval skill
Combine for progressive discovery:
- Cycle 1: Broad search (npm, PyPI, MCP)
- Cycle 2: Evaluate top candidates in detail
- Cycle 3: Test compatibility with project constraints
## Examples
### Example 1: "Add dead link checking"
```
Need: Check markdown files for broken links
Search: npm "markdown dead link checker"
Found: textlint-rule-no-dead-link (score: 9/10)
Action: ADOPT — npm install textlint-rule-no-dead-link
Result: Zero custom code, battle-tested solution
```
### Example 2: "Add HTTP client wrapper"
```
Need: Resilient HTTP client with retries and timeout handling
Search: npm "http client retry", PyPI "httpx retry"
Found: got (Node) with retry plugin, httpx (Python) with built-in retry
Action: ADOPT — use got/httpx directly with retry config
Result: Zero custom code, production-proven libraries
```
### Example 3: "Add config file linter"
```
Need: Validate project config files against a schema
Search: npm "config linter schema", "json schema validator cli"
Found: ajv-cli (score: 8/10)
Action: ADOPT + EXTEND — install ajv-cli, write project-specific schema
Result: 1 package + 1 schema file, no custom validation logic
```
## Anti-Patterns
- **Jumping to code**: Writing a utility without checking if one exists
- **Ignoring MCP**: Not checking if an MCP server already provides the capability
- **Over-customizing**: Wrapping a library so heavily it loses its benefits
- **Dependency bloat**: Installing a massive package for one small feature

View File

@@ -0,0 +1,175 @@
---
description: "Use when auditing Claude skills and commands for quality. Supports Quick Scan (changed skills only) and Full Stocktake modes with sequential subagent batch evaluation."
---
# skill-stocktake
Slash command (`/skill-stocktake`) that audits all Claude skills and commands using a quality checklist + AI holistic judgment. Supports two modes: Quick Scan for recently changed skills, and Full Stocktake for a complete review.
## Scope
The command targets the following paths **relative to the directory where it is invoked**:
| Path | Description |
|------|-------------|
| `~/.claude/skills/` | Global skills (all projects) |
| `{cwd}/.claude/skills/` | Project-level skills (if the directory exists) |
**At the start of Phase 1, the command explicitly lists which paths were found and scanned.**
### Targeting a specific project
To include project-level skills, run from that project's root directory:
```bash
cd ~/path/to/my-project
/skill-stocktake
```
If the project has no `.claude/skills/` directory, only global skills and commands are evaluated.
## Modes
| Mode | Trigger | Duration |
|------|---------|---------|
| Quick Scan | `results.json` exists (default) | 510 min |
| Full Stocktake | `results.json` absent, or `/skill-stocktake full` | 2030 min |
**Results cache:** `~/.claude/skills/skill-stocktake/results.json`
## Quick Scan Flow
Re-evaluate only skills that have changed since the last run (510 min).
1. Read `~/.claude/skills/skill-stocktake/results.json`
2. Run: `bash ~/.claude/skills/skill-stocktake/scripts/quick-diff.sh \
~/.claude/skills/skill-stocktake/results.json`
(Project dir is auto-detected from `$PWD/.claude/skills`; pass it explicitly only if needed)
3. If output is `[]`: report "No changes since last run." and stop
4. Re-evaluate only those changed files using the same Phase 2 criteria
5. Carry forward unchanged skills from previous results
6. Output only the diff
7. Run: `bash ~/.claude/skills/skill-stocktake/scripts/save-results.sh \
~/.claude/skills/skill-stocktake/results.json <<< "$EVAL_RESULTS"`
## Full Stocktake Flow
### Phase 1 — Inventory
Run: `bash ~/.claude/skills/skill-stocktake/scripts/scan.sh`
The script enumerates skill files, extracts frontmatter, and collects UTC mtimes.
Project dir is auto-detected from `$PWD/.claude/skills`; pass it explicitly only if needed.
Present the scan summary and inventory table from the script output:
```
Scanning:
✓ ~/.claude/skills/ (17 files)
✗ {cwd}/.claude/skills/ (not found — global skills only)
```
| Skill | 7d use | 30d use | Description |
|-------|--------|---------|-------------|
### Phase 2 — Quality Evaluation
Launch a Task tool subagent (**Explore agent, model: opus**) with the full inventory and checklist.
The subagent reads each skill, applies the checklist, and returns per-skill JSON:
`{ "verdict": "Keep"|"Improve"|"Update"|"Retire"|"Merge into [X]", "reason": "..." }`
**Chunk guidance:** Process ~20 skills per subagent invocation to keep context manageable. Save intermediate results to `results.json` (`status: "in_progress"`) after each chunk.
After all skills are evaluated: set `status: "completed"`, proceed to Phase 3.
**Resume detection:** If `status: "in_progress"` is found on startup, resume from the first unevaluated skill.
Each skill is evaluated against this checklist:
```
- [ ] Content overlap with other skills checked
- [ ] Overlap with MEMORY.md / CLAUDE.md checked
- [ ] Freshness of technical references verified (use WebSearch if tool names / CLI flags / APIs are present)
- [ ] Usage frequency considered
```
Verdict criteria:
| Verdict | Meaning |
|---------|---------|
| Keep | Useful and current |
| Improve | Worth keeping, but specific improvements needed |
| Update | Referenced technology is outdated (verify with WebSearch) |
| Retire | Low quality, stale, or cost-asymmetric |
| Merge into [X] | Substantial overlap with another skill; name the merge target |
Evaluation is **holistic AI judgment** — not a numeric rubric. Guiding dimensions:
- **Actionability**: code examples, commands, or steps that let you act immediately
- **Scope fit**: name, trigger, and content are aligned; not too broad or narrow
- **Uniqueness**: value not replaceable by MEMORY.md / CLAUDE.md / another skill
- **Currency**: technical references work in the current environment
**Reason quality requirements** — the `reason` field must be self-contained and decision-enabling:
- Do NOT write "unchanged" alone — always restate the core evidence
- For **Retire**: state (1) what specific defect was found, (2) what covers the same need instead
- Bad: `"Superseded"`
- Good: `"disable-model-invocation: true already set; superseded by continuous-learning-v2 which covers all the same patterns plus confidence scoring. No unique content remains."`
- For **Merge**: name the target and describe what content to integrate
- Bad: `"Overlaps with X"`
- Good: `"42-line thin content; Step 4 of chatlog-to-article already covers the same workflow. Integrate the 'article angle' tip as a note in that skill."`
- For **Improve**: describe the specific change needed (what section, what action, target size if relevant)
- Bad: `"Too long"`
- Good: `"276 lines; Section 'Framework Comparison' (L80140) duplicates ai-era-architecture-principles; delete it to reach ~150 lines."`
- For **Keep** (mtime-only change in Quick Scan): restate the original verdict rationale, do not write "unchanged"
- Bad: `"Unchanged"`
- Good: `"mtime updated but content unchanged. Unique Python reference explicitly imported by rules/python/; no overlap found."`
### Phase 3 — Summary Table
| Skill | 7d use | Verdict | Reason |
|-------|--------|---------|--------|
### Phase 4 — Consolidation
1. **Retire / Merge**: present detailed justification per file before confirming with user:
- What specific problem was found (overlap, staleness, broken references, etc.)
- What alternative covers the same functionality (for Retire: which existing skill/rule; for Merge: the target file and what content to integrate)
- Impact of removal (any dependent skills, MEMORY.md references, or workflows affected)
2. **Improve**: present specific improvement suggestions with rationale:
- What to change and why (e.g., "trim 430→200 lines because sections X/Y duplicate python-patterns")
- User decides whether to act
3. **Update**: present updated content with sources checked
4. Check MEMORY.md line count; propose compression if >100 lines
## Results File Schema
`~/.claude/skills/skill-stocktake/results.json`:
**`evaluated_at`**: Must be set to the actual UTC time of evaluation completion.
Obtain via Bash: `date -u +%Y-%m-%dT%H:%M:%SZ`. Never use a date-only approximation like `T00:00:00Z`.
```json
{
"evaluated_at": "2026-02-21T10:00:00Z",
"mode": "full",
"batch_progress": {
"total": 80,
"evaluated": 80,
"status": "completed"
},
"skills": {
"skill-name": {
"path": "~/.claude/skills/skill-name/SKILL.md",
"verdict": "Keep",
"reason": "Concrete, actionable, unique value for X workflow",
"mtime": "2026-01-15T08:30:00Z"
}
}
}
```
## Notes
- Evaluation is blind: the same checklist applies to all skills regardless of origin (ECC, self-authored, auto-extracted)
- Archive / delete operations always require explicit user confirmation
- No verdict branching by skill origin

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# quick-diff.sh — compare skill file mtimes against results.json evaluated_at
# Usage: quick-diff.sh RESULTS_JSON [CWD_SKILLS_DIR]
# Output: JSON array of changed/new files to stdout (empty [] if no changes)
#
# When CWD_SKILLS_DIR is omitted, defaults to $PWD/.claude/skills so the
# script always picks up project-level skills without relying on the caller.
#
# Environment:
# SKILL_STOCKTAKE_GLOBAL_DIR Override ~/.claude/skills (for testing only;
# do not set in production — intended for bats tests)
# SKILL_STOCKTAKE_PROJECT_DIR Override project dir detection (for testing only)
set -euo pipefail
RESULTS_JSON="${1:-}"
CWD_SKILLS_DIR="${SKILL_STOCKTAKE_PROJECT_DIR:-${2:-$PWD/.claude/skills}}"
GLOBAL_DIR="${SKILL_STOCKTAKE_GLOBAL_DIR:-$HOME/.claude/skills}"
if [[ -z "$RESULTS_JSON" || ! -f "$RESULTS_JSON" ]]; then
echo "Error: RESULTS_JSON not found: ${RESULTS_JSON:-<empty>}" >&2
exit 1
fi
# Validate CWD_SKILLS_DIR looks like a .claude/skills path (defense-in-depth).
# Only warn when the path exists — a nonexistent path poses no traversal risk.
if [[ -n "$CWD_SKILLS_DIR" && -d "$CWD_SKILLS_DIR" && "$CWD_SKILLS_DIR" != */.claude/skills* ]]; then
echo "Warning: CWD_SKILLS_DIR does not look like a .claude/skills path: $CWD_SKILLS_DIR" >&2
fi
evaluated_at=$(jq -r '.evaluated_at' "$RESULTS_JSON")
# Fail fast on a missing or malformed evaluated_at rather than producing
# unpredictable results from ISO 8601 string comparison against "null".
if [[ ! "$evaluated_at" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then
echo "Error: invalid or missing evaluated_at in $RESULTS_JSON: $evaluated_at" >&2
exit 1
fi
# Pre-extract known paths from results.json once (O(1) lookup per file instead of O(n*m))
known_paths=$(jq -r '.skills[].path' "$RESULTS_JSON" 2>/dev/null)
tmpdir=$(mktemp -d)
# Use a function to avoid embedding $tmpdir in a quoted string (prevents injection
# if TMPDIR were crafted to contain shell metacharacters).
_cleanup() { rm -rf "$tmpdir"; }
trap _cleanup EXIT
# Shared counter across process_dir calls — intentionally NOT local
i=0
process_dir() {
local dir="$1"
while IFS= read -r file; do
local mtime dp is_new
mtime=$(date -u -r "$file" +%Y-%m-%dT%H:%M:%SZ)
dp="${file/#$HOME/~}"
# Check if this file is known to results.json (exact whole-line match to
# avoid substring false-positives, e.g. "python-patterns" matching "python-patterns-v2").
if echo "$known_paths" | grep -qxF "$dp"; then
is_new="false"
# Known file: only emit if mtime changed (ISO 8601 string comparison is safe)
[[ "$mtime" > "$evaluated_at" ]] || continue
else
is_new="true"
# New file: always emit regardless of mtime
fi
jq -n \
--arg path "$dp" \
--arg mtime "$mtime" \
--argjson is_new "$is_new" \
'{path:$path,mtime:$mtime,is_new:$is_new}' \
> "$tmpdir/$i.json"
i=$((i+1))
done < <(find "$dir" -name "*.md" -type f 2>/dev/null | sort)
}
[[ -d "$GLOBAL_DIR" ]] && process_dir "$GLOBAL_DIR"
[[ -n "$CWD_SKILLS_DIR" && -d "$CWD_SKILLS_DIR" ]] && process_dir "$CWD_SKILLS_DIR"
if [[ $i -eq 0 ]]; then
echo "[]"
else
jq -s '.' "$tmpdir"/*.json
fi

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# save-results.sh — merge evaluated skills into results.json with correct UTC timestamp
# Usage: save-results.sh RESULTS_JSON <<< "$EVAL_JSON"
#
# stdin format:
# { "skills": {...}, "mode"?: "full"|"quick", "batch_progress"?: {...} }
#
# Always sets evaluated_at to current UTC time via `date -u`.
# Merges stdin .skills into existing results.json (new entries override old).
# Optionally updates .mode and .batch_progress if present in stdin.
set -euo pipefail
RESULTS_JSON="${1:-}"
if [[ -z "$RESULTS_JSON" ]]; then
echo "Error: RESULTS_JSON argument required" >&2
echo "Usage: save-results.sh RESULTS_JSON <<< \"\$EVAL_JSON\"" >&2
exit 1
fi
EVALUATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# Read eval results from stdin and validate JSON before touching the results file
input_json=$(cat)
if ! echo "$input_json" | jq empty 2>/dev/null; then
echo "Error: stdin is not valid JSON" >&2
exit 1
fi
if [[ ! -f "$RESULTS_JSON" ]]; then
# Bootstrap: create new results.json from stdin JSON + current UTC timestamp
echo "$input_json" | jq --arg ea "$EVALUATED_AT" \
'. + { evaluated_at: $ea }' > "$RESULTS_JSON"
exit 0
fi
# Merge: new .skills override existing ones; old skills not in input_json are kept.
# Optionally update .mode and .batch_progress if provided.
#
# Use mktemp for a collision-safe temp file (concurrent runs on the same RESULTS_JSON
# would race on a predictable ".tmp" suffix; random suffix prevents silent overwrites).
tmp=$(mktemp "${RESULTS_JSON}.XXXXXX")
trap 'rm -f "$tmp"' EXIT
jq -s \
--arg ea "$EVALUATED_AT" \
'.[0] as $existing | .[1] as $new |
$existing |
.evaluated_at = $ea |
.skills = ($existing.skills + ($new.skills // {})) |
if ($new | has("mode")) then .mode = $new.mode else . end |
if ($new | has("batch_progress")) then .batch_progress = $new.batch_progress else . end' \
"$RESULTS_JSON" <(echo "$input_json") > "$tmp"
mv "$tmp" "$RESULTS_JSON"

View File

@@ -0,0 +1,170 @@
#!/usr/bin/env bash
# scan.sh — enumerate skill files, extract frontmatter and UTC mtime
# Usage: scan.sh [CWD_SKILLS_DIR]
# Output: JSON to stdout
#
# When CWD_SKILLS_DIR is omitted, defaults to $PWD/.claude/skills so the
# script always picks up project-level skills without relying on the caller.
#
# Environment:
# SKILL_STOCKTAKE_GLOBAL_DIR Override ~/.claude/skills (for testing only;
# do not set in production — intended for bats tests)
# SKILL_STOCKTAKE_PROJECT_DIR Override project dir detection (for testing only)
set -euo pipefail
GLOBAL_DIR="${SKILL_STOCKTAKE_GLOBAL_DIR:-$HOME/.claude/skills}"
CWD_SKILLS_DIR="${SKILL_STOCKTAKE_PROJECT_DIR:-${1:-$PWD/.claude/skills}}"
# Path to JSONL file containing tool-use observations (optional; used for usage frequency counts).
# Override via SKILL_STOCKTAKE_OBSERVATIONS env var if your setup uses a different path.
OBSERVATIONS="${SKILL_STOCKTAKE_OBSERVATIONS:-$HOME/.claude/observations.jsonl}"
# Validate CWD_SKILLS_DIR looks like a .claude/skills path (defense-in-depth).
# Only warn when the path exists — a nonexistent path poses no traversal risk.
if [[ -n "$CWD_SKILLS_DIR" && -d "$CWD_SKILLS_DIR" && "$CWD_SKILLS_DIR" != */.claude/skills* ]]; then
echo "Warning: CWD_SKILLS_DIR does not look like a .claude/skills path: $CWD_SKILLS_DIR" >&2
fi
# Extract a frontmatter field (handles both quoted and unquoted single-line values).
# Does NOT support multi-line YAML blocks (| or >) or nested YAML keys.
extract_field() {
local file="$1" field="$2"
awk -v f="$field" '
BEGIN { fm=0 }
/^---$/ { fm++; next }
fm==1 {
n = length(f) + 2
if (substr($0, 1, n) == f ": ") {
val = substr($0, n+1)
gsub(/^"/, "", val)
gsub(/"$/, "", val)
print val
exit
}
}
fm>=2 { exit }
' "$file"
}
# Get UTC timestamp N days ago (supports both macOS and GNU date)
date_ago() {
local n="$1"
date -u -v-"${n}d" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null ||
date -u -d "${n} days ago" +%Y-%m-%dT%H:%M:%SZ
}
# Count observations matching a file path since a cutoff timestamp
count_obs() {
local file="$1" cutoff="$2"
if [[ ! -f "$OBSERVATIONS" ]]; then
echo 0
return
fi
jq -r --arg p "$file" --arg c "$cutoff" \
'select(.tool=="Read" and .path==$p and .timestamp>=$c) | 1' \
"$OBSERVATIONS" 2>/dev/null | wc -l | tr -d ' '
}
# Scan a directory and produce a JSON array of skill objects
scan_dir_to_json() {
local dir="$1"
local c7 c30
c7=$(date_ago 7)
c30=$(date_ago 30)
local tmpdir
tmpdir=$(mktemp -d)
# Use a function to avoid embedding $tmpdir in a quoted string (prevents injection
# if TMPDIR were crafted to contain shell metacharacters).
local _scan_tmpdir="$tmpdir"
_scan_cleanup() { rm -rf "$_scan_tmpdir"; }
trap _scan_cleanup RETURN
# Pre-aggregate observation counts in two passes (one per window) instead of
# calling jq per-file — reduces from O(n*m) to O(n+m) jq invocations.
local obs_7d_counts obs_30d_counts
obs_7d_counts=""
obs_30d_counts=""
if [[ -f "$OBSERVATIONS" ]]; then
obs_7d_counts=$(jq -r --arg c "$c7" \
'select(.tool=="Read" and .timestamp>=$c) | .path' \
"$OBSERVATIONS" 2>/dev/null | sort | uniq -c)
obs_30d_counts=$(jq -r --arg c "$c30" \
'select(.tool=="Read" and .timestamp>=$c) | .path' \
"$OBSERVATIONS" 2>/dev/null | sort | uniq -c)
fi
local i=0
while IFS= read -r file; do
local name desc mtime u7 u30 dp
name=$(extract_field "$file" "name")
desc=$(extract_field "$file" "description")
mtime=$(date -u -r "$file" +%Y-%m-%dT%H:%M:%SZ)
# Use awk exact field match to avoid substring false-positives from grep -F.
# uniq -c output format: " N /path/to/file" — path is always field 2.
u7=$(echo "$obs_7d_counts" | awk -v f="$file" '$2 == f {print $1}' | head -1)
u7="${u7:-0}"
u30=$(echo "$obs_30d_counts" | awk -v f="$file" '$2 == f {print $1}' | head -1)
u30="${u30:-0}"
dp="${file/#$HOME/~}"
jq -n \
--arg path "$dp" \
--arg name "$name" \
--arg description "$desc" \
--arg mtime "$mtime" \
--argjson use_7d "$u7" \
--argjson use_30d "$u30" \
'{path:$path,name:$name,description:$description,use_7d:$use_7d,use_30d:$use_30d,mtime:$mtime}' \
> "$tmpdir/$i.json"
i=$((i+1))
done < <(find "$dir" -name "*.md" -type f 2>/dev/null | sort)
if [[ $i -eq 0 ]]; then
echo "[]"
else
jq -s '.' "$tmpdir"/*.json
fi
}
# --- Main ---
global_found="false"
global_count=0
global_skills="[]"
if [[ -d "$GLOBAL_DIR" ]]; then
global_found="true"
global_skills=$(scan_dir_to_json "$GLOBAL_DIR")
global_count=$(echo "$global_skills" | jq 'length')
fi
project_found="false"
project_path=""
project_count=0
project_skills="[]"
if [[ -n "$CWD_SKILLS_DIR" && -d "$CWD_SKILLS_DIR" ]]; then
project_found="true"
project_path="$CWD_SKILLS_DIR"
project_skills=$(scan_dir_to_json "$CWD_SKILLS_DIR")
project_count=$(echo "$project_skills" | jq 'length')
fi
# Merge global + project skills into one array
all_skills=$(jq -s 'add' <(echo "$global_skills") <(echo "$project_skills"))
jq -n \
--arg global_found "$global_found" \
--argjson global_count "$global_count" \
--arg project_found "$project_found" \
--arg project_path "$project_path" \
--argjson project_count "$project_count" \
--argjson skills "$all_skills" \
'{
scan_summary: {
global: { found: ($global_found == "true"), count: $global_count },
project: { found: ($project_found == "true"), path: $project_path, count: $project_count }
},
skills: $skills
}'

View File

@@ -11,7 +11,7 @@ const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { spawnSync, execFileSync } = require('child_process');
const { spawnSync } = require('child_process');
const evaluateScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'evaluate-session.js');

View File

@@ -1324,7 +1324,7 @@ async function runTests() {
val = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(val, 2, 'Second call should write count 2');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1341,7 +1341,7 @@ async function runTests() {
assert.strictEqual(result.code, 0);
assert.ok(result.stderr.includes('5 tool calls reached'), 'Should suggest compact at threshold');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1359,7 +1359,7 @@ async function runTests() {
assert.strictEqual(result.code, 0);
assert.ok(result.stderr.includes('30 tool calls'), 'Should suggest at threshold + 25n intervals');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1376,7 +1376,7 @@ async function runTests() {
assert.ok(!result.stderr.includes('tool calls reached'), 'Should not suggest below threshold');
assert.ok(!result.stderr.includes('checkpoint'), 'Should not suggest checkpoint');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1394,7 +1394,7 @@ async function runTests() {
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(newCount, 1, 'Should reset to 1 on overflow value');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1410,7 +1410,7 @@ async function runTests() {
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(newCount, 1, 'Should reset to 1 on negative value');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1426,7 +1426,7 @@ async function runTests() {
assert.strictEqual(result.code, 0);
assert.ok(result.stderr.includes('50 tool calls reached'), 'Zero threshold should fall back to 50');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1443,7 +1443,7 @@ async function runTests() {
assert.strictEqual(result.code, 0);
assert.ok(result.stderr.includes('50 tool calls reached'), 'Should use default threshold of 50');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1883,7 +1883,7 @@ async function runTests() {
assert.strictEqual(result.code, 0);
assert.ok(result.stderr.includes('38 tool calls'), 'Should suggest at threshold(13) + 25 = 38');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1901,7 +1901,7 @@ async function runTests() {
assert.strictEqual(result.code, 0);
assert.ok(!result.stderr.includes('checkpoint'), 'Should NOT suggest at count=50 with threshold=13');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1918,7 +1918,7 @@ async function runTests() {
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(newCount, 1, 'Should reset to 1 on corrupted file content');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1935,7 +1935,7 @@ async function runTests() {
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(newCount, 1000001, 'Should increment from exactly 1000000');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;

View File

@@ -19,11 +19,11 @@ const compactScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'sugg
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
console.log(` \u2713 ${name}`);
return true;
} catch (err) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${err.message}`);
} catch (_err) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${_err.message}`);
return false;
}
}
@@ -66,7 +66,11 @@ function runTests() {
// Cleanup helper
function cleanupCounter() {
try { fs.unlinkSync(counterFile); } catch {}
try {
fs.unlinkSync(counterFile);
} catch (_err) {
// Ignore error
}
}
// Basic functionality
@@ -80,7 +84,8 @@ function runTests() {
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Counter should be 1 after first run');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('increments counter on subsequent runs', () => {
cleanupCounter();
@@ -90,7 +95,8 @@ function runTests() {
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 3, 'Counter should be 3 after three runs');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// Threshold suggestion
console.log('\nThreshold suggestion:');
@@ -106,7 +112,8 @@ function runTests() {
`Should suggest compact at threshold. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('does NOT suggest compact before threshold', () => {
cleanupCounter();
@@ -117,7 +124,8 @@ function runTests() {
'Should NOT suggest compact before threshold'
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// Interval suggestion (every 25 calls after threshold)
console.log('\nInterval suggestion:');
@@ -135,7 +143,8 @@ function runTests() {
`Should suggest at threshold+25 interval. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// Environment variable handling
console.log('\nEnvironment variable handling:');
@@ -151,7 +160,8 @@ function runTests() {
`Should use default threshold of 50. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('ignores invalid COMPACT_THRESHOLD (negative)', () => {
cleanupCounter();
@@ -163,7 +173,8 @@ function runTests() {
`Should fallback to 50 for negative threshold. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('ignores non-numeric COMPACT_THRESHOLD', () => {
cleanupCounter();
@@ -175,7 +186,8 @@ function runTests() {
`Should fallback to 50 for non-numeric threshold. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// Corrupted counter file
console.log('\nCorrupted counter file:');
@@ -189,7 +201,8 @@ function runTests() {
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Should reset to 1 on corrupted file');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('resets counter on extremely large value', () => {
cleanupCounter();
@@ -200,7 +213,8 @@ function runTests() {
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Should reset to 1 for value > 1000000');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('handles empty counter file', () => {
cleanupCounter();
@@ -211,7 +225,8 @@ function runTests() {
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Should start at 1 for empty file');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// Session isolation
console.log('\nSession isolation:');
@@ -230,10 +245,11 @@ function runTests() {
assert.strictEqual(countA, 2, 'Session A should have count 2');
assert.strictEqual(countB, 1, 'Session B should have count 1');
} finally {
try { fs.unlinkSync(fileA); } catch {}
try { fs.unlinkSync(fileB); } catch {}
try { fs.unlinkSync(fileA); } catch (_err) { /* ignore */ }
try { fs.unlinkSync(fileB); } catch (_err) { /* ignore */ }
}
})) passed++; else failed++;
})) passed++;
else failed++;
// Always exits 0
console.log('\nExit code:');
@@ -243,7 +259,8 @@ function runTests() {
const result = runCompact({ CLAUDE_SESSION_ID: testSession });
assert.strictEqual(result.code, 0, 'Should always exit 0');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// ── Round 29: threshold boundary values ──
console.log('\nThreshold boundary values:');
@@ -258,7 +275,8 @@ function runTests() {
`Should fallback to 50 for threshold=0. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('accepts COMPACT_THRESHOLD=10000 (boundary max)', () => {
cleanupCounter();
@@ -270,7 +288,8 @@ function runTests() {
`Should accept threshold=10000. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('rejects COMPACT_THRESHOLD=10001 (falls back to 50)', () => {
cleanupCounter();
@@ -282,7 +301,8 @@ function runTests() {
`Should fallback to 50 for threshold=10001. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('rejects float COMPACT_THRESHOLD (e.g. 3.5)', () => {
cleanupCounter();
@@ -297,33 +317,36 @@ function runTests() {
'Float threshold should be parseInt-ed to 3, no suggestion at count=50'
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('counter value at exact boundary 1000000 is valid', () => {
cleanupCounter();
fs.writeFileSync(counterFile, '999999');
const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' });
runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' });
// 999999 is valid (> 0, <= 1000000), count becomes 1000000
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1000000, 'Counter at 1000000 boundary should be valid');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('counter value at 1000001 is clamped (reset to 1)', () => {
cleanupCounter();
fs.writeFileSync(counterFile, '1000001');
const result = runCompact({ CLAUDE_SESSION_ID: testSession });
runCompact({ CLAUDE_SESSION_ID: testSession });
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Counter > 1000000 should be reset to 1');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// ── Round 64: default session ID fallback ──
console.log('\nDefault session ID fallback (Round 64):');
if (test('uses "default" session ID when CLAUDE_SESSION_ID is empty', () => {
const defaultCounterFile = getCounterFilePath('default');
try { fs.unlinkSync(defaultCounterFile); } catch {}
try { fs.unlinkSync(defaultCounterFile); } catch (_err) { /* ignore */ }
try {
// Pass empty CLAUDE_SESSION_ID — falsy, so script uses 'default'
const env = { ...process.env, CLAUDE_SESSION_ID: '' };
@@ -338,12 +361,14 @@ function runTests() {
const count = parseInt(fs.readFileSync(defaultCounterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Counter should be 1 for first run with default session');
} finally {
try { fs.unlinkSync(defaultCounterFile); } catch {}
try { fs.unlinkSync(defaultCounterFile); } catch (_err) { /* ignore */ }
}
})) passed++; else failed++;
})) passed++;
else failed++;
// Summary
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
console.log(`
Results: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}

View File

@@ -262,8 +262,13 @@ async function runTests() {
});
});
assert.ok(stderr.includes('BLOCKED'), 'Blocking hook should output BLOCKED');
assert.strictEqual(code, 2, 'Blocking hook should exit with code 2');
// Hook only blocks on non-Windows platforms (tmux is Unix-only)
if (process.platform === 'win32') {
assert.strictEqual(code, 0, 'On Windows, hook should not block (exit 0)');
} else {
assert.ok(stderr.includes('BLOCKED'), 'Blocking hook should output BLOCKED');
assert.strictEqual(code, 2, 'Blocking hook should exit with code 2');
}
})) passed++; else failed++;
// ==========================================
@@ -298,7 +303,12 @@ async function runTests() {
});
});
assert.strictEqual(code, 2, 'Blocking hook should exit 2');
// Hook only blocks on non-Windows platforms (tmux is Unix-only)
if (process.platform === 'win32') {
assert.strictEqual(code, 0, 'On Windows, hook should not block (exit 0)');
} else {
assert.strictEqual(code, 2, 'Blocking hook should exit 2');
}
})) passed++; else failed++;
if (await asyncTest('hooks handle missing files gracefully', async () => {

File diff suppressed because it is too large Load Diff

View File

@@ -787,7 +787,6 @@ function runTests() {
// Verify the file exists
const aliasesPath = path.join(tmpHome, '.claude', 'session-aliases.json');
assert.ok(fs.existsSync(aliasesPath), 'Aliases file should exist');
const contentBefore = fs.readFileSync(aliasesPath, 'utf8');
// Attempt to save circular data — will fail
const circular = { aliases: {}, metadata: {} };

View File

@@ -1124,7 +1124,7 @@ src/main.ts
} else {
delete process.env.USERPROFILE;
}
try { fs.rmSync(r33Home, { recursive: true, force: true }); } catch {}
try { fs.rmSync(r33Home, { recursive: true, force: true }); } catch (_e) { /* ignore cleanup errors */ }
// ── Round 46: path heuristic and checklist edge cases ──
console.log('\ngetSessionStats Windows path heuristic (Round 46):');
@@ -1488,6 +1488,27 @@ src/main.ts
'Content without session items should have 0 totalItems');
})) passed++; else failed++;
// Re-establish test environment for Rounds 95-98 (these tests need sessions to exist)
const tmpHome2 = path.join(os.tmpdir(), `ecc-session-mgr-test-2-${Date.now()}`);
const tmpSessionsDir2 = path.join(tmpHome2, '.claude', 'sessions');
fs.mkdirSync(tmpSessionsDir2, { recursive: true });
const origHome2 = process.env.HOME;
const origUserProfile2 = process.env.USERPROFILE;
// Create test session files for these tests
const testSessions2 = [
{ name: '2026-01-15-aaaa1111-session.tmp', content: '# Test Session 1' },
{ name: '2026-02-01-bbbb2222-session.tmp', content: '# Test Session 2' },
{ name: '2026-02-10-cccc3333-session.tmp', content: '# Test Session 3' },
];
for (const session of testSessions2) {
const filePath = path.join(tmpSessionsDir2, session.name);
fs.writeFileSync(filePath, session.content);
}
process.env.HOME = tmpHome2;
process.env.USERPROFILE = tmpHome2;
// ── Round 95: getAllSessions with both negative offset AND negative limit ──
console.log('\nRound 95: getAllSessions (both negative offset and negative limit):');
@@ -1579,6 +1600,20 @@ src/main.ts
);
})) passed++; else failed++;
// Cleanup test environment for Rounds 95-98 that needed sessions
// (Round 98: parseSessionFilename below doesn't need sessions)
process.env.HOME = origHome2;
if (origUserProfile2 !== undefined) {
process.env.USERPROFILE = origUserProfile2;
} else {
delete process.env.USERPROFILE;
}
try {
fs.rmSync(tmpHome2, { recursive: true, force: true });
} catch {
// best-effort
}
// ── Round 98: parseSessionFilename with null input throws TypeError ──
console.log('\nRound 98: parseSessionFilename (null input — crashes at line 30):');
@@ -1985,7 +2020,7 @@ file.ts
assert.ok(!afterContent.includes('Appended data'),
'Original content should be unchanged');
} finally {
try { fs.chmodSync(readOnlyFile, 0o644); } catch {}
try { fs.chmodSync(readOnlyFile, 0o644); } catch (_e) { /* ignore permission errors */ }
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
@@ -2329,6 +2364,7 @@ file.ts
if (test('getSessionById matches old format YYYY-MM-DD-session.tmp via noIdMatch path', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'r122-old-format-'));
const origHome = process.env.HOME;
const origUserProfile = process.env.USERPROFILE;
const origDir = process.env.CLAUDE_DIR;
try {
// Set up isolated environment
@@ -2336,6 +2372,7 @@ file.ts
const sessionsDir = path.join(claudeDir, 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
process.env.HOME = tmpDir;
process.env.USERPROFILE = tmpDir; // Windows: os.homedir() uses USERPROFILE
delete process.env.CLAUDE_DIR;
// Clear require cache for fresh module with new HOME
@@ -2361,6 +2398,8 @@ file.ts
'Non-matching date should return null');
} finally {
process.env.HOME = origHome;
if (origUserProfile !== undefined) process.env.USERPROFILE = origUserProfile;
else delete process.env.USERPROFILE;
if (origDir) process.env.CLAUDE_DIR = origDir;
delete require.cache[require.resolve('../../scripts/lib/utils')];
delete require.cache[require.resolve('../../scripts/lib/session-manager')];
@@ -2450,6 +2489,7 @@ file.ts
// "2026/01/15" or "Jan 15 2026" will never match, silently returning empty.
// No validation or normalization occurs on the date parameter.
const origHome = process.env.HOME;
const origUserProfile = process.env.USERPROFILE;
const origDir = process.env.CLAUDE_DIR;
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'r124-date-format-'));
const homeDir = path.join(tmpDir, 'home');
@@ -2457,6 +2497,7 @@ file.ts
try {
process.env.HOME = homeDir;
process.env.USERPROFILE = homeDir; // Windows: os.homedir() uses USERPROFILE
delete process.env.CLAUDE_DIR;
delete require.cache[require.resolve('../../scripts/lib/utils')];
delete require.cache[require.resolve('../../scripts/lib/session-manager')];
@@ -2495,6 +2536,8 @@ file.ts
'null date skips filter and returns all sessions');
} finally {
process.env.HOME = origHome;
if (origUserProfile !== undefined) process.env.USERPROFILE = origUserProfile;
else delete process.env.USERPROFILE;
if (origDir) process.env.CLAUDE_DIR = origDir;
delete require.cache[require.resolve('../../scripts/lib/utils')];
delete require.cache[require.resolve('../../scripts/lib/session-manager')];

View File

@@ -836,7 +836,8 @@ function runTests() {
console.log('\nrunCommand Edge Cases:');
if (test('runCommand returns trimmed output', () => {
const result = utils.runCommand('echo " hello "');
// Windows echo includes quotes in output, use node to ensure consistent behavior
const result = utils.runCommand('node -e "process.stdout.write(\' hello \')"');
assert.strictEqual(result.success, true);
assert.strictEqual(result.output, 'hello', 'Should trim leading/trailing whitespace');
})) passed++; else failed++;
@@ -884,6 +885,10 @@ function runTests() {
console.log('\nreadStdinJson maxSize truncation:');
if (test('readStdinJson maxSize stops accumulating after threshold (chunk-level guard)', () => {
if (process.platform === 'win32') {
console.log(' (skipped — stdin chunking behavior differs on Windows)');
return true;
}
const { execFileSync } = require('child_process');
// maxSize is a chunk-level guard: once data.length >= maxSize, no MORE chunks are added.
// A single small chunk that arrives when data.length < maxSize is added in full.
@@ -1678,6 +1683,10 @@ function runTests() {
// ── Round 110: findFiles root directory unreadable — silent empty return (not throw) ──
console.log('\nRound 110: findFiles (root directory unreadable — EACCES on readdirSync caught silently):');
if (test('findFiles returns empty array when root directory exists but is unreadable', () => {
if (process.platform === 'win32' || process.getuid?.() === 0) {
console.log(' (skipped — chmod ineffective on Windows/root)');
return true;
}
const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r110-unreadable-root-'));
const unreadableDir = path.join(tmpDir, 'no-read');
fs.mkdirSync(unreadableDir);
@@ -1697,7 +1706,7 @@ function runTests() {
'Recursive search on unreadable root should also return empty array');
} finally {
// Restore permissions before cleanup
try { fs.chmodSync(unreadableDir, 0o755); } catch {}
try { fs.chmodSync(unreadableDir, 0o755); } catch (_e) { /* ignore permission errors */ }
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;