mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 21:53:28 +08:00
Compare commits
204 Commits
2787b8e92f
...
fix/stop-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
284d2995ba | ||
|
|
dd675d4258 | ||
|
|
db12d3d838 | ||
|
|
46f37ae4fb | ||
|
|
0c166e14da | ||
|
|
527c79350c | ||
|
|
0ebcfc368e | ||
|
|
bec1ebf76d | ||
|
|
be76918850 | ||
|
|
99a154a908 | ||
|
|
ebf0f135bb | ||
|
|
2d27da52e2 | ||
|
|
65c4a0f6ba | ||
|
|
ab49c9adf5 | ||
|
|
b7a82cf240 | ||
|
|
9a55fd069b | ||
|
|
d9e8305aa1 | ||
|
|
f2bf72c005 | ||
|
|
3ae0df781f | ||
|
|
a346a304b0 | ||
|
|
81acf0c928 | ||
|
|
06a77911e6 | ||
|
|
9406f35fab | ||
|
|
c5e3658ba6 | ||
|
|
eeeea506a6 | ||
|
|
fc1ea4fbea | ||
|
|
00787d68e4 | ||
|
|
1e3572becf | ||
|
|
7462168377 | ||
|
|
3c3781ca43 | ||
|
|
27d71c9548 | ||
|
|
6f16e75f9d | ||
|
|
0d30da1fc7 | ||
|
|
e686bcbc82 | ||
|
|
25c8a5de08 | ||
|
|
ec104c94c5 | ||
|
|
14a51404c0 | ||
|
|
666c639206 | ||
|
|
a8e088a54e | ||
|
|
eac0228f88 | ||
|
|
b6e3434ff4 | ||
|
|
4eaee83448 | ||
|
|
1e43639cc7 | ||
|
|
766f846478 | ||
|
|
dd38518afe | ||
|
|
c1d98b071e | ||
|
|
70b98f3178 | ||
|
|
dcc4d914d2 | ||
|
|
71219ff656 | ||
|
|
e815f0d05c | ||
|
|
b3a43f34e6 | ||
|
|
0d26f5295d | ||
|
|
9181382065 | ||
|
|
9434e07749 | ||
|
|
9cde3427e2 | ||
|
|
c6b4c719b2 | ||
|
|
f98207feea | ||
|
|
52e9bd58f1 | ||
|
|
4257c093ca | ||
|
|
23d743b92c | ||
|
|
414ea90e11 | ||
|
|
d473cf87e6 | ||
|
|
64847d0a21 | ||
|
|
c865d4c676 | ||
|
|
72de19effd | ||
|
|
56076edd48 | ||
|
|
04d7eeb16f | ||
|
|
4e7773c2ce | ||
|
|
a3fc90f7ac | ||
|
|
55efeb7f20 | ||
|
|
1e7c299706 | ||
|
|
47aa415b06 | ||
|
|
d7e6bb242a | ||
|
|
9f37a5d8c7 | ||
|
|
d9ec51c9e9 | ||
|
|
9033f2a997 | ||
|
|
67660540ac | ||
|
|
432788d0b5 | ||
|
|
6a7a115e18 | ||
|
|
1181d93498 | ||
|
|
80d6a89f12 | ||
|
|
28a1fbc3f2 | ||
|
|
4fcaaf8a89 | ||
|
|
7a4cb8c570 | ||
|
|
4b4f077d18 | ||
|
|
78c98dd4fd | ||
|
|
f07797533d | ||
|
|
87d883eb1b | ||
|
|
652f87c5b6 | ||
|
|
70b65a9d06 | ||
|
|
24674a7bd6 | ||
|
|
d49c95a5ec | ||
|
|
70a96bd363 | ||
|
|
8f7445a260 | ||
|
|
9ad4351f53 | ||
|
|
451732164f | ||
|
|
ebd14cde7d | ||
|
|
ae21a8df85 | ||
|
|
d8e3b9d593 | ||
|
|
7148d9006f | ||
|
|
c14765e701 | ||
|
|
194bc0000b | ||
|
|
1e44475458 | ||
|
|
31af1adcc8 | ||
|
|
c80631fc1d | ||
|
|
00f8628b83 | ||
|
|
ba09a34432 | ||
|
|
27e0d53f6d | ||
|
|
8b6140dedc | ||
|
|
7633386e04 | ||
|
|
b4296c7095 | ||
|
|
17f6f95090 | ||
|
|
1e226ba556 | ||
|
|
cc60bf6b65 | ||
|
|
160624d0ed | ||
|
|
73c10122fe | ||
|
|
9b24bedf85 | ||
|
|
e3f2bda9fc | ||
|
|
fe6a6fc106 | ||
|
|
63737544a1 | ||
|
|
dafc9bcd60 | ||
|
|
2d0fddf174 | ||
|
|
f471f27658 | ||
|
|
925d830c53 | ||
|
|
2243f15581 | ||
|
|
6408511611 | ||
|
|
9348751b8e | ||
|
|
c96c4d2742 | ||
|
|
da74f85c10 | ||
|
|
c146fae2ce | ||
|
|
3f5e042b40 | ||
|
|
b5148f184a | ||
|
|
b44ba7096f | ||
|
|
45baaa1ea5 | ||
|
|
4da1fb388c | ||
|
|
917c35bb6f | ||
|
|
ee3f348dcb | ||
|
|
e6eb99271f | ||
|
|
7cabf77142 | ||
|
|
9cfcfac665 | ||
|
|
0284f60871 | ||
|
|
7a17ec9b14 | ||
|
|
243fae8476 | ||
|
|
dc92b5c62b | ||
|
|
3fbfd7f7ff | ||
|
|
a6a81490f6 | ||
|
|
d170cdd175 | ||
|
|
57e9983c88 | ||
|
|
d952a07c73 | ||
|
|
369f66297a | ||
|
|
9cc5d085e1 | ||
|
|
678fb6f0d3 | ||
|
|
401e26a45a | ||
|
|
eb934afbb5 | ||
|
|
8303970258 | ||
|
|
319f9efafb | ||
|
|
6c2a3a2bae | ||
|
|
adaeab9dba | ||
|
|
8981dd6067 | ||
|
|
7229e09df1 | ||
|
|
4105a2f36c | ||
|
|
0166231ddb | ||
|
|
cf439dd481 | ||
|
|
9903ae528b | ||
|
|
44c2bf6f7b | ||
|
|
e78c092499 | ||
|
|
61f70de479 | ||
|
|
776ac439f3 | ||
|
|
b19b4c6b5e | ||
|
|
b5157f4ed1 | ||
|
|
2d1e384eef | ||
|
|
9c5ca92e6e | ||
|
|
7b510c886e | ||
|
|
c1b47ac9db | ||
|
|
3f02fa439a | ||
|
|
f6b10481f3 | ||
|
|
d3699f9010 | ||
|
|
445ae5099d | ||
|
|
00bc7f30be | ||
|
|
1d0aa5ac2a | ||
|
|
7f7e319d9f | ||
|
|
d7bcc92007 | ||
|
|
e883385ab0 | ||
|
|
e7d827548c | ||
|
|
bf7ed1fce2 | ||
|
|
fee93f2dab | ||
|
|
a61947bb5c | ||
|
|
3c59d8dc60 | ||
|
|
46f6e3644b | ||
|
|
39a34e46db | ||
|
|
95a1435f61 | ||
|
|
e57ad5c33d | ||
|
|
f7d589ce21 | ||
|
|
9c381b4469 | ||
|
|
e3510f62a8 | ||
|
|
7726c25e46 | ||
|
|
6af7ca1afc | ||
|
|
d6061cf937 | ||
|
|
ec921e5202 | ||
|
|
d016e68cee | ||
|
|
aed18eb571 | ||
|
|
f3cf808814 | ||
|
|
e22cb57718 | ||
|
|
4811e8c73b |
20
.agents/plugins/marketplace.json
Normal file
20
.agents/plugins/marketplace.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "everything-claude-code",
|
||||
"interface": {
|
||||
"displayName": "Everything Claude Code"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "everything-claude-code",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "../.."
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -21,5 +21,37 @@
|
||||
"workflow",
|
||||
"automation",
|
||||
"best-practices"
|
||||
]
|
||||
],
|
||||
"agents": [
|
||||
"./agents/architect.md",
|
||||
"./agents/build-error-resolver.md",
|
||||
"./agents/chief-of-staff.md",
|
||||
"./agents/code-reviewer.md",
|
||||
"./agents/cpp-build-resolver.md",
|
||||
"./agents/cpp-reviewer.md",
|
||||
"./agents/database-reviewer.md",
|
||||
"./agents/doc-updater.md",
|
||||
"./agents/docs-lookup.md",
|
||||
"./agents/e2e-runner.md",
|
||||
"./agents/flutter-reviewer.md",
|
||||
"./agents/go-build-resolver.md",
|
||||
"./agents/go-reviewer.md",
|
||||
"./agents/harness-optimizer.md",
|
||||
"./agents/java-build-resolver.md",
|
||||
"./agents/java-reviewer.md",
|
||||
"./agents/kotlin-build-resolver.md",
|
||||
"./agents/kotlin-reviewer.md",
|
||||
"./agents/loop-operator.md",
|
||||
"./agents/planner.md",
|
||||
"./agents/python-reviewer.md",
|
||||
"./agents/pytorch-build-resolver.md",
|
||||
"./agents/refactor-cleaner.md",
|
||||
"./agents/rust-build-resolver.md",
|
||||
"./agents/rust-reviewer.md",
|
||||
"./agents/security-reviewer.md",
|
||||
"./agents/tdd-guide.md",
|
||||
"./agents/typescript-reviewer.md"
|
||||
],
|
||||
"skills": ["./skills/"],
|
||||
"commands": ["./commands/"]
|
||||
}
|
||||
|
||||
47
.claude/rules/node.md
Normal file
47
.claude/rules/node.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Node.js Rules for everything-claude-code
|
||||
|
||||
> Project-specific rules for the ECC codebase. Extends common rules.
|
||||
|
||||
## Stack
|
||||
|
||||
- **Runtime**: Node.js >=18 (no transpilation, plain CommonJS)
|
||||
- **Test runner**: `node tests/run-all.js` — individual files via `node tests/**/*.test.js`
|
||||
- **Linter**: ESLint (`@eslint/js`, flat config)
|
||||
- **Coverage**: c8
|
||||
- **Lint**: markdownlint-cli for `.md` files
|
||||
|
||||
## File Conventions
|
||||
|
||||
- `scripts/` — Node.js utilities, hooks. CommonJS (`require`/`module.exports`)
|
||||
- `agents/`, `commands/`, `skills/`, `rules/` — Markdown with YAML frontmatter
|
||||
- `tests/` — Mirror the `scripts/` structure. Test files named `*.test.js`
|
||||
- File naming: **lowercase with hyphens** (e.g. `session-start.js`, `post-edit-format.js`)
|
||||
|
||||
## Code Style
|
||||
|
||||
- CommonJS only — no ESM (`import`/`export`) unless file ends in `.mjs`
|
||||
- No TypeScript — plain `.js` throughout
|
||||
- Prefer `const` over `let`; never `var`
|
||||
- Keep hook scripts under 200 lines — extract helpers to `scripts/lib/`
|
||||
- All hooks must `exit 0` on non-critical errors (never block tool execution unexpectedly)
|
||||
|
||||
## Hook Development
|
||||
|
||||
- Hook scripts normally receive JSON on stdin, but hooks routed through `scripts/hooks/run-with-flags.js` can export `run(rawInput)` and let the wrapper handle parsing/gating
|
||||
- Async hooks: mark `"async": true` in `settings.json` with a timeout ≤30s
|
||||
- Blocking hooks (PreToolUse, stop): keep fast (<200ms) — no network calls
|
||||
- Use `run-with-flags.js` wrapper for all hooks so `ECC_HOOK_PROFILE` and `ECC_DISABLED_HOOKS` runtime gating works
|
||||
- Always exit 0 on parse errors; log to stderr with `[HookName]` prefix
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
- Run `node tests/run-all.js` before committing
|
||||
- New scripts in `scripts/lib/` require a matching test in `tests/lib/`
|
||||
- New hooks require at least one integration test in `tests/hooks/`
|
||||
|
||||
## Markdown / Agent Files
|
||||
|
||||
- Agents: YAML frontmatter with `name`, `description`, `tools`, `model`
|
||||
- Skills: sections — When to Use, How It Works, Examples
|
||||
- Commands: `description:` frontmatter line required
|
||||
- Run `npx markdownlint-cli '**/*.md' --ignore node_modules` before committing
|
||||
49
.codex-plugin/README.md
Normal file
49
.codex-plugin/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# .codex-plugin — Codex Native Plugin for ECC
|
||||
|
||||
This directory contains the **Codex plugin manifest** for Everything Claude Code.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
.codex-plugin/
|
||||
└── plugin.json — Codex plugin manifest (name, version, skills ref, MCP ref)
|
||||
.mcp.json — MCP server configurations at plugin root (NOT inside .codex-plugin/)
|
||||
```
|
||||
|
||||
## What This Provides
|
||||
|
||||
- **125 skills** from `./skills/` — reusable Codex workflows for TDD, security,
|
||||
code review, architecture, and more
|
||||
- **6 MCP servers** — GitHub, Context7, Exa, Memory, Playwright, Sequential Thinking
|
||||
|
||||
## Installation
|
||||
|
||||
Codex plugin support is currently in preview. Once generally available:
|
||||
|
||||
```bash
|
||||
# Install from Codex CLI
|
||||
codex plugin install affaan-m/everything-claude-code
|
||||
|
||||
# Or reference locally during development
|
||||
codex plugin install ./
|
||||
|
||||
Run this from the repository root so `./` points to the repo root and `.mcp.json` resolves correctly.
|
||||
```
|
||||
|
||||
## MCP Servers Included
|
||||
|
||||
| Server | Purpose |
|
||||
|---|---|
|
||||
| `github` | GitHub API access |
|
||||
| `context7` | Live documentation lookup |
|
||||
| `exa` | Neural web search |
|
||||
| `memory` | Persistent memory across sessions |
|
||||
| `playwright` | Browser automation & E2E testing |
|
||||
| `sequential-thinking` | Step-by-step reasoning |
|
||||
|
||||
## Notes
|
||||
|
||||
- The `skills/` directory at the repo root is shared between Claude Code (`.claude-plugin/`)
|
||||
and Codex (`.codex-plugin/`) — same source of truth, no duplication
|
||||
- MCP server credentials are inherited from the launching environment (env vars)
|
||||
- This manifest does **not** override `~/.codex/config.toml` settings
|
||||
30
.codex-plugin/plugin.json
Normal file
30
.codex-plugin/plugin.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "everything-claude-code",
|
||||
"version": "1.9.0",
|
||||
"description": "Battle-tested Codex workflows — 125 skills, production-ready MCP configs, and agent definitions for TDD, security scanning, code review, and autonomous development.",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
"email": "me@affaanmustafa.com",
|
||||
"url": "https://x.com/affaanmustafa"
|
||||
},
|
||||
"homepage": "https://github.com/affaan-m/everything-claude-code",
|
||||
"repository": "https://github.com/affaan-m/everything-claude-code",
|
||||
"license": "MIT",
|
||||
"keywords": ["codex", "agents", "skills", "tdd", "code-review", "security", "workflow", "automation"],
|
||||
"skills": "./skills/",
|
||||
"mcpServers": "./.mcp.json",
|
||||
"interface": {
|
||||
"displayName": "Everything Claude Code",
|
||||
"shortDescription": "125 battle-tested skills for TDD, security, code review, and autonomous development.",
|
||||
"longDescription": "Everything Claude Code (ECC) is a community-maintained collection of Codex skills and MCP configs evolved over 10+ months of intensive daily use. It covers TDD workflows, security scanning, code review, architecture decisions, and more — all in one installable plugin.",
|
||||
"developerName": "Affaan Mustafa",
|
||||
"category": "Productivity",
|
||||
"capabilities": ["Read", "Write"],
|
||||
"websiteURL": "https://github.com/affaan-m/everything-claude-code",
|
||||
"defaultPrompt": [
|
||||
"Use the tdd-workflow skill to write tests before implementation.",
|
||||
"Use the security-review skill to scan for OWASP Top 10 vulnerabilities.",
|
||||
"Use the code-review skill to review this PR for correctness and security."
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -46,12 +46,15 @@ Available skills:
|
||||
|
||||
Treat the project-local `.codex/config.toml` as the default Codex baseline for ECC. The current ECC baseline enables GitHub, Context7, Exa, Memory, Playwright, and Sequential Thinking; add heavier extras in `~/.codex/config.toml` only when a task actually needs them.
|
||||
|
||||
ECC's canonical Codex section name is `[mcp_servers.context7]`. The launcher package remains `@upstash/context7-mcp`; only the TOML section name is normalized for consistency with `codex mcp list` and the reference config.
|
||||
|
||||
### Automatic config.toml merging
|
||||
|
||||
The sync script (`scripts/sync-ecc-to-codex.sh`) uses a Node-based TOML parser to safely merge ECC MCP servers into `~/.codex/config.toml`:
|
||||
|
||||
- **Add-only by default** — missing ECC servers are appended; existing servers are never modified or removed.
|
||||
- **7 managed servers** — Supabase, Playwright, Context7, Exa, GitHub, Memory, Sequential Thinking.
|
||||
- **Canonical naming** — ECC manages Context7 as `[mcp_servers.context7]`; legacy `[mcp_servers.context7-mcp]` entries are treated as aliases during updates.
|
||||
- **Package-manager aware** — uses the project's configured package manager (npm/pnpm/yarn/bun) instead of hardcoding `pnpm`.
|
||||
- **Drift warnings** — if an existing server's config differs from the ECC recommendation, the script logs a warning.
|
||||
- **`--update-mcp`** — explicitly replaces all ECC-managed servers with the latest recommended config (safely removes subtables like `[mcp_servers.supabase.env]`).
|
||||
|
||||
@@ -27,7 +27,10 @@ notify = [
|
||||
"-sound", "default",
|
||||
]
|
||||
|
||||
# Prefer AGENTS.md and project-local .codex/AGENTS.md for instructions.
|
||||
# Persistent instructions are appended to every prompt (additive, unlike
|
||||
# model_instructions_file which replaces AGENTS.md).
|
||||
persistent_instructions = "Follow project AGENTS.md guidelines. Use available MCP servers when they can help."
|
||||
|
||||
# model_instructions_file replaces built-in instructions instead of AGENTS.md,
|
||||
# so leave it unset unless you intentionally want a single override file.
|
||||
# model_instructions_file = "/absolute/path/to/instructions.md"
|
||||
@@ -38,10 +41,14 @@ notify = [
|
||||
[mcp_servers.github]
|
||||
command = "npx"
|
||||
args = ["-y", "@modelcontextprotocol/server-github"]
|
||||
startup_timeout_sec = 30
|
||||
|
||||
[mcp_servers.context7]
|
||||
command = "npx"
|
||||
# Canonical Codex section name is `context7`; the package itself remains
|
||||
# `@upstash/context7-mcp`.
|
||||
args = ["-y", "@upstash/context7-mcp@latest"]
|
||||
startup_timeout_sec = 30
|
||||
|
||||
[mcp_servers.exa]
|
||||
url = "https://mcp.exa.ai/mcp"
|
||||
@@ -49,14 +56,17 @@ url = "https://mcp.exa.ai/mcp"
|
||||
[mcp_servers.memory]
|
||||
command = "npx"
|
||||
args = ["-y", "@modelcontextprotocol/server-memory"]
|
||||
startup_timeout_sec = 30
|
||||
|
||||
[mcp_servers.playwright]
|
||||
command = "npx"
|
||||
args = ["-y", "@playwright/mcp@latest", "--extension"]
|
||||
startup_timeout_sec = 30
|
||||
|
||||
[mcp_servers.sequential-thinking]
|
||||
command = "npx"
|
||||
args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]
|
||||
startup_timeout_sec = 30
|
||||
|
||||
# Additional MCP servers (uncomment as needed):
|
||||
# [mcp_servers.supabase]
|
||||
@@ -76,7 +86,8 @@ args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]
|
||||
# args = ["-y", "@cloudflare/mcp-server-cloudflare"]
|
||||
|
||||
[features]
|
||||
# Codex multi-agent support is experimental as of March 2026.
|
||||
# Codex multi-agent collaboration is stable and on by default in current builds.
|
||||
# Keep the explicit toggle here so the repo documents its expectation clearly.
|
||||
multi_agent = true
|
||||
|
||||
# Profiles — switch with `codex -p <name>`
|
||||
@@ -91,6 +102,9 @@ sandbox_mode = "workspace-write"
|
||||
web_search = "live"
|
||||
|
||||
[agents]
|
||||
[agents]
|
||||
# Multi-agent role limits and local role definitions.
|
||||
# These map to `.codex/agents/*.toml` and mirror the repo's explorer/reviewer/docs workflow.
|
||||
max_threads = 6
|
||||
max_depth = 1
|
||||
|
||||
|
||||
51
.github/workflows/ci.yml
vendored
51
.github/workflows/ci.yml
vendored
@@ -34,23 +34,30 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
# Package manager setup
|
||||
- name: Setup pnpm
|
||||
if: matrix.pm == 'pnpm'
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Setup Yarn (via Corepack)
|
||||
if: matrix.pm == 'yarn'
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare yarn@stable --activate
|
||||
|
||||
- name: Setup Bun
|
||||
if: matrix.pm == 'bun'
|
||||
uses: oven-sh/setup-bun@v2
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
|
||||
# Cache configuration
|
||||
- name: Get npm cache directory
|
||||
@@ -61,7 +68,7 @@ jobs:
|
||||
|
||||
- name: Cache npm
|
||||
if: matrix.pm == 'npm'
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ matrix.node }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -76,7 +83,7 @@ jobs:
|
||||
|
||||
- name: Cache pnpm
|
||||
if: matrix.pm == 'pnpm'
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ matrix.node }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
@@ -97,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Cache yarn
|
||||
if: matrix.pm == 'yarn'
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ matrix.node }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
@@ -106,7 +113,7 @@ jobs:
|
||||
|
||||
- name: Cache bun
|
||||
if: matrix.pm == 'bun'
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
|
||||
@@ -114,14 +121,18 @@ jobs:
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
# Install dependencies
|
||||
# COREPACK_ENABLE_STRICT=0 allows pnpm to install even though
|
||||
# package.json declares "packageManager": "yarn@..."
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
env:
|
||||
COREPACK_ENABLE_STRICT: '0'
|
||||
run: |
|
||||
case "${{ matrix.pm }}" in
|
||||
npm) npm ci ;;
|
||||
pnpm) pnpm install ;;
|
||||
# --ignore-engines required for Node 18 compat with some devDependencies (e.g., markdownlint-cli)
|
||||
yarn) yarn install --ignore-engines ;;
|
||||
pnpm) pnpm install --no-frozen-lockfile ;;
|
||||
# Yarn Berry (v4+) removed --ignore-engines; engine checking is no longer a core feature
|
||||
yarn) yarn install ;;
|
||||
bun) bun install ;;
|
||||
*) echo "Unsupported package manager: ${{ matrix.pm }}" && exit 1 ;;
|
||||
esac
|
||||
@@ -135,7 +146,7 @@ jobs:
|
||||
# Upload test artifacts on failure
|
||||
- name: Upload test artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: test-results-${{ matrix.os }}-node${{ matrix.node }}-${{ matrix.pm }}
|
||||
path: |
|
||||
@@ -149,10 +160,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
@@ -175,6 +186,10 @@ jobs:
|
||||
run: node scripts/ci/validate-skills.js
|
||||
continue-on-error: false
|
||||
|
||||
- name: Validate install manifests
|
||||
run: node scripts/ci/validate-install-manifests.js
|
||||
continue-on-error: false
|
||||
|
||||
- name: Validate rules
|
||||
run: node scripts/ci/validate-rules.js
|
||||
continue-on-error: false
|
||||
@@ -190,10 +205,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
@@ -208,10 +223,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
|
||||
10
.github/workflows/maintenance.yml
vendored
10
.github/workflows/maintenance.yml
vendored
@@ -15,8 +15,8 @@ jobs:
|
||||
name: Check Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
- name: Check for outdated packages
|
||||
@@ -26,8 +26,8 @@ jobs:
|
||||
name: Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
- name: Run security audit
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
name: Stale Issues/PRs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale due to inactivity.'
|
||||
stale-pr-message: 'This PR is stale due to inactivity.'
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -14,17 +14,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate version tag
|
||||
run: |
|
||||
if ! [[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
if ! [[ "${REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Invalid version tag format. Expected vX.Y.Z"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
- name: Verify plugin.json version matches tag
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
@@ -61,7 +63,7 @@ jobs:
|
||||
EOF
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
|
||||
with:
|
||||
body_path: release_body.md
|
||||
generate_release_notes: true
|
||||
|
||||
4
.github/workflows/reusable-release.yml
vendored
4
.github/workflows/reusable-release.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
EOF
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
|
||||
with:
|
||||
tag_name: ${{ inputs.tag }}
|
||||
body_path: release_body.md
|
||||
|
||||
34
.github/workflows/reusable-test.yml
vendored
34
.github/workflows/reusable-test.yml
vendored
@@ -27,22 +27,29 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Setup pnpm
|
||||
if: inputs.package-manager == 'pnpm'
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Setup Yarn (via Corepack)
|
||||
if: inputs.package-manager == 'yarn'
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare yarn@stable --activate
|
||||
|
||||
- name: Setup Bun
|
||||
if: inputs.package-manager == 'bun'
|
||||
uses: oven-sh/setup-bun@v2
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
|
||||
- name: Get npm cache directory
|
||||
if: inputs.package-manager == 'npm'
|
||||
@@ -52,7 +59,7 @@ jobs:
|
||||
|
||||
- name: Cache npm
|
||||
if: inputs.package-manager == 'npm'
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ inputs.node-version }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -67,7 +74,7 @@ jobs:
|
||||
|
||||
- name: Cache pnpm
|
||||
if: inputs.package-manager == 'pnpm'
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ inputs.node-version }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
@@ -88,7 +95,7 @@ jobs:
|
||||
|
||||
- name: Cache yarn
|
||||
if: inputs.package-manager == 'yarn'
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ inputs.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
@@ -97,20 +104,25 @@ jobs:
|
||||
|
||||
- name: Cache bun
|
||||
if: inputs.package-manager == 'bun'
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
# COREPACK_ENABLE_STRICT=0 allows pnpm to install even though
|
||||
# package.json declares "packageManager": "yarn@..."
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
env:
|
||||
COREPACK_ENABLE_STRICT: '0'
|
||||
run: |
|
||||
case "${{ inputs.package-manager }}" in
|
||||
npm) npm ci ;;
|
||||
pnpm) pnpm install ;;
|
||||
yarn) yarn install --ignore-engines ;;
|
||||
pnpm) pnpm install --no-frozen-lockfile ;;
|
||||
# Yarn Berry (v4+) removed --ignore-engines; engine checking is no longer a core feature
|
||||
yarn) yarn install ;;
|
||||
bun) bun install ;;
|
||||
*) echo "Unsupported package manager: ${{ inputs.package-manager }}" && exit 1 ;;
|
||||
esac
|
||||
@@ -122,7 +134,7 @@ jobs:
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: test-results-${{ inputs.os }}-node${{ inputs.node-version }}-${{ inputs.package-manager }}
|
||||
path: |
|
||||
|
||||
7
.github/workflows/reusable-validate.yml
vendored
7
.github/workflows/reusable-validate.yml
vendored
@@ -17,10 +17,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
@@ -39,5 +39,8 @@ jobs:
|
||||
- name: Validate skills
|
||||
run: node scripts/ci/validate-skills.js
|
||||
|
||||
- name: Validate install manifests
|
||||
run: node scripts/ci/validate-install-manifests.js
|
||||
|
||||
- name: Validate rules
|
||||
run: node scripts/ci/validate-rules.js
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -83,6 +83,9 @@ temp/
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
# Observer temp files (continuous-learning-v2)
|
||||
.observer-tmp/
|
||||
|
||||
# Rust build artifacts
|
||||
ecc2/target/
|
||||
|
||||
|
||||
27
.mcp.json
Normal file
27
.mcp.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"]
|
||||
},
|
||||
"context7": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@upstash/context7-mcp@2.1.4"]
|
||||
},
|
||||
"exa": {
|
||||
"url": "https://mcp.exa.ai/mcp"
|
||||
},
|
||||
"memory": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-memory"]
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@playwright/mcp@0.0.68", "--extension"]
|
||||
},
|
||||
"sequential-thinking": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]
|
||||
}
|
||||
}
|
||||
}
|
||||
184
.trae/README.md
Normal file
184
.trae/README.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Everything Claude Code for Trae
|
||||
|
||||
Bring Everything Claude Code (ECC) workflows to Trae IDE. This repository provides custom commands, agents, skills, and rules that can be installed into any Trae project with a single command.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Local Installation (Current Project Only)
|
||||
|
||||
```bash
|
||||
# Install to current project
|
||||
cd /path/to/your/project
|
||||
TRAE_ENV=cn .trae/install.sh
|
||||
```
|
||||
|
||||
This creates `.trae-cn/` in your project directory.
|
||||
|
||||
### Option 2: Global Installation (All Projects)
|
||||
|
||||
```bash
|
||||
# Install globally to ~/.trae-cn/
|
||||
cd /path/to/your/project
|
||||
TRAE_ENV=cn .trae/install.sh ~
|
||||
|
||||
# Or from the .trae folder directly
|
||||
cd /path/to/your/project/.trae
|
||||
TRAE_ENV=cn ./install.sh ~
|
||||
```
|
||||
|
||||
This creates `~/.trae-cn/` which applies to all Trae projects.
|
||||
|
||||
### Option 3: Quick Install to Current Directory
|
||||
|
||||
```bash
|
||||
# If already in project directory with .trae folder
|
||||
cd .trae
|
||||
./install.sh
|
||||
```
|
||||
|
||||
The installer uses non-destructive copy - it will not overwrite your existing files.
|
||||
|
||||
## Installation Modes
|
||||
|
||||
### Local Installation
|
||||
|
||||
Install to the current project's `.trae-cn` directory:
|
||||
|
||||
```bash
|
||||
cd /path/to/your/project
|
||||
TRAE_ENV=cn .trae/install.sh
|
||||
```
|
||||
|
||||
This creates `/path/to/your/project/.trae-cn/` with all ECC components.
|
||||
|
||||
### Global Installation
|
||||
|
||||
Install to your home directory's `.trae-cn` directory (applies to all Trae projects):
|
||||
|
||||
```bash
|
||||
# From project directory
|
||||
TRAE_ENV=cn .trae/install.sh ~
|
||||
|
||||
# Or directly from .trae folder
|
||||
cd .trae
|
||||
TRAE_ENV=cn ./install.sh ~
|
||||
```
|
||||
|
||||
This creates `~/.trae-cn/` with all ECC components. All Trae projects will use these global installations.
|
||||
|
||||
**Note**: Global installation is useful when you want to maintain a single copy of ECC across all your projects.
|
||||
|
||||
## Environment Support
|
||||
|
||||
- **Default**: Uses `.trae` directory
|
||||
- **CN Environment**: Uses `.trae-cn` directory (set via `TRAE_ENV=cn`)
|
||||
|
||||
### Force Environment
|
||||
|
||||
```bash
|
||||
# From project root, force the CN environment
|
||||
TRAE_ENV=cn .trae/install.sh
|
||||
|
||||
# From inside the .trae folder
|
||||
cd .trae
|
||||
TRAE_ENV=cn ./install.sh
|
||||
```
|
||||
|
||||
**Note**: `TRAE_ENV` is a global environment variable that applies to the entire installation session.
|
||||
|
||||
## Uninstall
|
||||
|
||||
The uninstaller uses a manifest file (`.ecc-manifest`) to track installed files, ensuring safe removal:
|
||||
|
||||
```bash
|
||||
# Uninstall from current directory (if already inside .trae or .trae-cn)
|
||||
cd .trae-cn
|
||||
./uninstall.sh
|
||||
|
||||
# Or uninstall from project root
|
||||
cd /path/to/your/project
|
||||
TRAE_ENV=cn .trae/uninstall.sh
|
||||
|
||||
# Uninstall globally from home directory
|
||||
TRAE_ENV=cn .trae/uninstall.sh ~
|
||||
|
||||
# Will ask for confirmation before uninstalling
|
||||
```
|
||||
|
||||
### Uninstall Behavior
|
||||
|
||||
- **Safe removal**: Only removes files tracked in the manifest (installed by ECC)
|
||||
- **User files preserved**: Any files you added manually are kept
|
||||
- **Non-empty directories**: Directories containing user-added files are skipped
|
||||
- **Manifest-based**: Requires `.ecc-manifest` file (created during install)
|
||||
|
||||
### Environment Support
|
||||
|
||||
Uninstall respects the same `TRAE_ENV` environment variable as install:
|
||||
|
||||
```bash
|
||||
# Uninstall from .trae-cn (CN environment)
|
||||
TRAE_ENV=cn ./uninstall.sh
|
||||
|
||||
# Uninstall from .trae (default environment)
|
||||
./uninstall.sh
|
||||
```
|
||||
|
||||
**Note**: If no manifest file is found (old installation), the uninstaller will ask whether to remove the entire directory.
|
||||
|
||||
## What's Included
|
||||
|
||||
### Commands
|
||||
|
||||
Commands are on-demand workflows invocable via the `/` menu in Trae chat. All commands are reused directly from the project root's `commands/` folder.
|
||||
|
||||
### Agents
|
||||
|
||||
Agents are specialized AI assistants with specific tool configurations. All agents are reused directly from the project root's `agents/` folder.
|
||||
|
||||
### Skills
|
||||
|
||||
Skills are on-demand workflows invocable via the `/` menu in chat. All skills are reused directly from the project's `skills/` folder.
|
||||
|
||||
### Rules
|
||||
|
||||
Rules provide always-on rules and context that shape how the agent works with your code. All rules are reused directly from the project root's `rules/` folder.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Type `/` in chat to open the commands menu
|
||||
2. Select a command or skill
|
||||
3. The agent will guide you through the workflow with specific instructions and checklists
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.trae/ (or .trae-cn/)
|
||||
├── commands/ # Command files (reused from project root)
|
||||
├── agents/ # Agent files (reused from project root)
|
||||
├── skills/ # Skill files (reused from skills/)
|
||||
├── rules/ # Rule files (reused from project root)
|
||||
├── install.sh # Install script
|
||||
├── uninstall.sh # Uninstall script
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
All files are yours to modify after installation. The installer never overwrites existing files, so your customizations are safe across re-installs.
|
||||
|
||||
**Note**: The `install.sh` and `uninstall.sh` scripts are automatically copied to the target directory during installation, so you can run these commands directly from your project.
|
||||
|
||||
## Recommended Workflow
|
||||
|
||||
1. **Start with planning**: Use `/plan` command to break down complex features
|
||||
2. **Write tests first**: Invoke `/tdd` command before implementing
|
||||
3. **Review your code**: Use `/code-review` after writing code
|
||||
4. **Check security**: Use `/code-review` again for auth, API endpoints, or sensitive data handling
|
||||
5. **Fix build errors**: Use `/build-fix` if there are build errors
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Open your project in Trae
|
||||
- Type `/` to see available commands
|
||||
- Enjoy the ECC workflows!
|
||||
192
.trae/README.zh-CN.md
Normal file
192
.trae/README.zh-CN.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Everything Claude Code for Trae
|
||||
|
||||
为 Trae IDE 带来 Everything Claude Code (ECC) 工作流。此仓库提供自定义命令、智能体、技能和规则,可以通过单个命令安装到任何 Trae 项目中。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 方式一:本地安装到 `.trae` 目录(默认环境)
|
||||
|
||||
```bash
|
||||
# 安装到当前项目的 .trae 目录
|
||||
cd /path/to/your/project
|
||||
.trae/install.sh
|
||||
```
|
||||
|
||||
这将在您的项目目录中创建 `.trae/`。
|
||||
|
||||
### 方式二:本地安装到 `.trae-cn` 目录(CN 环境)
|
||||
|
||||
```bash
|
||||
# 安装到当前项目的 .trae-cn 目录
|
||||
cd /path/to/your/project
|
||||
TRAE_ENV=cn .trae/install.sh
|
||||
```
|
||||
|
||||
这将在您的项目目录中创建 `.trae-cn/`。
|
||||
|
||||
### 方式三:全局安装到 `~/.trae` 目录(默认环境)
|
||||
|
||||
```bash
|
||||
# 全局安装到 ~/.trae/
|
||||
cd /path/to/your/project
|
||||
.trae/install.sh ~
|
||||
```
|
||||
|
||||
这将创建 `~/.trae/`,适用于所有 Trae 项目。
|
||||
|
||||
### 方式四:全局安装到 `~/.trae-cn` 目录(CN 环境)
|
||||
|
||||
```bash
|
||||
# 全局安装到 ~/.trae-cn/
|
||||
cd /path/to/your/project
|
||||
TRAE_ENV=cn .trae/install.sh ~
|
||||
```
|
||||
|
||||
这将创建 `~/.trae-cn/`,适用于所有 Trae 项目。
|
||||
|
||||
安装程序使用非破坏性复制 - 它不会覆盖您现有的文件。
|
||||
|
||||
## 安装模式
|
||||
|
||||
### 本地安装
|
||||
|
||||
安装到当前项目的 `.trae` 或 `.trae-cn` 目录:
|
||||
|
||||
```bash
|
||||
# 安装到当前项目的 .trae 目录(默认)
|
||||
cd /path/to/your/project
|
||||
.trae/install.sh
|
||||
|
||||
# 安装到当前项目的 .trae-cn 目录(CN 环境)
|
||||
cd /path/to/your/project
|
||||
TRAE_ENV=cn .trae/install.sh
|
||||
```
|
||||
|
||||
### 全局安装
|
||||
|
||||
安装到您主目录的 `.trae` 或 `.trae-cn` 目录(适用于所有 Trae 项目):
|
||||
|
||||
```bash
|
||||
# 全局安装到 ~/.trae/(默认)
|
||||
.trae/install.sh ~
|
||||
|
||||
# 全局安装到 ~/.trae-cn/(CN 环境)
|
||||
TRAE_ENV=cn .trae/install.sh ~
|
||||
```
|
||||
|
||||
**注意**:全局安装适用于希望在所有项目之间维护单个 ECC 副本的场景。
|
||||
|
||||
## 环境支持
|
||||
|
||||
- **默认**:使用 `.trae` 目录
|
||||
- **CN 环境**:使用 `.trae-cn` 目录(通过 `TRAE_ENV=cn` 设置)
|
||||
|
||||
### 强制指定环境
|
||||
|
||||
```bash
|
||||
# 从项目根目录强制使用 CN 环境
|
||||
TRAE_ENV=cn .trae/install.sh
|
||||
|
||||
# 进入 .trae 目录后使用默认环境
|
||||
cd .trae
|
||||
./install.sh
|
||||
```
|
||||
|
||||
**注意**:`TRAE_ENV` 是一个全局环境变量,适用于整个安装会话。
|
||||
|
||||
## 卸载
|
||||
|
||||
卸载程序使用清单文件(`.ecc-manifest`)跟踪已安装的文件,确保安全删除:
|
||||
|
||||
```bash
|
||||
# 从当前目录卸载(如果已经在 .trae 或 .trae-cn 目录中)
|
||||
cd .trae-cn
|
||||
./uninstall.sh
|
||||
|
||||
# 或者从项目根目录卸载
|
||||
cd /path/to/your/project
|
||||
TRAE_ENV=cn .trae/uninstall.sh
|
||||
|
||||
# 从主目录全局卸载
|
||||
TRAE_ENV=cn .trae/uninstall.sh ~
|
||||
|
||||
# 卸载前会询问确认
|
||||
```
|
||||
|
||||
### 卸载行为
|
||||
|
||||
- **安全删除**:仅删除清单中跟踪的文件(由 ECC 安装的文件)
|
||||
- **保留用户文件**:您手动添加的任何文件都会被保留
|
||||
- **非空目录**:包含用户添加文件的目录会被跳过
|
||||
- **基于清单**:需要 `.ecc-manifest` 文件(在安装时创建)
|
||||
|
||||
### 环境支持
|
||||
|
||||
卸载程序遵循与安装程序相同的 `TRAE_ENV` 环境变量:
|
||||
|
||||
```bash
|
||||
# 从 .trae-cn 卸载(CN 环境)
|
||||
TRAE_ENV=cn ./uninstall.sh
|
||||
|
||||
# 从 .trae 卸载(默认环境)
|
||||
./uninstall.sh
|
||||
```
|
||||
|
||||
**注意**:如果找不到清单文件(旧版本安装),卸载程序将询问是否删除整个目录。
|
||||
|
||||
## 包含的内容
|
||||
|
||||
### 命令
|
||||
|
||||
命令是通过 Trae 聊天中的 `/` 菜单调用的按需工作流。所有命令都直接复用自项目根目录的 `commands/` 文件夹。
|
||||
|
||||
### 智能体
|
||||
|
||||
智能体是具有特定工具配置的专门 AI 助手。所有智能体都直接复用自项目根目录的 `agents/` 文件夹。
|
||||
|
||||
### 技能
|
||||
|
||||
技能是通过聊天中的 `/` 菜单调用的按需工作流。所有技能都直接复用自项目的 `skills/` 文件夹。
|
||||
|
||||
### 规则
|
||||
|
||||
规则提供始终适用的规则和上下文,塑造智能体处理代码的方式。所有规则都直接复用自项目根目录的 `rules/` 文件夹。
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 在聊天中输入 `/` 以打开命令菜单
|
||||
2. 选择一个命令或技能
|
||||
3. 智能体将通过具体说明和检查清单指导您完成工作流
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
.trae/ (或 .trae-cn/)
|
||||
├── commands/ # 命令文件(复用自项目根目录)
|
||||
├── agents/ # 智能体文件(复用自项目根目录)
|
||||
├── skills/ # 技能文件(复用自 skills/)
|
||||
├── rules/ # 规则文件(复用自项目根目录)
|
||||
├── install.sh # 安装脚本
|
||||
├── uninstall.sh # 卸载脚本
|
||||
└── README.md # 此文件
|
||||
```
|
||||
|
||||
## 自定义
|
||||
|
||||
安装后,所有文件都归您修改。安装程序永远不会覆盖现有文件,因此您的自定义在重新安装时是安全的。
|
||||
|
||||
**注意**:安装时会自动将 `install.sh` 和 `uninstall.sh` 脚本复制到目标目录,这样您可以在项目本地直接运行这些命令。
|
||||
|
||||
## 推荐的工作流
|
||||
|
||||
1. **从计划开始**:使用 `/plan` 命令分解复杂功能
|
||||
2. **先写测试**:在实现之前调用 `/tdd` 命令
|
||||
3. **审查您的代码**:编写代码后使用 `/code-review`
|
||||
4. **检查安全性**:对于身份验证、API 端点或敏感数据处理,再次使用 `/code-review`
|
||||
5. **修复构建错误**:如果有构建错误,使用 `/build-fix`
|
||||
|
||||
## 下一步
|
||||
|
||||
- 在 Trae 中打开您的项目
|
||||
- 输入 `/` 以查看可用命令
|
||||
- 享受 ECC 工作流!
|
||||
221
.trae/install.sh
Executable file
221
.trae/install.sh
Executable file
@@ -0,0 +1,221 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# ECC Trae Installer
|
||||
# Installs Everything Claude Code workflows into a Trae project.
|
||||
#
|
||||
# Usage:
|
||||
# ./install.sh # Install to current directory
|
||||
# ./install.sh ~ # Install globally to ~/.trae/ or ~/.trae-cn/
|
||||
#
|
||||
# Environment:
|
||||
# TRAE_ENV=cn # Force use .trae-cn directory
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# When globs match nothing, expand to empty list instead of the literal pattern
|
||||
shopt -s nullglob
|
||||
|
||||
# Resolve the directory where this script lives (the repo root)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Get the trae directory name (.trae or .trae-cn)
|
||||
get_trae_dir() {
|
||||
if [ "${TRAE_ENV:-}" = "cn" ]; then
|
||||
echo ".trae-cn"
|
||||
else
|
||||
echo ".trae"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_manifest_entry() {
|
||||
local manifest="$1"
|
||||
local entry="$2"
|
||||
|
||||
touch "$manifest"
|
||||
if ! grep -Fqx "$entry" "$manifest"; then
|
||||
echo "$entry" >> "$manifest"
|
||||
fi
|
||||
}
|
||||
|
||||
# Install function
|
||||
do_install() {
|
||||
local target_dir="$PWD"
|
||||
local trae_dir="$(get_trae_dir)"
|
||||
|
||||
# Check if ~ was specified (or expanded to $HOME)
|
||||
if [ "$#" -ge 1 ]; then
|
||||
if [ "$1" = "~" ] || [ "$1" = "$HOME" ]; then
|
||||
target_dir="$HOME"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if we're already inside a .trae or .trae-cn directory
|
||||
local current_dir_name="$(basename "$target_dir")"
|
||||
local trae_full_path
|
||||
|
||||
if [ "$current_dir_name" = ".trae" ] || [ "$current_dir_name" = ".trae-cn" ]; then
|
||||
# Already inside the trae directory, use it directly
|
||||
trae_full_path="$target_dir"
|
||||
else
|
||||
# Normal case: append trae_dir to target_dir
|
||||
trae_full_path="$target_dir/$trae_dir"
|
||||
fi
|
||||
|
||||
echo "ECC Trae Installer"
|
||||
echo "=================="
|
||||
echo ""
|
||||
echo "Source: $REPO_ROOT"
|
||||
echo "Target: $trae_full_path/"
|
||||
echo ""
|
||||
|
||||
# Subdirectories to create
|
||||
SUBDIRS="commands agents skills rules"
|
||||
|
||||
# Create all required trae subdirectories
|
||||
for dir in $SUBDIRS; do
|
||||
mkdir -p "$trae_full_path/$dir"
|
||||
done
|
||||
|
||||
# Manifest file to track installed files
|
||||
MANIFEST="$trae_full_path/.ecc-manifest"
|
||||
touch "$MANIFEST"
|
||||
|
||||
# Counters for summary
|
||||
commands=0
|
||||
agents=0
|
||||
skills=0
|
||||
rules=0
|
||||
other=0
|
||||
|
||||
# Copy commands from repo root
|
||||
if [ -d "$REPO_ROOT/commands" ]; then
|
||||
for f in "$REPO_ROOT/commands"/*.md; do
|
||||
[ -f "$f" ] || continue
|
||||
local_name=$(basename "$f")
|
||||
target_path="$trae_full_path/commands/$local_name"
|
||||
if [ ! -f "$target_path" ]; then
|
||||
cp "$f" "$target_path"
|
||||
ensure_manifest_entry "$MANIFEST" "commands/$local_name"
|
||||
commands=$((commands + 1))
|
||||
else
|
||||
ensure_manifest_entry "$MANIFEST" "commands/$local_name"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Copy agents from repo root
|
||||
if [ -d "$REPO_ROOT/agents" ]; then
|
||||
for f in "$REPO_ROOT/agents"/*.md; do
|
||||
[ -f "$f" ] || continue
|
||||
local_name=$(basename "$f")
|
||||
target_path="$trae_full_path/agents/$local_name"
|
||||
if [ ! -f "$target_path" ]; then
|
||||
cp "$f" "$target_path"
|
||||
ensure_manifest_entry "$MANIFEST" "agents/$local_name"
|
||||
agents=$((agents + 1))
|
||||
else
|
||||
ensure_manifest_entry "$MANIFEST" "agents/$local_name"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Copy skills from repo root (if available)
|
||||
if [ -d "$REPO_ROOT/skills" ]; then
|
||||
for d in "$REPO_ROOT/skills"/*/; do
|
||||
[ -d "$d" ] || continue
|
||||
skill_name="$(basename "$d")"
|
||||
target_skill_dir="$trae_full_path/skills/$skill_name"
|
||||
skill_copied=0
|
||||
|
||||
while IFS= read -r source_file; do
|
||||
relative_path="${source_file#$d}"
|
||||
target_path="$target_skill_dir/$relative_path"
|
||||
|
||||
mkdir -p "$(dirname "$target_path")"
|
||||
if [ ! -f "$target_path" ]; then
|
||||
cp "$source_file" "$target_path"
|
||||
skill_copied=1
|
||||
fi
|
||||
ensure_manifest_entry "$MANIFEST" "skills/$skill_name/$relative_path"
|
||||
done < <(find "$d" -type f | sort)
|
||||
|
||||
if [ "$skill_copied" -eq 1 ]; then
|
||||
skills=$((skills + 1))
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Copy rules from repo root
|
||||
if [ -d "$REPO_ROOT/rules" ]; then
|
||||
while IFS= read -r rule_file; do
|
||||
relative_path="${rule_file#$REPO_ROOT/rules/}"
|
||||
target_path="$trae_full_path/rules/$relative_path"
|
||||
|
||||
mkdir -p "$(dirname "$target_path")"
|
||||
if [ ! -f "$target_path" ]; then
|
||||
cp "$rule_file" "$target_path"
|
||||
rules=$((rules + 1))
|
||||
fi
|
||||
ensure_manifest_entry "$MANIFEST" "rules/$relative_path"
|
||||
done < <(find "$REPO_ROOT/rules" -type f | sort)
|
||||
fi
|
||||
|
||||
# Copy README files from this directory
|
||||
for readme_file in "$SCRIPT_DIR/README.md" "$SCRIPT_DIR/README.zh-CN.md"; do
|
||||
if [ -f "$readme_file" ]; then
|
||||
local_name=$(basename "$readme_file")
|
||||
target_path="$trae_full_path/$local_name"
|
||||
if [ ! -f "$target_path" ]; then
|
||||
cp "$readme_file" "$target_path"
|
||||
ensure_manifest_entry "$MANIFEST" "$local_name"
|
||||
other=$((other + 1))
|
||||
else
|
||||
ensure_manifest_entry "$MANIFEST" "$local_name"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Copy install and uninstall scripts
|
||||
for script_file in "$SCRIPT_DIR/install.sh" "$SCRIPT_DIR/uninstall.sh"; do
|
||||
if [ -f "$script_file" ]; then
|
||||
local_name=$(basename "$script_file")
|
||||
target_path="$trae_full_path/$local_name"
|
||||
if [ ! -f "$target_path" ]; then
|
||||
cp "$script_file" "$target_path"
|
||||
chmod +x "$target_path"
|
||||
ensure_manifest_entry "$MANIFEST" "$local_name"
|
||||
other=$((other + 1))
|
||||
else
|
||||
ensure_manifest_entry "$MANIFEST" "$local_name"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Add manifest file itself to manifest
|
||||
ensure_manifest_entry "$MANIFEST" ".ecc-manifest"
|
||||
|
||||
# Installation summary
|
||||
echo "Installation complete!"
|
||||
echo ""
|
||||
echo "Components installed:"
|
||||
echo " Commands: $commands"
|
||||
echo " Agents: $agents"
|
||||
echo " Skills: $skills"
|
||||
echo " Rules: $rules"
|
||||
echo ""
|
||||
echo "Directory: $(basename "$trae_full_path")"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Open your project in Trae"
|
||||
echo " 2. Type / to see available commands"
|
||||
echo " 3. Enjoy the ECC workflows!"
|
||||
echo ""
|
||||
echo "To uninstall later:"
|
||||
echo " cd $trae_full_path"
|
||||
echo " ./uninstall.sh"
|
||||
}
|
||||
|
||||
# Main logic
|
||||
do_install "$@"
|
||||
194
.trae/uninstall.sh
Executable file
194
.trae/uninstall.sh
Executable file
@@ -0,0 +1,194 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# ECC Trae Uninstaller
|
||||
# Uninstalls Everything Claude Code workflows from a Trae project.
|
||||
#
|
||||
# Usage:
|
||||
# ./uninstall.sh # Uninstall from current directory
|
||||
# ./uninstall.sh ~ # Uninstall globally from ~/.trae/
|
||||
#
|
||||
# Environment:
|
||||
# TRAE_ENV=cn # Force use .trae-cn directory
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve the directory where this script lives
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Get the trae directory name (.trae or .trae-cn)
|
||||
get_trae_dir() {
|
||||
# Check environment variable first
|
||||
if [ "${TRAE_ENV:-}" = "cn" ]; then
|
||||
echo ".trae-cn"
|
||||
else
|
||||
echo ".trae"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_path() {
|
||||
python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$1"
|
||||
}
|
||||
|
||||
is_valid_manifest_entry() {
|
||||
local file_path="$1"
|
||||
|
||||
case "$file_path" in
|
||||
""|/*|~*|*/../*|../*|*/..|..)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Main uninstall function
|
||||
do_uninstall() {
|
||||
local target_dir="$PWD"
|
||||
local trae_dir="$(get_trae_dir)"
|
||||
|
||||
# Check if ~ was specified (or expanded to $HOME)
|
||||
if [ "$#" -ge 1 ]; then
|
||||
if [ "$1" = "~" ] || [ "$1" = "$HOME" ]; then
|
||||
target_dir="$HOME"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if we're already inside a .trae or .trae-cn directory
|
||||
local current_dir_name="$(basename "$target_dir")"
|
||||
local trae_full_path
|
||||
|
||||
if [ "$current_dir_name" = ".trae" ] || [ "$current_dir_name" = ".trae-cn" ]; then
|
||||
# Already inside the trae directory, use it directly
|
||||
trae_full_path="$target_dir"
|
||||
else
|
||||
# Normal case: append trae_dir to target_dir
|
||||
trae_full_path="$target_dir/$trae_dir"
|
||||
fi
|
||||
|
||||
echo "ECC Trae Uninstaller"
|
||||
echo "===================="
|
||||
echo ""
|
||||
echo "Target: $trae_full_path/"
|
||||
echo ""
|
||||
|
||||
if [ ! -d "$trae_full_path" ]; then
|
||||
echo "Error: $trae_dir directory not found at $target_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
trae_root_resolved="$(resolve_path "$trae_full_path")"
|
||||
|
||||
# Manifest file path
|
||||
MANIFEST="$trae_full_path/.ecc-manifest"
|
||||
|
||||
if [ ! -f "$MANIFEST" ]; then
|
||||
echo "Warning: No manifest file found (.ecc-manifest)"
|
||||
echo ""
|
||||
echo "This could mean:"
|
||||
echo " 1. ECC was installed with an older version without manifest support"
|
||||
echo " 2. The manifest file was manually deleted"
|
||||
echo ""
|
||||
read -p "Do you want to remove the entire $trae_dir directory? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Uninstall cancelled."
|
||||
exit 0
|
||||
fi
|
||||
rm -rf "$trae_full_path"
|
||||
echo "Uninstall complete!"
|
||||
echo ""
|
||||
echo "Removed: $trae_full_path/"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found manifest file - will only remove files installed by ECC"
|
||||
echo ""
|
||||
read -p "Are you sure you want to uninstall ECC from $trae_dir? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Uninstall cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Counters
|
||||
removed=0
|
||||
skipped=0
|
||||
|
||||
# Read manifest and remove files
|
||||
while IFS= read -r file_path; do
|
||||
[ -z "$file_path" ] && continue
|
||||
|
||||
if ! is_valid_manifest_entry "$file_path"; then
|
||||
echo "Skipped: $file_path (invalid manifest entry)"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
full_path="$trae_full_path/$file_path"
|
||||
resolved_full="$(resolve_path "$full_path")"
|
||||
|
||||
case "$resolved_full" in
|
||||
"$trae_root_resolved"|"$trae_root_resolved"/*)
|
||||
;;
|
||||
*)
|
||||
echo "Skipped: $file_path (invalid manifest entry)"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -f "$resolved_full" ]; then
|
||||
rm -f "$resolved_full"
|
||||
echo "Removed: $file_path"
|
||||
removed=$((removed + 1))
|
||||
elif [ -d "$resolved_full" ]; then
|
||||
# Only remove directory if it's empty
|
||||
if [ -z "$(ls -A "$resolved_full" 2>/dev/null)" ]; then
|
||||
rmdir "$resolved_full" 2>/dev/null || true
|
||||
if [ ! -d "$resolved_full" ]; then
|
||||
echo "Removed: $file_path/"
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
else
|
||||
echo "Skipped: $file_path/ (not empty - contains user files)"
|
||||
skipped=$((skipped + 1))
|
||||
fi
|
||||
else
|
||||
skipped=$((skipped + 1))
|
||||
fi
|
||||
done < "$MANIFEST"
|
||||
|
||||
while IFS= read -r empty_dir; do
|
||||
[ "$empty_dir" = "$trae_full_path" ] && continue
|
||||
relative_dir="${empty_dir#$trae_full_path/}"
|
||||
rmdir "$empty_dir" 2>/dev/null || true
|
||||
if [ ! -d "$empty_dir" ]; then
|
||||
echo "Removed: $relative_dir/"
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
done < <(find "$trae_full_path" -depth -type d -empty 2>/dev/null | sort -r)
|
||||
|
||||
# Try to remove the main trae directory if it's empty
|
||||
if [ -d "$trae_full_path" ] && [ -z "$(ls -A "$trae_full_path" 2>/dev/null)" ]; then
|
||||
rmdir "$trae_full_path" 2>/dev/null || true
|
||||
if [ ! -d "$trae_full_path" ]; then
|
||||
echo "Removed: $trae_dir/"
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Uninstall complete!"
|
||||
echo ""
|
||||
echo "Summary:"
|
||||
echo " Removed: $removed items"
|
||||
echo " Skipped: $skipped items (not found or user-modified)"
|
||||
echo ""
|
||||
if [ -d "$trae_full_path" ]; then
|
||||
echo "Note: $trae_dir directory still exists (contains user-added files)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Execute uninstall
|
||||
do_uninstall "$@"
|
||||
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
@@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
||||
@@ -1,6 +1,6 @@
|
||||
# Everything Claude Code (ECC) — Agent Instructions
|
||||
|
||||
This is a **production-ready AI coding plugin** providing 28 specialized agents, 125 skills, 60 commands, and automated hook workflows for software development.
|
||||
This is a **production-ready AI coding plugin** providing 30 specialized agents, 135 skills, 60 commands, and automated hook workflows for software development.
|
||||
|
||||
**Version:** 1.9.0
|
||||
|
||||
@@ -141,8 +141,8 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
agents/ — 28 specialized subagents
|
||||
skills/ — 117 workflow skills and domain knowledge
|
||||
agents/ — 30 specialized subagents
|
||||
skills/ — 135 workflow skills and domain knowledge
|
||||
commands/ — 60 slash commands
|
||||
hooks/ — Trigger-based automations
|
||||
rules/ — Always-follow guidelines (common + per-language)
|
||||
|
||||
11
CLAUDE.md
11
CLAUDE.md
@@ -59,3 +59,14 @@ Follow the formats in CONTRIBUTING.md:
|
||||
- Hooks: JSON with matcher and hooks array
|
||||
|
||||
File naming: lowercase with hyphens (e.g., `python-reviewer.md`, `tdd-workflow.md`)
|
||||
|
||||
## Skills
|
||||
|
||||
Use the following skills when working on related files:
|
||||
|
||||
| File(s) | Skill |
|
||||
|---------|-------|
|
||||
| `README.md` | `/readme` |
|
||||
| `.github/workflows/*.yml` | `/ci-workflow` |
|
||||
|
||||
When spawning subagents, always pass conventions from the respective skill into the agent's prompt.
|
||||
|
||||
159
COMMANDS-QUICK-REF.md
Normal file
159
COMMANDS-QUICK-REF.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Commands Quick Reference
|
||||
|
||||
> 59 slash commands installed globally. Type `/` in any Claude Code session to invoke.
|
||||
|
||||
---
|
||||
|
||||
## Core Workflow
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `/plan` | Restate requirements, assess risks, write step-by-step implementation plan — **waits for your confirm before touching code** |
|
||||
| `/tdd` | Enforce test-driven development: scaffold interface → write failing test → implement → verify 80%+ coverage |
|
||||
| `/code-review` | Full code quality, security, and maintainability review of changed files |
|
||||
| `/build-fix` | Detect and fix build errors — delegates to the right build-resolver agent automatically |
|
||||
| `/verify` | Run the full verification loop: build → lint → test → type-check |
|
||||
| `/quality-gate` | Quality gate check against project standards |
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `/tdd` | Universal TDD workflow (any language) |
|
||||
| `/e2e` | Generate + run Playwright end-to-end tests, capture screenshots/videos/traces |
|
||||
| `/test-coverage` | Report test coverage, identify gaps |
|
||||
| `/go-test` | TDD workflow for Go (table-driven, 80%+ coverage with `go test -cover`) |
|
||||
| `/kotlin-test` | TDD for Kotlin (Kotest + Kover) |
|
||||
| `/rust-test` | TDD for Rust (cargo test, integration tests) |
|
||||
| `/cpp-test` | TDD for C++ (GoogleTest + gcov/lcov) |
|
||||
|
||||
---
|
||||
|
||||
## Code Review
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `/code-review` | Universal code review |
|
||||
| `/python-review` | Python — PEP 8, type hints, security, idiomatic patterns |
|
||||
| `/go-review` | Go — idiomatic patterns, concurrency safety, error handling |
|
||||
| `/kotlin-review` | Kotlin — null safety, coroutine safety, clean architecture |
|
||||
| `/rust-review` | Rust — ownership, lifetimes, unsafe usage |
|
||||
| `/cpp-review` | C++ — memory safety, modern idioms, concurrency |
|
||||
|
||||
---
|
||||
|
||||
## Build Fixers
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `/build-fix` | Auto-detect language and fix build errors |
|
||||
| `/go-build` | Fix Go build errors and `go vet` warnings |
|
||||
| `/kotlin-build` | Fix Kotlin/Gradle compiler errors |
|
||||
| `/rust-build` | Fix Rust build + borrow checker issues |
|
||||
| `/cpp-build` | Fix C++ CMake and linker problems |
|
||||
| `/gradle-build` | Fix Gradle errors for Android / KMP |
|
||||
|
||||
---
|
||||
|
||||
## Planning & Architecture
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `/plan` | Implementation plan with risk assessment |
|
||||
| `/multi-plan` | Multi-model collaborative planning |
|
||||
| `/multi-workflow` | Multi-model collaborative development |
|
||||
| `/multi-backend` | Backend-focused multi-model development |
|
||||
| `/multi-frontend` | Frontend-focused multi-model development |
|
||||
| `/multi-execute` | Multi-model collaborative execution |
|
||||
| `/orchestrate` | Guide for tmux/worktree multi-agent orchestration |
|
||||
| `/devfleet` | Orchestrate parallel Claude Code agents via DevFleet |
|
||||
|
||||
---
|
||||
|
||||
## Session Management
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `/save-session` | Save current session state to `~/.claude/session-data/` |
|
||||
| `/resume-session` | Load the most recent saved session from the canonical session store and resume from where you left off |
|
||||
| `/sessions` | Browse, search, and manage session history with aliases from `~/.claude/session-data/` (with legacy reads from `~/.claude/sessions/`) |
|
||||
| `/checkpoint` | Mark a checkpoint in the current session |
|
||||
| `/aside` | Answer a quick side question without losing current task context |
|
||||
| `/context-budget` | Analyse context window usage — find token overhead, optimise |
|
||||
|
||||
---
|
||||
|
||||
## Learning & Improvement
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `/learn` | Extract reusable patterns from the current session |
|
||||
| `/learn-eval` | Extract patterns + self-evaluate quality before saving |
|
||||
| `/evolve` | Analyse learned instincts, suggest evolved skill structures |
|
||||
| `/promote` | Promote project-scoped instincts to global scope |
|
||||
| `/instinct-status` | Show all learned instincts (project + global) with confidence scores |
|
||||
| `/instinct-export` | Export instincts to a file |
|
||||
| `/instinct-import` | Import instincts from a file or URL |
|
||||
| `/skill-create` | Analyse local git history → generate a reusable skill |
|
||||
| `/skill-health` | Skill portfolio health dashboard with analytics |
|
||||
| `/rules-distill` | Scan skills, extract cross-cutting principles, distill into rules |
|
||||
|
||||
---
|
||||
|
||||
## Refactoring & Cleanup
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `/refactor-clean` | Remove dead code, consolidate duplicates, clean up structure |
|
||||
| `/prompt-optimize` | Analyse a draft prompt and output an optimised ECC-enriched version |
|
||||
|
||||
---
|
||||
|
||||
## Docs & Research
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `/docs` | Look up current library/API documentation via Context7 |
|
||||
| `/update-docs` | Update project documentation |
|
||||
| `/update-codemaps` | Regenerate codemaps for the codebase |
|
||||
|
||||
---
|
||||
|
||||
## Loops & Automation
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `/loop-start` | Start a recurring agent loop on an interval |
|
||||
| `/loop-status` | Check status of running loops |
|
||||
| `/claw` | Start NanoClaw v2 — persistent REPL with model routing, skill hot-load, branching, and metrics |
|
||||
|
||||
---
|
||||
|
||||
## Project & Infrastructure
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `/projects` | List known projects and their instinct statistics |
|
||||
| `/harness-audit` | Audit the agent harness configuration for reliability and cost |
|
||||
| `/eval` | Run the evaluation harness |
|
||||
| `/model-route` | Route a task to the right model (Haiku / Sonnet / Opus) |
|
||||
| `/pm2` | PM2 process manager initialisation |
|
||||
| `/setup-pm` | Configure package manager (npm / pnpm / yarn / bun) |
|
||||
|
||||
---
|
||||
|
||||
## Quick Decision Guide
|
||||
|
||||
```
|
||||
Starting a new feature? → /plan first, then /tdd
|
||||
Code just written? → /code-review
|
||||
Build broken? → /build-fix
|
||||
Need live docs? → /docs <library>
|
||||
Session about to end? → /save-session or /learn-eval
|
||||
Resuming next day? → /resume-session
|
||||
Context getting heavy? → /context-budget then /checkpoint
|
||||
Want to extract what you learned? → /learn-eval then /evolve
|
||||
Running repeated tasks? → /loop-start
|
||||
```
|
||||
@@ -73,6 +73,13 @@ git add . && git commit -m "feat: add my-skill" && git push -u origin feat/my-co
|
||||
|
||||
Skills are knowledge modules that Claude Code loads based on context.
|
||||
|
||||
> **📚 Comprehensive Guide:** For detailed guidance on creating effective skills, see [Skill Development Guide](docs/SKILL-DEVELOPMENT-GUIDE.md). It covers:
|
||||
> - Skill architecture and categories
|
||||
> - Writing effective content with examples
|
||||
> - Best practices and common patterns
|
||||
> - Testing and validation
|
||||
> - Complete examples gallery
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
@@ -86,7 +93,7 @@ skills/
|
||||
```markdown
|
||||
---
|
||||
name: your-skill-name
|
||||
description: Brief description shown in skill list
|
||||
description: Brief description shown in skill list and used for auto-activation
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
@@ -94,6 +101,10 @@ origin: ECC
|
||||
|
||||
Brief overview of what this skill covers.
|
||||
|
||||
## When to Activate
|
||||
|
||||
Describe scenarios where Claude should use this skill. This is critical for auto-activation.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
Explain key patterns and guidelines.
|
||||
@@ -107,33 +118,54 @@ function example() {
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
Show what NOT to do with examples.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Actionable guidelines
|
||||
- Do's and don'ts
|
||||
- Common pitfalls to avoid
|
||||
|
||||
## When to Use
|
||||
## Related Skills
|
||||
|
||||
Describe scenarios where this skill applies.
|
||||
Link to complementary skills (e.g., `related-skill-1`, `related-skill-2`).
|
||||
```
|
||||
|
||||
### Skill Categories
|
||||
|
||||
| Category | Purpose | Examples |
|
||||
|----------|---------|----------|
|
||||
| **Language Standards** | Idioms, conventions, best practices | `python-patterns`, `golang-patterns` |
|
||||
| **Framework Patterns** | Framework-specific guidance | `django-patterns`, `nextjs-patterns` |
|
||||
| **Workflow** | Step-by-step processes | `tdd-workflow`, `refactoring-workflow` |
|
||||
| **Domain Knowledge** | Specialized domains | `security-review`, `api-design` |
|
||||
| **Tool Integration** | Tool/library usage | `docker-patterns`, `supabase-patterns` |
|
||||
| **Template** | Project-specific skill templates | `project-guidelines-example` |
|
||||
|
||||
### Skill Checklist
|
||||
|
||||
- [ ] Focused on one domain/technology
|
||||
- [ ] Includes practical code examples
|
||||
- [ ] Under 500 lines
|
||||
- [ ] Focused on one domain/technology (not too broad)
|
||||
- [ ] Includes "When to Activate" section for auto-activation
|
||||
- [ ] Includes practical, copy-pasteable code examples
|
||||
- [ ] Shows anti-patterns (what NOT to do)
|
||||
- [ ] Under 500 lines (800 max)
|
||||
- [ ] Uses clear section headers
|
||||
- [ ] Tested with Claude Code
|
||||
- [ ] Links to related skills
|
||||
- [ ] No sensitive data (API keys, tokens, paths)
|
||||
|
||||
### Example Skills
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `coding-standards/` | TypeScript/JavaScript patterns |
|
||||
| `frontend-patterns/` | React and Next.js best practices |
|
||||
| `backend-patterns/` | API and database patterns |
|
||||
| `security-review/` | Security checklist |
|
||||
| Skill | Category | Purpose |
|
||||
|-------|----------|---------|
|
||||
| `coding-standards/` | Language Standards | TypeScript/JavaScript patterns |
|
||||
| `frontend-patterns/` | Framework Patterns | React and Next.js best practices |
|
||||
| `backend-patterns/` | Framework Patterns | API and database patterns |
|
||||
| `security-review/` | Domain Knowledge | Security checklist |
|
||||
| `tdd-workflow/` | Workflow | Test-driven development process |
|
||||
| `project-guidelines-example/` | Template | Project-specific skill template |
|
||||
|
||||
---
|
||||
|
||||
|
||||
122
EVALUATION.md
Normal file
122
EVALUATION.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Repo Evaluation vs Current Setup
|
||||
|
||||
**Date:** 2026-03-21
|
||||
**Branch:** `claude/evaluate-repo-comparison-ASZ9Y`
|
||||
|
||||
---
|
||||
|
||||
## Current Setup (`~/.claude/`)
|
||||
|
||||
The active Claude Code installation is near-minimal:
|
||||
|
||||
| Component | Current |
|
||||
|-----------|---------|
|
||||
| Agents | 0 |
|
||||
| Skills | 0 installed |
|
||||
| Commands | 0 |
|
||||
| Hooks | 1 (Stop: git check) |
|
||||
| Rules | 0 |
|
||||
| MCP configs | 0 |
|
||||
|
||||
**Installed hooks:**
|
||||
- `Stop` → `stop-hook-git-check.sh` — blocks session end if there are uncommitted changes or unpushed commits
|
||||
|
||||
**Installed permissions:**
|
||||
- `Skill` — allows skill invocations
|
||||
|
||||
**Plugins:** Only `blocklist.json` (no active plugins installed)
|
||||
|
||||
---
|
||||
|
||||
## This Repo (`everything-claude-code` v1.9.0)
|
||||
|
||||
| Component | Repo |
|
||||
|-----------|------|
|
||||
| Agents | 28 |
|
||||
| Skills | 116 |
|
||||
| Commands | 59 |
|
||||
| Rules sets | 12 languages + common (60+ rule files) |
|
||||
| Hooks | Comprehensive system (PreToolUse, PostToolUse, SessionStart, Stop) |
|
||||
| MCP configs | 1 (Context7 + others) |
|
||||
| Schemas | 9 JSON validators |
|
||||
| Scripts/CLI | 46+ Node.js modules + multiple CLIs |
|
||||
| Tests | 58 test files |
|
||||
| Install profiles | core, developer, security, research, full |
|
||||
| Supported harnesses | Claude Code, Codex, Cursor, OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## Gap Analysis
|
||||
|
||||
### Hooks
|
||||
- **Current:** 1 Stop hook (git hygiene check)
|
||||
- **Repo:** Full hook matrix covering:
|
||||
- Dangerous command blocking (`rm -rf`, force pushes)
|
||||
- Auto-formatting on file edits
|
||||
- Dev server tmux enforcement
|
||||
- Cost tracking
|
||||
- Session evaluation and governance capture
|
||||
- MCP health monitoring
|
||||
|
||||
### Agents (28 missing)
|
||||
The repo provides specialized agents for every major workflow:
|
||||
- Language reviewers: TypeScript, Python, Go, Java, Kotlin, Rust, C++, Flutter
|
||||
- Build resolvers: Go, Java, Kotlin, Rust, C++, PyTorch
|
||||
- Workflow agents: planner, tdd-guide, code-reviewer, security-reviewer, architect
|
||||
- Automation: loop-operator, doc-updater, refactor-cleaner, harness-optimizer
|
||||
|
||||
### Skills (116 missing)
|
||||
Domain knowledge modules covering:
|
||||
- Language patterns (Python, Go, Kotlin, Rust, C++, Java, Swift, Perl, Laravel, Django)
|
||||
- Testing strategies (TDD, E2E, coverage)
|
||||
- Architecture patterns (backend, frontend, API design, database migrations)
|
||||
- AI/ML workflows (Claude API, eval harness, agent loops, cost-aware pipelines)
|
||||
- Business workflows (investor materials, market research, content engine)
|
||||
|
||||
### Commands (59 missing)
|
||||
- `/tdd`, `/plan`, `/e2e`, `/code-review` — core dev workflows
|
||||
- `/sessions`, `/save-session`, `/resume-session` — session persistence
|
||||
- `/orchestrate`, `/multi-plan`, `/multi-execute` — multi-agent coordination
|
||||
- `/learn`, `/skill-create`, `/evolve` — continuous improvement
|
||||
- `/build-fix`, `/verify`, `/quality-gate` — build/quality automation
|
||||
|
||||
### Rules (60+ files missing)
|
||||
Language-specific coding style, patterns, testing, and security guidelines for:
|
||||
TypeScript, Python, Go, Java, Kotlin, Rust, C++, C#, Swift, Perl, PHP, and common/cross-language rules.
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate value (core install)
|
||||
Run `ecc install --profile core` to get:
|
||||
- Core agents (code-reviewer, planner, tdd-guide, security-reviewer)
|
||||
- Essential skills (tdd-workflow, coding-standards, security-review)
|
||||
- Key commands (/tdd, /plan, /code-review, /build-fix)
|
||||
|
||||
### Full install
|
||||
Run `ecc install --profile full` to get all 28 agents, 116 skills, and 59 commands.
|
||||
|
||||
### Hooks upgrade
|
||||
The current Stop hook is solid. The repo's `hooks.json` adds:
|
||||
- Dangerous command blocking (safety)
|
||||
- Auto-formatting (quality)
|
||||
- Cost tracking (observability)
|
||||
- Session evaluation (learning)
|
||||
|
||||
### Rules
|
||||
Adding language rules (e.g., TypeScript, Python) provides always-on coding guidelines without relying on per-session prompts.
|
||||
|
||||
---
|
||||
|
||||
## What the Current Setup Does Well
|
||||
|
||||
- The `stop-hook-git-check.sh` Stop hook is production-quality and already enforces good git hygiene
|
||||
- The `Skill` permission is correctly configured
|
||||
- The setup is clean with no conflicts or cruft
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The current setup is essentially a blank slate with one well-implemented git hygiene hook. This repo provides a complete, production-tested enhancement layer covering agents, skills, commands, hooks, and rules — with a selective install system so you can add exactly what you need without bloating the configuration.
|
||||
70
README.md
70
README.md
@@ -1,6 +1,4 @@
|
||||
**Language:** English | [Português (Brasil)](docs/pt-BR/README.md) | [简体中文](README.zh-CN.md) | [繁體中文](docs/zh-TW/README.md) | [日本語](docs/ja-JP/README.md) | [한국어](docs/ko-KR/README.md)
|
||||
[Türkçe](docs/tr/README.md)
|
||||
|
||||
**Language:** English | [Português (Brasil)](docs/pt-BR/README.md) | [简体中文](README.zh-CN.md) | [繁體中文](docs/zh-TW/README.md) | [日本語](docs/ja-JP/README.md) | [한국어](docs/ko-KR/README.md) | [Türkçe](docs/tr/README.md)
|
||||
|
||||
# Everything Claude Code
|
||||
|
||||
@@ -180,6 +178,11 @@ cd everything-claude-code
|
||||
npm install # or: pnpm install | yarn install | bun install
|
||||
|
||||
# macOS/Linux
|
||||
|
||||
# Recommended: install everything (full profile)
|
||||
./install.sh --profile full
|
||||
|
||||
# Or install for specific languages only
|
||||
./install.sh typescript # or python or golang or swift or php
|
||||
# ./install.sh typescript python golang swift php
|
||||
# ./install.sh --target cursor typescript
|
||||
@@ -188,6 +191,11 @@ npm install # or: pnpm install | yarn install | bun install
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell
|
||||
|
||||
# Recommended: install everything (full profile)
|
||||
.\install.ps1 --profile full
|
||||
|
||||
# Or install for specific languages only
|
||||
.\install.ps1 typescript # or python or golang or swift or php
|
||||
# .\install.ps1 typescript python golang swift php
|
||||
# .\install.ps1 --target cursor typescript
|
||||
@@ -197,7 +205,7 @@ npm install # or: pnpm install | yarn install | bun install
|
||||
npx ecc-install typescript
|
||||
```
|
||||
|
||||
For manual install instructions see the README in the `rules/` folder.
|
||||
For manual install instructions see the README in the `rules/` folder. When copying rules manually, copy the whole language directory (for example `rules/common` or `rules/golang`), not the files inside it, so relative references keep working and filenames do not collide.
|
||||
|
||||
### Step 3: Start Using
|
||||
|
||||
@@ -212,7 +220,21 @@ 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 28 agents, 125 skills, and 60 commands.
|
||||
✨ **That's it!** You now have access to 30 agents, 135 skills, and 60 commands.
|
||||
|
||||
### Multi-model commands require additional setup
|
||||
|
||||
> ⚠️ `multi-*` commands are **not** covered by the base plugin/rules install above.
|
||||
>
|
||||
> To use `/multi-plan`, `/multi-execute`, `/multi-backend`, `/multi-frontend`, and `/multi-workflow`, you must also install the `ccg-workflow` runtime.
|
||||
>
|
||||
> Initialize it with `npx ccg-workflow`.
|
||||
>
|
||||
> That runtime provides the external dependencies these commands expect, including:
|
||||
> - `~/.claude/bin/codeagent-wrapper`
|
||||
> - `~/.claude/.ccg/prompts/*`
|
||||
>
|
||||
> Without `ccg-workflow`, these `multi-*` commands will not run correctly.
|
||||
|
||||
---
|
||||
|
||||
@@ -273,7 +295,7 @@ everything-claude-code/
|
||||
| |-- plugin.json # Plugin metadata and component paths
|
||||
| |-- marketplace.json # Marketplace catalog for /plugin marketplace add
|
||||
|
|
||||
|-- agents/ # 28 specialized subagents for delegation
|
||||
|-- agents/ # 30 specialized subagents for delegation
|
||||
| |-- planner.md # Feature implementation planning
|
||||
| |-- architect.md # System design decisions
|
||||
| |-- tdd-guide.md # Test-driven development
|
||||
@@ -614,16 +636,16 @@ This gives you instant access to all commands, agents, skills, and hooks.
|
||||
>
|
||||
> # Option A: User-level rules (applies to all projects)
|
||||
> mkdir -p ~/.claude/rules
|
||||
> cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # pick your stack
|
||||
> cp -r everything-claude-code/rules/python/* ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/php/* ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/common ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/typescript ~/.claude/rules/ # pick your stack
|
||||
> cp -r everything-claude-code/rules/python ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/golang ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/php ~/.claude/rules/
|
||||
>
|
||||
> # Option B: Project-level rules (applies to current project only)
|
||||
> mkdir -p .claude/rules
|
||||
> cp -r everything-claude-code/rules/common/* .claude/rules/
|
||||
> cp -r everything-claude-code/rules/typescript/* .claude/rules/ # pick your stack
|
||||
> cp -r everything-claude-code/rules/common .claude/rules/
|
||||
> cp -r everything-claude-code/rules/typescript .claude/rules/ # pick your stack
|
||||
> ```
|
||||
|
||||
---
|
||||
@@ -639,12 +661,13 @@ git clone https://github.com/affaan-m/everything-claude-code.git
|
||||
# Copy agents to your Claude config
|
||||
cp everything-claude-code/agents/*.md ~/.claude/agents/
|
||||
|
||||
# Copy rules (common + language-specific)
|
||||
cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # pick your stack
|
||||
cp -r everything-claude-code/rules/python/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/php/* ~/.claude/rules/
|
||||
# Copy rules directories (common + language-specific)
|
||||
mkdir -p ~/.claude/rules
|
||||
cp -r everything-claude-code/rules/common ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/typescript ~/.claude/rules/ # pick your stack
|
||||
cp -r everything-claude-code/rules/python ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/golang ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/php ~/.claude/rules/
|
||||
|
||||
# Copy commands
|
||||
cp everything-claude-code/commands/*.md ~/.claude/commands/
|
||||
@@ -850,7 +873,8 @@ Yes. Use Option 2 (manual installation) and copy only what you need:
|
||||
cp everything-claude-code/agents/*.md ~/.claude/agents/
|
||||
|
||||
# Just rules
|
||||
cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
mkdir -p ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/common ~/.claude/rules/
|
||||
```
|
||||
|
||||
Each component is fully independent.
|
||||
@@ -999,6 +1023,8 @@ cp .codex/config.toml ~/.codex/config.toml
|
||||
|
||||
The sync script safely merges ECC MCP servers into your existing `~/.codex/config.toml` using an **add-only** strategy — it never removes or modifies your existing servers. Run with `--dry-run` to preview changes, or `--update-mcp` to force-refresh ECC servers to the latest recommended config.
|
||||
|
||||
For Context7, ECC uses the canonical Codex section name `[mcp_servers.context7]` while still launching the `@upstash/context7-mcp` package. If you already have a legacy `[mcp_servers.context7-mcp]` entry, `--update-mcp` migrates it to the canonical section name.
|
||||
|
||||
Codex macOS app:
|
||||
- Open this repository as your workspace.
|
||||
- The root `AGENTS.md` is auto-detected.
|
||||
@@ -1083,9 +1109,9 @@ The configuration is automatically detected from `.opencode/opencode.json`.
|
||||
|
||||
| Feature | Claude Code | OpenCode | Status |
|
||||
|---------|-------------|----------|--------|
|
||||
| Agents | ✅ 28 agents | ✅ 12 agents | **Claude Code leads** |
|
||||
| Agents | ✅ 30 agents | ✅ 12 agents | **Claude Code leads** |
|
||||
| Commands | ✅ 60 commands | ✅ 31 commands | **Claude Code leads** |
|
||||
| Skills | ✅ 125 skills | ✅ 37 skills | **Claude Code leads** |
|
||||
| Skills | ✅ 135 skills | ✅ 37 skills | **Claude Code leads** |
|
||||
| Hooks | ✅ 8 event types | ✅ 11 events | **OpenCode has more!** |
|
||||
| Rules | ✅ 29 rules | ✅ 13 instructions | **Claude Code leads** |
|
||||
| MCP Servers | ✅ 14 servers | ✅ Full | **Full parity** |
|
||||
|
||||
@@ -82,14 +82,17 @@
|
||||
# 首先克隆仓库
|
||||
git clone https://github.com/affaan-m/everything-claude-code.git
|
||||
|
||||
# 复制规则(通用 + 语言特定)
|
||||
cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # 选择你的技术栈
|
||||
cp -r everything-claude-code/rules/python/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/perl/* ~/.claude/rules/
|
||||
# 复制规则目录(通用 + 语言特定)
|
||||
mkdir -p ~/.claude/rules
|
||||
cp -r everything-claude-code/rules/common ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/typescript ~/.claude/rules/ # 选择你的技术栈
|
||||
cp -r everything-claude-code/rules/python ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/golang ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/perl ~/.claude/rules/
|
||||
```
|
||||
|
||||
复制规则时,请复制整个目录(例如 `rules/common`、`rules/golang`),而不是复制目录内的文件;这样可以保留相对引用,并避免不同规则集中的同名文件互相覆盖。
|
||||
|
||||
### 第三步:开始使用
|
||||
|
||||
```bash
|
||||
@@ -105,6 +108,20 @@ cp -r everything-claude-code/rules/perl/* ~/.claude/rules/
|
||||
|
||||
✨ **完成!** 你现在可以使用 13 个代理、43 个技能和 31 个命令。
|
||||
|
||||
### multi-* 命令需要额外配置
|
||||
|
||||
> ⚠️ 上面的基础插件 / rules 安装**不包含** `multi-*` 命令所需的运行时。
|
||||
>
|
||||
> 如果要使用 `/multi-plan`、`/multi-execute`、`/multi-backend`、`/multi-frontend` 和 `/multi-workflow`,还需要额外安装 `ccg-workflow` 运行时。
|
||||
>
|
||||
> 可通过 `npx ccg-workflow` 完成初始化安装。
|
||||
>
|
||||
> 该运行时会提供这些命令依赖的关键组件,包括:
|
||||
> - `~/.claude/bin/codeagent-wrapper`
|
||||
> - `~/.claude/.ccg/prompts/*`
|
||||
>
|
||||
> 未安装 `ccg-workflow` 时,这些 `multi-*` 命令将无法正常运行。
|
||||
|
||||
---
|
||||
|
||||
## 🌐 跨平台支持
|
||||
@@ -352,11 +369,20 @@ everything-claude-code/
|
||||
> git clone https://github.com/affaan-m/everything-claude-code.git
|
||||
>
|
||||
> # 选项 A:用户级规则(应用于所有项目)
|
||||
> cp -r everything-claude-code/rules/* ~/.claude/rules/
|
||||
> mkdir -p ~/.claude/rules
|
||||
> cp -r everything-claude-code/rules/common ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/typescript ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/python ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/golang ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/perl ~/.claude/rules/
|
||||
>
|
||||
> # 选项 B:项目级规则(仅应用于当前项目)
|
||||
> mkdir -p .claude/rules
|
||||
> cp -r everything-claude-code/rules/* .claude/rules/
|
||||
> cp -r everything-claude-code/rules/common .claude/rules/
|
||||
> cp -r everything-claude-code/rules/typescript .claude/rules/
|
||||
> cp -r everything-claude-code/rules/python .claude/rules/
|
||||
> cp -r everything-claude-code/rules/golang .claude/rules/
|
||||
> cp -r everything-claude-code/rules/perl .claude/rules/
|
||||
> ```
|
||||
|
||||
---
|
||||
@@ -372,12 +398,13 @@ git clone https://github.com/affaan-m/everything-claude-code.git
|
||||
# 将代理复制到你的 Claude 配置
|
||||
cp everything-claude-code/agents/*.md ~/.claude/agents/
|
||||
|
||||
# 复制规则(通用 + 语言特定)
|
||||
cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # 选择你的技术栈
|
||||
cp -r everything-claude-code/rules/python/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/perl/* ~/.claude/rules/
|
||||
# 复制规则目录(通用 + 语言特定)
|
||||
mkdir -p ~/.claude/rules
|
||||
cp -r everything-claude-code/rules/common ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/typescript ~/.claude/rules/ # 选择你的技术栈
|
||||
cp -r everything-claude-code/rules/python ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/golang ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/perl ~/.claude/rules/
|
||||
|
||||
# 复制命令
|
||||
cp everything-claude-code/commands/*.md ~/.claude/commands/
|
||||
|
||||
196
REPO-ASSESSMENT.md
Normal file
196
REPO-ASSESSMENT.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Repo & Fork Assessment + Setup Recommendations
|
||||
|
||||
**Date:** 2026-03-21
|
||||
|
||||
---
|
||||
|
||||
## What's Available
|
||||
|
||||
### Repo: `Infiniteyieldai/everything-claude-code`
|
||||
|
||||
This is a **fork of `affaan-m/everything-claude-code`** (the upstream project with 50K+ stars, 6K+ forks).
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| Version | 1.9.0 (current) |
|
||||
| Status | Clean fork — 1 commit ahead of upstream `main` (the EVALUATION.md doc added in this session) |
|
||||
| Remote branches | `main`, `claude/evaluate-repo-comparison-ASZ9Y` |
|
||||
| Upstream sync | Fully synced — last upstream commit merged was the zh-CN docs PR (#728) |
|
||||
| License | MIT |
|
||||
|
||||
**This is the right repo to work from.** It's the latest upstream version with no divergence or merge conflicts.
|
||||
|
||||
---
|
||||
|
||||
### Current `~/.claude/` Installation
|
||||
|
||||
| Component | Installed | Available in Repo |
|
||||
|-----------|-----------|-------------------|
|
||||
| Agents | 0 | 28 |
|
||||
| Skills | 0 | 116 |
|
||||
| Commands | 0 | 59 |
|
||||
| Rules | 0 | 60+ files (12 languages) |
|
||||
| Hooks | 1 (git Stop check) | Full PreToolUse/PostToolUse matrix |
|
||||
| MCP configs | 0 | 1 (Context7) |
|
||||
|
||||
The existing Stop hook (`stop-hook-git-check.sh`) is solid — blocks session end on uncommitted/unpushed work. Keep it.
|
||||
|
||||
---
|
||||
|
||||
## Install Profile Recommendations
|
||||
|
||||
The repo ships 5 install profiles. Choose based on your primary use case:
|
||||
|
||||
### Profile: `core` (Minimum viable setup)
|
||||
> Fastest to install. Gets you commands, core agents, hooks runtime, and quality workflow.
|
||||
|
||||
**Best for:** Trying ECC out, minimal footprint, or a constrained environment.
|
||||
|
||||
```bash
|
||||
node scripts/install-plan.js --profile core
|
||||
node scripts/install-apply.js
|
||||
```
|
||||
|
||||
**Installs:** rules-core, agents-core, commands-core, hooks-runtime, platform-configs, workflow-quality
|
||||
|
||||
---
|
||||
|
||||
### Profile: `developer` (Recommended for daily dev work)
|
||||
> The default engineering profile for most ECC users.
|
||||
|
||||
**Best for:** General software development across app codebases.
|
||||
|
||||
```bash
|
||||
node scripts/install-plan.js --profile developer
|
||||
node scripts/install-apply.js
|
||||
```
|
||||
|
||||
**Adds over core:** framework-language skills, database patterns, orchestration commands
|
||||
|
||||
---
|
||||
|
||||
### Profile: `security`
|
||||
> Baseline runtime + security-specific agents and rules.
|
||||
|
||||
**Best for:** Security-focused workflows, code audits, vulnerability reviews.
|
||||
|
||||
---
|
||||
|
||||
### Profile: `research`
|
||||
> Investigation, synthesis, and publishing workflows.
|
||||
|
||||
**Best for:** Content creation, investor materials, market research, cross-posting.
|
||||
|
||||
---
|
||||
|
||||
### Profile: `full`
|
||||
> Everything — all 18 modules.
|
||||
|
||||
**Best for:** Power users who want the complete toolkit.
|
||||
|
||||
```bash
|
||||
node scripts/install-plan.js --profile full
|
||||
node scripts/install-apply.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Priority Additions (High Value, Low Risk)
|
||||
|
||||
Regardless of profile, these components add immediate value:
|
||||
|
||||
### 1. Core Agents (highest ROI)
|
||||
|
||||
| Agent | Why it matters |
|
||||
|-------|----------------|
|
||||
| `planner.md` | Breaks complex tasks into implementation plans |
|
||||
| `code-reviewer.md` | Quality and maintainability review |
|
||||
| `tdd-guide.md` | TDD workflow (RED→GREEN→IMPROVE) |
|
||||
| `security-reviewer.md` | Vulnerability detection |
|
||||
| `architect.md` | System design & scalability decisions |
|
||||
|
||||
### 2. Key Commands
|
||||
|
||||
| Command | Why it matters |
|
||||
|---------|----------------|
|
||||
| `/plan` | Implementation planning before coding |
|
||||
| `/tdd` | Test-driven workflow |
|
||||
| `/code-review` | On-demand review |
|
||||
| `/build-fix` | Automated build error resolution |
|
||||
| `/learn` | Extract patterns from current session |
|
||||
|
||||
### 3. Hook Upgrades (from `hooks/hooks.json`)
|
||||
The repo's hook system adds these over the current single Stop hook:
|
||||
|
||||
| Hook | Trigger | Value |
|
||||
|------|---------|-------|
|
||||
| `block-no-verify` | PreToolUse: Bash | Blocks `--no-verify` git flag abuse |
|
||||
| `pre-bash-git-push-reminder` | PreToolUse: Bash | Pre-push review reminder |
|
||||
| `doc-file-warning` | PreToolUse: Write | Warns on non-standard doc files |
|
||||
| `suggest-compact` | PreToolUse: Edit/Write | Suggests compaction at logical intervals |
|
||||
| Continuous learning observer | PreToolUse: * | Captures tool use patterns for skill improvement |
|
||||
|
||||
### 4. Rules (Always-on guidelines)
|
||||
The `rules/common/` directory provides baseline guidelines that fire on every session:
|
||||
- `security.md` — Security guardrails
|
||||
- `testing.md` — 80%+ coverage requirement
|
||||
- `git-workflow.md` — Conventional commits, branch strategy
|
||||
- `coding-style.md` — Cross-language style standards
|
||||
|
||||
---
|
||||
|
||||
## What to Do With the Fork
|
||||
|
||||
### Option A: Use as upstream tracker (current state)
|
||||
Keep the fork synced with `affaan-m/everything-claude-code` upstream. Periodically merge upstream changes:
|
||||
```bash
|
||||
git fetch upstream
|
||||
git merge upstream/main
|
||||
```
|
||||
Install from the local clone. This is clean and maintainable.
|
||||
|
||||
### Option B: Customize the fork
|
||||
Add personal skills, agents, or commands to the fork. Good for:
|
||||
- Business-specific domain skills (your vertical)
|
||||
- Team-specific coding conventions
|
||||
- Custom hooks for your stack
|
||||
|
||||
The fork already has the EVALUATION.md and REPO-ASSESSMENT.md docs — that's fine for a working fork.
|
||||
|
||||
### Option C: Install from npm (simplest for fresh machines)
|
||||
```bash
|
||||
npx ecc-universal install --profile developer
|
||||
```
|
||||
No need to clone the repo. This is the recommended install method for most users.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Setup Steps
|
||||
|
||||
1. **Keep the existing Stop hook** — it's doing its job
|
||||
2. **Run the developer profile install** from the local fork:
|
||||
```bash
|
||||
cd /path/to/everything-claude-code
|
||||
node scripts/install-plan.js --profile developer
|
||||
node scripts/install-apply.js
|
||||
```
|
||||
3. **Add language rules** for your primary stack (TypeScript, Python, Go, etc.):
|
||||
```bash
|
||||
node scripts/install-plan.js --add rules/typescript
|
||||
node scripts/install-apply.js
|
||||
```
|
||||
4. **Enable MCP Context7** for live documentation lookup:
|
||||
- Copy `mcp-configs/mcp-servers.json` into your project's `.claude/` dir
|
||||
5. **Review hooks** — enable the `hooks/hooks.json` additions selectively, starting with `block-no-verify` and `pre-bash-git-push-reminder`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Question | Answer |
|
||||
|----------|--------|
|
||||
| Is the fork healthy? | Yes — fully synced with upstream v1.9.0 |
|
||||
| Other forks to consider? | None visible in this environment; upstream `affaan-m/everything-claude-code` is the source of truth |
|
||||
| Best install profile? | `developer` for day-to-day dev work |
|
||||
| Biggest gap in current setup? | 0 agents installed — add at minimum: planner, code-reviewer, tdd-guide, security-reviewer |
|
||||
| Quickest win? | Run `node scripts/install-plan.js --profile core && node scripts/install-apply.js` |
|
||||
83
agents/healthcare-reviewer.md
Normal file
83
agents/healthcare-reviewer.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: healthcare-reviewer
|
||||
description: Reviews healthcare application code for clinical safety, CDSS accuracy, PHI compliance, and medical data integrity. Specialized for EMR/EHR, clinical decision support, and health information systems.
|
||||
tools: ["Read", "Grep", "Glob"]
|
||||
model: opus
|
||||
---
|
||||
|
||||
# Healthcare Reviewer — Clinical Safety & PHI Compliance
|
||||
|
||||
You are a clinical informatics reviewer for healthcare software. Patient safety is your top priority. You review code for clinical accuracy, data protection, and regulatory compliance.
|
||||
|
||||
## Your Responsibilities
|
||||
|
||||
1. **CDSS accuracy** — Verify drug interaction logic, dose validation rules, and clinical scoring implementations match published medical standards
|
||||
2. **PHI/PII protection** — Scan for patient data exposure in logs, errors, responses, URLs, and client storage
|
||||
3. **Clinical data integrity** — Ensure audit trails, locked records, and cascade protection
|
||||
4. **Medical data correctness** — Verify ICD-10/SNOMED mappings, lab reference ranges, and drug database entries
|
||||
5. **Integration compliance** — Validate HL7/FHIR message handling and error recovery
|
||||
|
||||
## Critical Checks
|
||||
|
||||
### CDSS Engine
|
||||
|
||||
- [ ] All drug interaction pairs produce correct alerts (both directions)
|
||||
- [ ] Dose validation rules fire on out-of-range values
|
||||
- [ ] Clinical scoring matches published specification (NEWS2 = Royal College of Physicians, qSOFA = Sepsis-3)
|
||||
- [ ] No false negatives (missed interaction = patient safety event)
|
||||
- [ ] Malformed inputs produce errors, NOT silent passes
|
||||
|
||||
### PHI Protection
|
||||
|
||||
- [ ] No patient data in `console.log`, `console.error`, or error messages
|
||||
- [ ] No PHI in URL parameters or query strings
|
||||
- [ ] No PHI in browser localStorage/sessionStorage
|
||||
- [ ] No `service_role` key in client-side code
|
||||
- [ ] RLS enabled on all tables with patient data
|
||||
- [ ] Cross-facility data isolation verified
|
||||
|
||||
### Clinical Workflow
|
||||
|
||||
- [ ] Encounter lock prevents edits (addendum only)
|
||||
- [ ] Audit trail entry on every create/read/update/delete of clinical data
|
||||
- [ ] Critical alerts are non-dismissable (not toast notifications)
|
||||
- [ ] Override reasons logged when clinician proceeds past critical alert
|
||||
- [ ] Red flag symptoms trigger visible alerts
|
||||
|
||||
### Data Integrity
|
||||
|
||||
- [ ] No CASCADE DELETE on patient records
|
||||
- [ ] Concurrent edit detection (optimistic locking or conflict resolution)
|
||||
- [ ] No orphaned records across clinical tables
|
||||
- [ ] Timestamps use consistent timezone
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
## Healthcare Review: [module/feature]
|
||||
|
||||
### Patient Safety Impact: [CRITICAL / HIGH / MEDIUM / LOW / NONE]
|
||||
|
||||
### Clinical Accuracy
|
||||
- CDSS: [checks passed/failed]
|
||||
- Drug DB: [verified/issues]
|
||||
- Scoring: [matches spec/deviates]
|
||||
|
||||
### PHI Compliance
|
||||
- Exposure vectors checked: [list]
|
||||
- Issues found: [list or none]
|
||||
|
||||
### Issues
|
||||
1. [PATIENT SAFETY / CLINICAL / PHI / TECHNICAL] Description
|
||||
- Impact: [potential harm or exposure]
|
||||
- Fix: [required change]
|
||||
|
||||
### Verdict: [SAFE TO DEPLOY / NEEDS FIXES / BLOCK — PATIENT SAFETY RISK]
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- When in doubt about clinical accuracy, flag as NEEDS REVIEW — never approve uncertain clinical logic
|
||||
- A single missed drug interaction is worse than a hundred false alarms
|
||||
- PHI exposure is always CRITICAL severity, regardless of how small the leak
|
||||
- Never approve code that silently catches CDSS errors
|
||||
446
agents/performance-optimizer.md
Normal file
446
agents/performance-optimizer.md
Normal file
@@ -0,0 +1,446 @@
|
||||
---
|
||||
name: performance-optimizer
|
||||
description: Performance analysis and optimization specialist. Use PROACTIVELY for identifying bottlenecks, optimizing slow code, reducing bundle sizes, and improving runtime performance. Profiling, memory leaks, render optimization, and algorithmic improvements.
|
||||
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Performance Optimizer
|
||||
|
||||
You are an expert performance specialist focused on identifying bottlenecks and optimizing application speed, memory usage, and efficiency. Your mission is to make code faster, lighter, and more responsive.
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
1. **Performance Profiling** — Identify slow code paths, memory leaks, and bottlenecks
|
||||
2. **Bundle Optimization** — Reduce JavaScript bundle sizes, lazy loading, code splitting
|
||||
3. **Runtime Optimization** — Improve algorithmic efficiency, reduce unnecessary computations
|
||||
4. **React/Rendering Optimization** — Prevent unnecessary re-renders, optimize component trees
|
||||
5. **Database & Network** — Optimize queries, reduce API calls, implement caching
|
||||
6. **Memory Management** — Detect leaks, optimize memory usage, cleanup resources
|
||||
|
||||
## Analysis Commands
|
||||
|
||||
```bash
|
||||
# Bundle analysis
|
||||
npx bundle-analyzer
|
||||
npx source-map-explorer build/static/js/*.js
|
||||
|
||||
# Lighthouse performance audit
|
||||
npx lighthouse https://your-app.com --view
|
||||
|
||||
# Node.js profiling
|
||||
node --prof your-app.js
|
||||
node --prof-process isolate-*.log
|
||||
|
||||
# Memory analysis
|
||||
node --inspect your-app.js # Then use Chrome DevTools
|
||||
|
||||
# React profiling (in browser)
|
||||
# React DevTools > Profiler tab
|
||||
|
||||
# Network analysis
|
||||
npx webpack-bundle-analyzer
|
||||
```
|
||||
|
||||
## Performance Review Workflow
|
||||
|
||||
### 1. Identify Performance Issues
|
||||
|
||||
**Critical Performance Indicators:**
|
||||
|
||||
| Metric | Target | Action if Exceeded |
|
||||
|--------|--------|-------------------|
|
||||
| First Contentful Paint | < 1.8s | Optimize critical path, inline critical CSS |
|
||||
| Largest Contentful Paint | < 2.5s | Lazy load images, optimize server response |
|
||||
| Time to Interactive | < 3.8s | Code splitting, reduce JavaScript |
|
||||
| Cumulative Layout Shift | < 0.1 | Reserve space for images, avoid layout thrashing |
|
||||
| Total Blocking Time | < 200ms | Break up long tasks, use web workers |
|
||||
| Bundle Size (gzipped) | < 200KB | Tree shaking, lazy loading, code splitting |
|
||||
|
||||
### 2. Algorithmic Analysis
|
||||
|
||||
Check for inefficient algorithms:
|
||||
|
||||
| Pattern | Complexity | Better Alternative |
|
||||
|---------|------------|-------------------|
|
||||
| Nested loops on same data | O(n²) | Use Map/Set for O(1) lookups |
|
||||
| Repeated array searches | O(n) per search | Convert to Map for O(1) |
|
||||
| Sorting inside loop | O(n² log n) | Sort once outside loop |
|
||||
| String concatenation in loop | O(n²) | Use array.join() |
|
||||
| Deep cloning large objects | O(n) each time | Use shallow copy or immer |
|
||||
| Recursion without memoization | O(2^n) | Add memoization |
|
||||
|
||||
```typescript
|
||||
// BAD: O(n²) - searching array in loop
|
||||
for (const user of users) {
|
||||
const posts = allPosts.filter(p => p.userId === user.id); // O(n) per user
|
||||
}
|
||||
|
||||
// GOOD: O(n) - group once with Map
|
||||
const postsByUser = new Map<number, Post[]>();
|
||||
for (const post of allPosts) {
|
||||
const userPosts = postsByUser.get(post.userId) || [];
|
||||
userPosts.push(post);
|
||||
postsByUser.set(post.userId, userPosts);
|
||||
}
|
||||
// Now O(1) lookup per user
|
||||
```
|
||||
|
||||
### 3. React Performance Optimization
|
||||
|
||||
**Common React Anti-patterns:**
|
||||
|
||||
```tsx
|
||||
// BAD: Inline function creation in render
|
||||
<Button onClick={() => handleClick(id)}>Submit</Button>
|
||||
|
||||
// GOOD: Stable callback with useCallback
|
||||
const handleButtonClick = useCallback(() => handleClick(id), [handleClick, id]);
|
||||
<Button onClick={handleButtonClick}>Submit</Button>
|
||||
|
||||
// BAD: Object creation in render
|
||||
<Child style={{ color: 'red' }} />
|
||||
|
||||
// GOOD: Stable object reference
|
||||
const style = useMemo(() => ({ color: 'red' }), []);
|
||||
<Child style={style} />
|
||||
|
||||
// BAD: Expensive computation on every render
|
||||
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// GOOD: Memoize expensive computations
|
||||
const sortedItems = useMemo(
|
||||
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[items]
|
||||
);
|
||||
|
||||
// BAD: List without keys or with index
|
||||
{items.map((item, index) => <Item key={index} />)}
|
||||
|
||||
// GOOD: Stable unique keys
|
||||
{items.map(item => <Item key={item.id} item={item} />)}
|
||||
```
|
||||
|
||||
**React Performance Checklist:**
|
||||
|
||||
- [ ] `useMemo` for expensive computations
|
||||
- [ ] `useCallback` for functions passed to children
|
||||
- [ ] `React.memo` for frequently re-rendered components
|
||||
- [ ] Proper dependency arrays in hooks
|
||||
- [ ] Virtualization for long lists (react-window, react-virtualized)
|
||||
- [ ] Lazy loading for heavy components (`React.lazy`)
|
||||
- [ ] Code splitting at route level
|
||||
|
||||
### 4. Bundle Size Optimization
|
||||
|
||||
**Bundle Analysis Checklist:**
|
||||
|
||||
```bash
|
||||
# Analyze bundle composition
|
||||
npx webpack-bundle-analyzer build/static/js/*.js
|
||||
|
||||
# Check for duplicate dependencies
|
||||
npx duplicate-package-checker-analyzer
|
||||
|
||||
# Find largest files
|
||||
du -sh node_modules/* | sort -hr | head -20
|
||||
```
|
||||
|
||||
**Optimization Strategies:**
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Large vendor bundle | Tree shaking, smaller alternatives |
|
||||
| Duplicate code | Extract to shared module |
|
||||
| Unused exports | Remove dead code with knip |
|
||||
| Moment.js | Use date-fns or dayjs (smaller) |
|
||||
| Lodash | Use lodash-es or native methods |
|
||||
| Large icons library | Import only needed icons |
|
||||
|
||||
```javascript
|
||||
// BAD: Import entire library
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
// GOOD: Import only what you need
|
||||
import debounce from 'lodash/debounce';
|
||||
import { format, addDays } from 'date-fns';
|
||||
|
||||
// Or use lodash-es with tree shaking
|
||||
import { debounce, throttle } from 'lodash-es';
|
||||
```
|
||||
|
||||
### 5. Database & Query Optimization
|
||||
|
||||
**Query Optimization Patterns:**
|
||||
|
||||
```sql
|
||||
-- BAD: Select all columns
|
||||
SELECT * FROM users WHERE active = true;
|
||||
|
||||
-- GOOD: Select only needed columns
|
||||
SELECT id, name, email FROM users WHERE active = true;
|
||||
|
||||
-- BAD: N+1 queries (in application loop)
|
||||
-- 1 query for users, then N queries for each user's orders
|
||||
|
||||
-- GOOD: Single query with JOIN or batch fetch
|
||||
SELECT u.*, o.id as order_id, o.total
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.active = true;
|
||||
|
||||
-- Add index for frequently queried columns
|
||||
CREATE INDEX idx_users_active ON users(active);
|
||||
CREATE INDEX idx_orders_user_id ON orders(user_id);
|
||||
```
|
||||
|
||||
**Database Performance Checklist:**
|
||||
|
||||
- [ ] Indexes on frequently queried columns
|
||||
- [ ] Composite indexes for multi-column queries
|
||||
- [ ] Avoid SELECT * in production code
|
||||
- [ ] Use connection pooling
|
||||
- [ ] Implement query result caching
|
||||
- [ ] Use pagination for large result sets
|
||||
- [ ] Monitor slow query logs
|
||||
|
||||
### 6. Network & API Optimization
|
||||
|
||||
**Network Optimization Strategies:**
|
||||
|
||||
```typescript
|
||||
// BAD: Multiple sequential requests
|
||||
const user = await fetchUser(id);
|
||||
const posts = await fetchPosts(user.id);
|
||||
const comments = await fetchComments(posts[0].id);
|
||||
|
||||
// GOOD: Parallel requests when independent
|
||||
const [user, posts] = await Promise.all([
|
||||
fetchUser(id),
|
||||
fetchPosts(id)
|
||||
]);
|
||||
|
||||
// GOOD: Batch requests when possible
|
||||
const results = await batchFetch(['user1', 'user2', 'user3']);
|
||||
|
||||
// Implement request caching
|
||||
const fetchWithCache = async (url: string, ttl = 300000) => {
|
||||
const cached = cache.get(url);
|
||||
if (cached) return cached;
|
||||
|
||||
const data = await fetch(url).then(r => r.json());
|
||||
cache.set(url, data, ttl);
|
||||
return data;
|
||||
};
|
||||
|
||||
// Debounce rapid API calls
|
||||
const debouncedSearch = debounce(async (query: string) => {
|
||||
const results = await searchAPI(query);
|
||||
setResults(results);
|
||||
}, 300);
|
||||
```
|
||||
|
||||
**Network Optimization Checklist:**
|
||||
|
||||
- [ ] Parallel independent requests with `Promise.all`
|
||||
- [ ] Implement request caching
|
||||
- [ ] Debounce rapid-fire requests
|
||||
- [ ] Use streaming for large responses
|
||||
- [ ] Implement pagination for large datasets
|
||||
- [ ] Use GraphQL or API batching to reduce requests
|
||||
- [ ] Enable compression (gzip/brotli) on server
|
||||
|
||||
### 7. Memory Leak Detection
|
||||
|
||||
**Common Memory Leak Patterns:**
|
||||
|
||||
```typescript
|
||||
// BAD: Event listener without cleanup
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
// Missing cleanup!
|
||||
}, []);
|
||||
|
||||
// GOOD: Clean up event listeners
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// BAD: Timer without cleanup
|
||||
useEffect(() => {
|
||||
setInterval(() => pollData(), 1000);
|
||||
// Missing cleanup!
|
||||
}, []);
|
||||
|
||||
// GOOD: Clean up timers
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => pollData(), 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// BAD: Holding references in closures
|
||||
const Component = () => {
|
||||
const largeData = useLargeData();
|
||||
useEffect(() => {
|
||||
eventEmitter.on('update', () => {
|
||||
console.log(largeData); // Closure keeps reference
|
||||
});
|
||||
}, [largeData]);
|
||||
};
|
||||
|
||||
// GOOD: Use refs or proper dependencies
|
||||
const largeDataRef = useRef(largeData);
|
||||
useEffect(() => {
|
||||
largeDataRef.current = largeData;
|
||||
}, [largeData]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleUpdate = () => {
|
||||
console.log(largeDataRef.current);
|
||||
};
|
||||
eventEmitter.on('update', handleUpdate);
|
||||
return () => eventEmitter.off('update', handleUpdate);
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Memory Leak Detection:**
|
||||
|
||||
```bash
|
||||
# Chrome DevTools Memory tab:
|
||||
# 1. Take heap snapshot
|
||||
# 2. Perform action
|
||||
# 3. Take another snapshot
|
||||
# 4. Compare to find objects that shouldn't exist
|
||||
# 5. Look for detached DOM nodes, event listeners, closures
|
||||
|
||||
# Node.js memory debugging
|
||||
node --inspect app.js
|
||||
# Open chrome://inspect
|
||||
# Take heap snapshots and compare
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
|
||||
### Lighthouse Audits
|
||||
|
||||
```bash
|
||||
# Run full lighthouse audit
|
||||
npx lighthouse https://your-app.com --view --preset=desktop
|
||||
|
||||
# CI mode for automated checks
|
||||
npx lighthouse https://your-app.com --output=json --output-path=./lighthouse.json
|
||||
|
||||
# Check specific metrics
|
||||
npx lighthouse https://your-app.com --only-categories=performance
|
||||
```
|
||||
|
||||
### Performance Budgets
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "./build/static/js/*.js",
|
||||
"maxSize": "200 kB"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Web Vitals Monitoring
|
||||
|
||||
```typescript
|
||||
// Track Core Web Vitals
|
||||
import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals';
|
||||
|
||||
getCLS(console.log); // Cumulative Layout Shift
|
||||
getFID(console.log); // First Input Delay
|
||||
getLCP(console.log); // Largest Contentful Paint
|
||||
getFCP(console.log); // First Contentful Paint
|
||||
getTTFB(console.log); // Time to First Byte
|
||||
```
|
||||
|
||||
## Performance Report Template
|
||||
|
||||
````markdown
|
||||
# Performance Audit Report
|
||||
|
||||
## Executive Summary
|
||||
- **Overall Score**: X/100
|
||||
- **Critical Issues**: X
|
||||
- **Recommendations**: X
|
||||
|
||||
## Bundle Analysis
|
||||
| Metric | Current | Target | Status |
|
||||
|--------|---------|--------|--------|
|
||||
| Total Size (gzip) | XXX KB | < 200 KB | ⚠️ |
|
||||
| Main Bundle | XXX KB | < 100 KB | ✅ |
|
||||
| Vendor Bundle | XXX KB | < 150 KB | ⚠️ |
|
||||
|
||||
## Web Vitals
|
||||
| Metric | Current | Target | Status |
|
||||
|--------|---------|--------|--------|
|
||||
| LCP | X.Xs | < 2.5s | ✅ |
|
||||
| FID | XXms | < 100ms | ✅ |
|
||||
| CLS | X.XX | < 0.1 | ⚠️ |
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### 1. [Issue Title]
|
||||
**File**: path/to/file.ts:42
|
||||
**Impact**: High - Causes XXXms delay
|
||||
**Fix**: [Description of fix]
|
||||
|
||||
```typescript
|
||||
// Before (slow)
|
||||
const slowCode = ...;
|
||||
|
||||
// After (optimized)
|
||||
const fastCode = ...;
|
||||
```
|
||||
|
||||
### 2. [Issue Title]
|
||||
...
|
||||
|
||||
## Recommendations
|
||||
1. [Priority recommendation]
|
||||
2. [Priority recommendation]
|
||||
3. [Priority recommendation]
|
||||
|
||||
## Estimated Impact
|
||||
- Bundle size reduction: XX KB (XX%)
|
||||
- LCP improvement: XXms
|
||||
- Time to Interactive improvement: XXms
|
||||
````
|
||||
|
||||
## When to Run
|
||||
|
||||
**ALWAYS:** Before major releases, after adding new features, when users report slowness, during performance regression testing.
|
||||
|
||||
**IMMEDIATELY:** Lighthouse score drops, bundle size increases >10%, memory usage grows, slow page loads.
|
||||
|
||||
## Red Flags - Act Immediately
|
||||
|
||||
| Issue | Action |
|
||||
|-------|--------|
|
||||
| Bundle > 500KB gzip | Code split, lazy load, tree shake |
|
||||
| LCP > 4s | Optimize critical path, preload resources |
|
||||
| Memory usage growing | Check for leaks, review useEffect cleanup |
|
||||
| CPU spikes | Profile with Chrome DevTools |
|
||||
| Database query > 1s | Add index, optimize query, cache results |
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- Lighthouse performance score > 90
|
||||
- All Core Web Vitals in "good" range
|
||||
- Bundle size under budget
|
||||
- No memory leaks detected
|
||||
- Test suite still passing
|
||||
- No performance regressions
|
||||
|
||||
---
|
||||
|
||||
**Remember**: Performance is a feature. Users notice speed. Every 100ms of improvement matters. Optimize for the 90th percentile, not the average.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Load the most recent session file from ~/.claude/sessions/ and resume work with full context from where the last session ended.
|
||||
description: Load the most recent session file from ~/.claude/session-data/ and resume work with full context from where the last session ended.
|
||||
---
|
||||
|
||||
# Resume Session Command
|
||||
@@ -17,10 +17,10 @@ This command is the counterpart to `/save-session`.
|
||||
## Usage
|
||||
|
||||
```
|
||||
/resume-session # loads most recent file in ~/.claude/sessions/
|
||||
/resume-session # loads most recent file in ~/.claude/session-data/
|
||||
/resume-session 2024-01-15 # loads most recent session for that date
|
||||
/resume-session ~/.claude/sessions/2024-01-15-session.tmp # loads a specific legacy-format file
|
||||
/resume-session ~/.claude/sessions/2024-01-15-abc123de-session.tmp # loads a current short-id session file
|
||||
/resume-session ~/.claude/session-data/2024-01-15-abc123de-session.tmp # loads a current short-id session file
|
||||
/resume-session ~/.claude/sessions/2024-01-15-session.tmp # loads a specific legacy-format file
|
||||
```
|
||||
|
||||
## Process
|
||||
@@ -29,19 +29,20 @@ This command is the counterpart to `/save-session`.
|
||||
|
||||
If no argument provided:
|
||||
|
||||
1. Check `~/.claude/sessions/`
|
||||
1. Check `~/.claude/session-data/`
|
||||
2. Pick the most recently modified `*-session.tmp` file
|
||||
3. If the folder does not exist or has no matching files, tell the user:
|
||||
```
|
||||
No session files found in ~/.claude/sessions/
|
||||
No session files found in ~/.claude/session-data/
|
||||
Run /save-session at the end of a session to create one.
|
||||
```
|
||||
Then stop.
|
||||
|
||||
If an argument is provided:
|
||||
|
||||
- If it looks like a date (`YYYY-MM-DD`), search `~/.claude/sessions/` for files matching
|
||||
`YYYY-MM-DD-session.tmp` (legacy format) or `YYYY-MM-DD-<shortid>-session.tmp` (current format)
|
||||
- If it looks like a date (`YYYY-MM-DD`), search `~/.claude/session-data/` first, then the legacy
|
||||
`~/.claude/sessions/`, for files matching `YYYY-MM-DD-session.tmp` (legacy format) or
|
||||
`YYYY-MM-DD-<shortid>-session.tmp` (current format)
|
||||
and load the most recently modified variant for that date
|
||||
- If it looks like a file path, read that file directly
|
||||
- If not found, report clearly and stop
|
||||
@@ -114,7 +115,7 @@ Report: "Session file found but appears empty or unreadable. You may need to cre
|
||||
## Example Output
|
||||
|
||||
```
|
||||
SESSION LOADED: /Users/you/.claude/sessions/2024-01-15-abc123de-session.tmp
|
||||
SESSION LOADED: /Users/you/.claude/session-data/2024-01-15-abc123de-session.tmp
|
||||
════════════════════════════════════════════════
|
||||
|
||||
PROJECT: my-app — JWT Authentication
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Save current session state to a dated file in ~/.claude/sessions/ so work can be resumed in a future session with full context.
|
||||
description: Save current session state to a dated file in ~/.claude/session-data/ so work can be resumed in a future session with full context.
|
||||
---
|
||||
|
||||
# Save Session Command
|
||||
@@ -29,19 +29,19 @@ Before writing the file, collect:
|
||||
Create the canonical sessions folder in the user's Claude home directory:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.claude/sessions
|
||||
mkdir -p ~/.claude/session-data
|
||||
```
|
||||
|
||||
### Step 3: Write the session file
|
||||
|
||||
Create `~/.claude/sessions/YYYY-MM-DD-<short-id>-session.tmp`, using today's actual date and a short-id that satisfies the rules enforced by `SESSION_FILENAME_REGEX` in `session-manager.js`:
|
||||
Create `~/.claude/session-data/YYYY-MM-DD-<short-id>-session.tmp`, using today's actual date and a short-id that satisfies the rules enforced by `SESSION_FILENAME_REGEX` in `session-manager.js`:
|
||||
|
||||
- Allowed characters: lowercase `a-z`, digits `0-9`, hyphens `-`
|
||||
- Minimum length: 8 characters
|
||||
- No uppercase letters, no underscores, no spaces
|
||||
- Compatibility characters: letters `a-z` / `A-Z`, digits `0-9`, hyphens `-`, underscores `_`
|
||||
- Compatibility minimum length: 1 character
|
||||
- Recommended style for new files: lowercase letters, digits, and hyphens with 8+ characters to avoid collisions
|
||||
|
||||
Valid examples: `abc123de`, `a1b2c3d4`, `frontend-worktree-1`
|
||||
Invalid examples: `ABC123de` (uppercase), `short` (under 8 chars), `test_id1` (underscore)
|
||||
Valid examples: `abc123de`, `a1b2c3d4`, `frontend-worktree-1`, `ChezMoi_2`
|
||||
Avoid for new files: `A`, `test_id1`, `ABC123de`
|
||||
|
||||
Full valid filename example: `2024-01-15-abc123de-session.tmp`
|
||||
|
||||
@@ -271,5 +271,5 @@ Then test with Postman — the response should include a `Set-Cookie` header.
|
||||
- The "What Did NOT Work" section is the most critical — future sessions will blindly retry failed approaches without it
|
||||
- If the user asks to save mid-session (not just at the end), save what's known so far and mark in-progress items clearly
|
||||
- The file is meant to be read by Claude at the start of the next session via `/resume-session`
|
||||
- Use the canonical global session store: `~/.claude/sessions/`
|
||||
- Use the canonical global session store: `~/.claude/session-data/`
|
||||
- Prefer the short-id filename form (`YYYY-MM-DD-<short-id>-session.tmp`) for any new session file
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Manage Claude Code session history, aliases, and session metadata.
|
||||
|
||||
# Sessions Command
|
||||
|
||||
Manage Claude Code session history - list, load, alias, and edit sessions stored in `~/.claude/sessions/`.
|
||||
Manage Claude Code session history - list, load, alias, and edit sessions stored in `~/.claude/session-data/` with legacy reads from `~/.claude/sessions/`.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -89,7 +89,7 @@ const size = sm.getSessionSize(session.sessionPath);
|
||||
const aliases = aa.getAliasesForSession(session.filename);
|
||||
|
||||
console.log('Session: ' + session.filename);
|
||||
console.log('Path: ~/.claude/sessions/' + session.filename);
|
||||
console.log('Path: ' + session.sessionPath);
|
||||
console.log('');
|
||||
console.log('Statistics:');
|
||||
console.log(' Lines: ' + stats.lineCount);
|
||||
@@ -327,7 +327,7 @@ $ARGUMENTS:
|
||||
|
||||
## Notes
|
||||
|
||||
- Sessions are stored as markdown files in `~/.claude/sessions/`
|
||||
- Sessions are stored as markdown files in `~/.claude/session-data/` with legacy reads from `~/.claude/sessions/`
|
||||
- Aliases are stored in `~/.claude/session-aliases.json`
|
||||
- Session IDs can be shortened (first 4-8 characters usually unique enough)
|
||||
- Use aliases for frequently referenced sessions
|
||||
|
||||
919
docs/SKILL-DEVELOPMENT-GUIDE.md
Normal file
919
docs/SKILL-DEVELOPMENT-GUIDE.md
Normal file
@@ -0,0 +1,919 @@
|
||||
# Skill Development Guide
|
||||
|
||||
A comprehensive guide to creating effective skills for Everything Claude Code (ECC).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [What Are Skills?](#what-are-skills)
|
||||
- [Skill Architecture](#skill-architecture)
|
||||
- [Creating Your First Skill](#creating-your-first-skill)
|
||||
- [Skill Categories](#skill-categories)
|
||||
- [Writing Effective Skill Content](#writing-effective-skill-content)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Common Patterns](#common-patterns)
|
||||
- [Testing Your Skill](#testing-your-skill)
|
||||
- [Submitting Your Skill](#submitting-your-skill)
|
||||
- [Examples Gallery](#examples-gallery)
|
||||
|
||||
---
|
||||
|
||||
## What Are Skills?
|
||||
|
||||
Skills are **knowledge modules** that Claude Code loads based on context. They provide:
|
||||
|
||||
- **Domain expertise**: Framework patterns, language idioms, best practices
|
||||
- **Workflow definitions**: Step-by-step processes for common tasks
|
||||
- **Reference material**: Code snippets, checklists, decision trees
|
||||
- **Context injection**: Activate when specific conditions are met
|
||||
|
||||
Unlike **agents** (specialized subassistants) or **commands** (user-triggered actions), skills are passive knowledge that Claude Code references when relevant.
|
||||
|
||||
### When Skills Activate
|
||||
|
||||
Skills activate when:
|
||||
- The user's task matches the skill's domain
|
||||
- Claude Code detects relevant context
|
||||
- A command references a skill
|
||||
- An agent needs domain knowledge
|
||||
|
||||
### Skill vs Agent vs Command
|
||||
|
||||
| Component | Purpose | Activation |
|
||||
|-----------|---------|------------|
|
||||
| **Skill** | Knowledge repository | Context-based (automatic) |
|
||||
| **Agent** | Task executor | Explicit delegation |
|
||||
| **Command** | User action | User-invoked (`/command`) |
|
||||
| **Hook** | Automation | Event-triggered |
|
||||
| **Rule** | Always-on guidelines | Always active |
|
||||
|
||||
---
|
||||
|
||||
## Skill Architecture
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
skills/
|
||||
└── your-skill-name/
|
||||
├── SKILL.md # Required: Main skill definition
|
||||
├── examples/ # Optional: Code examples
|
||||
│ ├── basic.ts
|
||||
│ └── advanced.ts
|
||||
└── references/ # Optional: External references
|
||||
└── links.md
|
||||
```
|
||||
|
||||
### SKILL.md Format
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: skill-name
|
||||
description: Brief description shown in skill list and used for auto-activation
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Skill Title
|
||||
|
||||
Brief overview of what this skill covers.
|
||||
|
||||
## When to Activate
|
||||
|
||||
Describe scenarios where Claude should use this skill.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
Main patterns and guidelines.
|
||||
|
||||
## Code Examples
|
||||
|
||||
\`\`\`typescript
|
||||
// Practical, tested examples
|
||||
\`\`\`
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
Show what NOT to do with concrete examples.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Actionable guidelines
|
||||
- Do's and don'ts
|
||||
|
||||
## Related Skills
|
||||
|
||||
Link to complementary skills.
|
||||
```
|
||||
|
||||
### YAML Frontmatter Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `name` | Yes | Lowercase, hyphenated identifier (e.g., `react-patterns`) |
|
||||
| `description` | Yes | One-line description for skill list and auto-activation |
|
||||
| `origin` | No | Source identifier (e.g., `ECC`, `community`, project name) |
|
||||
| `tags` | No | Array of tags for categorization |
|
||||
| `version` | No | Skill version for tracking updates |
|
||||
|
||||
---
|
||||
|
||||
## Creating Your First Skill
|
||||
|
||||
### Step 1: Choose a Focus
|
||||
|
||||
Good skills are **focused and actionable**:
|
||||
|
||||
| ✅ Good Focus | ❌ Too Broad |
|
||||
|---------------|--------------|
|
||||
| `react-hook-patterns` | `react` |
|
||||
| `postgresql-indexing` | `databases` |
|
||||
| `pytest-fixtures` | `python-testing` |
|
||||
| `nextjs-app-router` | `nextjs` |
|
||||
|
||||
### Step 2: Create the Directory
|
||||
|
||||
```bash
|
||||
mkdir -p skills/your-skill-name
|
||||
```
|
||||
|
||||
### Step 3: Write SKILL.md
|
||||
|
||||
Here's a minimal template:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: your-skill-name
|
||||
description: Brief description of when to use this skill
|
||||
---
|
||||
|
||||
# Your Skill Title
|
||||
|
||||
Brief overview (1-2 sentences).
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Scenario 1
|
||||
- Scenario 2
|
||||
- Scenario 3
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Concept 1
|
||||
|
||||
Explanation with examples.
|
||||
|
||||
### Concept 2
|
||||
|
||||
Another pattern with code.
|
||||
|
||||
## Code Examples
|
||||
|
||||
\`\`\`typescript
|
||||
// Practical example
|
||||
\`\`\`
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Do this
|
||||
- Avoid that
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `related-skill-1`
|
||||
- `related-skill-2`
|
||||
```
|
||||
|
||||
### Step 4: Add Content
|
||||
|
||||
Write content that Claude can **immediately use**:
|
||||
|
||||
- ✅ Copy-pasteable code examples
|
||||
- ✅ Clear decision trees
|
||||
- ✅ Checklists for verification
|
||||
- ❌ Vague explanations without examples
|
||||
- ❌ Long prose without actionable guidance
|
||||
|
||||
---
|
||||
|
||||
## Skill Categories
|
||||
|
||||
### Language Standards
|
||||
|
||||
Focus on idiomatic code, naming conventions, and language-specific patterns.
|
||||
|
||||
**Examples:** `python-patterns`, `golang-patterns`, `typescript-standards`
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: python-patterns
|
||||
description: Python idioms, best practices, and patterns for clean, idiomatic code.
|
||||
---
|
||||
|
||||
# Python Patterns
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Writing Python code
|
||||
- Refactoring Python modules
|
||||
- Python code review
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Context Managers
|
||||
|
||||
\`\`\`python
|
||||
# Always use context managers for resources
|
||||
with open('file.txt') as f:
|
||||
content = f.read()
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### Framework Patterns
|
||||
|
||||
Focus on framework-specific conventions, common patterns, and anti-patterns.
|
||||
|
||||
**Examples:** `django-patterns`, `nextjs-patterns`, `springboot-patterns`
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: django-patterns
|
||||
description: Django best practices for models, views, URLs, and templates.
|
||||
---
|
||||
|
||||
# Django Patterns
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Building Django applications
|
||||
- Creating models and views
|
||||
- Django URL configuration
|
||||
```
|
||||
|
||||
### Workflow Skills
|
||||
|
||||
Define step-by-step processes for common development tasks.
|
||||
|
||||
**Examples:** `tdd-workflow`, `code-review-workflow`, `deployment-checklist`
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: code-review-workflow
|
||||
description: Systematic code review process for quality and security.
|
||||
---
|
||||
|
||||
# Code Review Workflow
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Understand Context** - Read PR description and linked issues
|
||||
2. **Check Tests** - Verify test coverage and quality
|
||||
3. **Review Logic** - Analyze implementation for correctness
|
||||
4. **Check Security** - Look for vulnerabilities
|
||||
5. **Verify Style** - Ensure code follows conventions
|
||||
```
|
||||
|
||||
### Domain Knowledge
|
||||
|
||||
Specialized knowledge for specific domains (security, performance, etc.).
|
||||
|
||||
**Examples:** `security-review`, `performance-optimization`, `api-design`
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: api-design
|
||||
description: REST and GraphQL API design patterns, versioning, and best practices.
|
||||
---
|
||||
|
||||
# API Design Patterns
|
||||
|
||||
## RESTful Conventions
|
||||
|
||||
| Method | Endpoint | Purpose |
|
||||
|--------|----------|---------|
|
||||
| GET | /resources | List all |
|
||||
| GET | /resources/:id | Get one |
|
||||
| POST | /resources | Create |
|
||||
```
|
||||
|
||||
### Tool Integration
|
||||
|
||||
Guidance for using specific tools, libraries, or services.
|
||||
|
||||
**Examples:** `supabase-patterns`, `docker-patterns`, `mcp-server-patterns`
|
||||
|
||||
---
|
||||
|
||||
## Writing Effective Skill Content
|
||||
|
||||
### 1. Start with "When to Activate"
|
||||
|
||||
This section is **critical** for auto-activation. Be specific:
|
||||
|
||||
```markdown
|
||||
## When to Activate
|
||||
|
||||
- Creating new React components
|
||||
- Refactoring existing components
|
||||
- Debugging React state issues
|
||||
- Reviewing React code for best practices
|
||||
```
|
||||
|
||||
### 2. Use "Show, Don't Tell"
|
||||
|
||||
Bad:
|
||||
```markdown
|
||||
## Error Handling
|
||||
|
||||
Always handle errors properly in async functions.
|
||||
```
|
||||
|
||||
Good:
|
||||
```markdown
|
||||
## Error Handling
|
||||
|
||||
\`\`\`typescript
|
||||
async function fetchData(url: string) {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(\`HTTP \${response.status}: \${response.statusText}\`)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Fetch failed:', error)
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Key Points
|
||||
|
||||
- Check \`response.ok\` before parsing
|
||||
- Log errors for debugging
|
||||
- Re-throw with user-friendly message
|
||||
```
|
||||
|
||||
### 3. Include Anti-Patterns
|
||||
|
||||
Show what NOT to do:
|
||||
|
||||
```markdown
|
||||
## Anti-Patterns
|
||||
|
||||
### ❌ Direct State Mutation
|
||||
|
||||
\`\`\`typescript
|
||||
// NEVER do this
|
||||
user.name = 'New Name'
|
||||
items.push(newItem)
|
||||
\`\`\`
|
||||
|
||||
### ✅ Immutable Updates
|
||||
|
||||
\`\`\`typescript
|
||||
// ALWAYS do this
|
||||
const updatedUser = { ...user, name: 'New Name' }
|
||||
const updatedItems = [...items, newItem]
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### 4. Provide Checklists
|
||||
|
||||
Checklists are actionable and easy to follow:
|
||||
|
||||
```markdown
|
||||
## Pre-Deployment Checklist
|
||||
|
||||
- [ ] All tests passing
|
||||
- [ ] No console.log in production code
|
||||
- [ ] Environment variables documented
|
||||
- [ ] Secrets not hardcoded
|
||||
- [ ] Error handling complete
|
||||
- [ ] Input validation in place
|
||||
```
|
||||
|
||||
### 5. Use Decision Trees
|
||||
|
||||
For complex decisions:
|
||||
|
||||
```markdown
|
||||
## Choosing the Right Approach
|
||||
|
||||
\`\`\`
|
||||
Need to fetch data?
|
||||
├── Single request → use fetch directly
|
||||
├── Multiple independent → Promise.all()
|
||||
├── Multiple dependent → await sequentially
|
||||
└── With caching → use SWR or React Query
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
| Practice | Example |
|
||||
|----------|---------|
|
||||
| **Be specific** | "Use \`useCallback\` for event handlers passed to child components" |
|
||||
| **Show examples** | Include copy-pasteable code |
|
||||
| **Explain WHY** | "Immutability prevents unexpected side effects in React state" |
|
||||
| **Link related skills** | "See also: \`react-performance\`" |
|
||||
| **Keep focused** | One skill = one domain/concept |
|
||||
| **Use sections** | Clear headers for easy scanning |
|
||||
|
||||
### DON'T
|
||||
|
||||
| Practice | Why It's Bad |
|
||||
|----------|--------------|
|
||||
| **Be vague** | "Write good code" - not actionable |
|
||||
| **Long prose** | Hard to parse, better as code |
|
||||
| **Cover too much** | "Python, Django, and Flask patterns" - too broad |
|
||||
| **Skip examples** | Theory without practice is less useful |
|
||||
| **Ignore anti-patterns** | Learning what NOT to do is valuable |
|
||||
|
||||
### Content Guidelines
|
||||
|
||||
1. **Length**: 200-500 lines typical, 800 lines maximum
|
||||
2. **Code blocks**: Include language identifier
|
||||
3. **Headers**: Use `##` and `###` hierarchy
|
||||
4. **Lists**: Use `-` for unordered, `1.` for ordered
|
||||
5. **Tables**: For comparisons and references
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Standards Skill
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: language-standards
|
||||
description: Coding standards and best practices for [language].
|
||||
---
|
||||
|
||||
# [Language] Coding Standards
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Writing [language] code
|
||||
- Code review
|
||||
- Setting up linting
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Element | Convention | Example |
|
||||
|---------|------------|---------|
|
||||
| Variables | camelCase | userName |
|
||||
| Constants | SCREAMING_SNAKE | MAX_RETRY |
|
||||
| Functions | camelCase | fetchUser |
|
||||
| Classes | PascalCase | UserService |
|
||||
|
||||
## Code Examples
|
||||
|
||||
[Include practical examples]
|
||||
|
||||
## Linting Setup
|
||||
|
||||
[Include configuration]
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `language-testing`
|
||||
- `language-security`
|
||||
```
|
||||
|
||||
### Pattern 2: Workflow Skill
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: task-workflow
|
||||
description: Step-by-step workflow for [task].
|
||||
---
|
||||
|
||||
# [Task] Workflow
|
||||
|
||||
## When to Activate
|
||||
|
||||
- [Trigger 1]
|
||||
- [Trigger 2]
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Requirement 1]
|
||||
- [Requirement 2]
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: [Name]
|
||||
|
||||
[Description]
|
||||
|
||||
\`\`\`bash
|
||||
[Commands]
|
||||
\`\`\`
|
||||
|
||||
### Step 2: [Name]
|
||||
|
||||
[Description]
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] [Check 1]
|
||||
- [ ] [Check 2]
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| [Issue] | [Fix] |
|
||||
```
|
||||
|
||||
### Pattern 3: Reference Skill
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: api-reference
|
||||
description: Quick reference for [API/Library].
|
||||
---
|
||||
|
||||
# [API/Library] Reference
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Using [API/Library]
|
||||
- Looking up [API/Library] syntax
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Operation 1
|
||||
|
||||
\`\`\`typescript
|
||||
// Basic usage
|
||||
\`\`\`
|
||||
|
||||
### Operation 2
|
||||
|
||||
\`\`\`typescript
|
||||
// Advanced usage
|
||||
\`\`\`
|
||||
|
||||
## Configuration
|
||||
|
||||
[Include config examples]
|
||||
|
||||
## Error Handling
|
||||
|
||||
[Include error patterns]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Your Skill
|
||||
|
||||
### Local Testing
|
||||
|
||||
1. **Copy to Claude Code skills directory**:
|
||||
```bash
|
||||
cp -r skills/your-skill-name ~/.claude/skills/
|
||||
```
|
||||
|
||||
2. **Test with Claude Code**:
|
||||
```
|
||||
You: "I need to [task that should trigger your skill]"
|
||||
|
||||
Claude should reference your skill's patterns.
|
||||
```
|
||||
|
||||
3. **Verify activation**:
|
||||
- Ask Claude to explain a concept from your skill
|
||||
- Check if it uses your examples and patterns
|
||||
- Ensure it follows your guidelines
|
||||
|
||||
### Validation Checklist
|
||||
|
||||
- [ ] **YAML frontmatter valid** - No syntax errors
|
||||
- [ ] **Name follows convention** - lowercase-with-hyphens
|
||||
- [ ] **Description is clear** - Tells when to use
|
||||
- [ ] **Examples work** - Code compiles and runs
|
||||
- [ ] **Links valid** - Related skills exist
|
||||
- [ ] **No sensitive data** - No API keys, tokens, paths
|
||||
|
||||
### Code Example Testing
|
||||
|
||||
Test all code examples:
|
||||
|
||||
```bash
|
||||
# From the repo root
|
||||
npx tsc --noEmit skills/your-skill-name/examples/*.ts
|
||||
|
||||
# Or from inside the skill directory
|
||||
npx tsc --noEmit examples/*.ts
|
||||
|
||||
# From the repo root
|
||||
python -m py_compile skills/your-skill-name/examples/*.py
|
||||
|
||||
# Or from inside the skill directory
|
||||
python -m py_compile examples/*.py
|
||||
|
||||
# From the repo root
|
||||
go build ./skills/your-skill-name/examples/...
|
||||
|
||||
# Or from inside the skill directory
|
||||
go build ./examples/...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Submitting Your Skill
|
||||
|
||||
### 1. Fork and Clone
|
||||
|
||||
```bash
|
||||
gh repo fork affaan-m/everything-claude-code --clone
|
||||
cd everything-claude-code
|
||||
```
|
||||
|
||||
### 2. Create Branch
|
||||
|
||||
```bash
|
||||
git checkout -b feat/skill-your-skill-name
|
||||
```
|
||||
|
||||
### 3. Add Your Skill
|
||||
|
||||
```bash
|
||||
mkdir -p skills/your-skill-name
|
||||
# Create SKILL.md
|
||||
```
|
||||
|
||||
### 4. Validate
|
||||
|
||||
```bash
|
||||
# Check YAML frontmatter
|
||||
head -10 skills/your-skill-name/SKILL.md
|
||||
|
||||
# Verify structure
|
||||
ls -la skills/your-skill-name/
|
||||
|
||||
# Run tests if available
|
||||
npm test
|
||||
```
|
||||
|
||||
### 5. Commit and Push
|
||||
|
||||
```bash
|
||||
git add skills/your-skill-name/
|
||||
git commit -m "feat(skills): add your-skill-name skill"
|
||||
git push -u origin feat/skill-your-skill-name
|
||||
```
|
||||
|
||||
### 6. Create Pull Request
|
||||
|
||||
Use this PR template:
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
Brief description of the skill and why it's valuable.
|
||||
|
||||
## Skill Type
|
||||
|
||||
- [ ] Language standards
|
||||
- [ ] Framework patterns
|
||||
- [ ] Workflow
|
||||
- [ ] Domain knowledge
|
||||
- [ ] Tool integration
|
||||
|
||||
## Testing
|
||||
|
||||
How I tested this skill locally.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] YAML frontmatter valid
|
||||
- [ ] Code examples tested
|
||||
- [ ] Follows skill guidelines
|
||||
- [ ] No sensitive data
|
||||
- [ ] Clear activation triggers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples Gallery
|
||||
|
||||
### Example 1: Language Standards
|
||||
|
||||
**File:** `skills/rust-patterns/SKILL.md`
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: rust-patterns
|
||||
description: Rust idioms, ownership patterns, and best practices for safe, idiomatic code.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Rust Patterns
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Writing Rust code
|
||||
- Handling ownership and borrowing
|
||||
- Error handling with Result/Option
|
||||
- Implementing traits
|
||||
|
||||
## Ownership Patterns
|
||||
|
||||
### Borrowing Rules
|
||||
|
||||
\`\`\`rust
|
||||
// ✅ CORRECT: Borrow when you don't need ownership
|
||||
fn process_data(data: &str) -> usize {
|
||||
data.len()
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Take ownership when you need to modify or consume
|
||||
fn consume_data(data: Vec<u8>) -> String {
|
||||
String::from_utf8(data).unwrap()
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Result Pattern
|
||||
|
||||
\`\`\`rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Parse error: {0}")]
|
||||
Parse(#[from] std::num::ParseIntError),
|
||||
}
|
||||
|
||||
pub type AppResult<T> = Result<T, AppError>;
|
||||
\`\`\`
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `rust-testing`
|
||||
- `rust-security`
|
||||
```
|
||||
|
||||
### Example 2: Framework Patterns
|
||||
|
||||
**File:** `skills/fastapi-patterns/SKILL.md`
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: fastapi-patterns
|
||||
description: FastAPI patterns for routing, dependency injection, validation, and async operations.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# FastAPI Patterns
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Building FastAPI applications
|
||||
- Creating API endpoints
|
||||
- Implementing dependency injection
|
||||
- Handling async database operations
|
||||
|
||||
## Project Structure
|
||||
|
||||
\`\`\`
|
||||
app/
|
||||
├── main.py # FastAPI app entry point
|
||||
├── routers/ # Route handlers
|
||||
│ ├── users.py
|
||||
│ └── items.py
|
||||
├── models/ # Pydantic models
|
||||
│ ├── user.py
|
||||
│ └── item.py
|
||||
├── services/ # Business logic
|
||||
│ └── user_service.py
|
||||
└── dependencies.py # Shared dependencies
|
||||
\`\`\`
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
\`\`\`python
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
async def get_db() -> AsyncSession:
|
||||
async with AsyncSessionLocal() as session:
|
||||
yield session
|
||||
|
||||
@router.get("/users/{user_id}")
|
||||
async def get_user(
|
||||
user_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
# Use db session
|
||||
pass
|
||||
\`\`\`
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `python-patterns`
|
||||
- `pydantic-validation`
|
||||
```
|
||||
|
||||
### Example 3: Workflow Skill
|
||||
|
||||
**File:** `skills/refactoring-workflow/SKILL.md`
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: refactoring-workflow
|
||||
description: Systematic refactoring workflow for improving code quality without changing behavior.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Refactoring Workflow
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Improving code structure
|
||||
- Reducing technical debt
|
||||
- Simplifying complex code
|
||||
- Extracting reusable components
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- All tests passing
|
||||
- Git working directory clean
|
||||
- Feature branch created
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
### Step 1: Identify Refactoring Target
|
||||
|
||||
- Look for code smells (long methods, duplicate code, large classes)
|
||||
- Check test coverage for target area
|
||||
- Document current behavior
|
||||
|
||||
### Step 2: Ensure Tests Exist
|
||||
|
||||
\`\`\`bash
|
||||
# Run tests to verify current behavior
|
||||
npm test
|
||||
|
||||
# Check coverage for target files
|
||||
npm run test:coverage
|
||||
\`\`\`
|
||||
|
||||
### Step 3: Make Small Changes
|
||||
|
||||
- One refactoring at a time
|
||||
- Run tests after each change
|
||||
- Commit frequently
|
||||
|
||||
### Step 4: Verify Behavior Unchanged
|
||||
|
||||
\`\`\`bash
|
||||
# Run full test suite
|
||||
npm test
|
||||
|
||||
# Run E2E tests
|
||||
npm run test:e2e
|
||||
\`\`\`
|
||||
|
||||
## Common Refactorings
|
||||
|
||||
| Smell | Refactoring |
|
||||
|-------|-------------|
|
||||
| Long method | Extract method |
|
||||
| Duplicate code | Extract to shared function |
|
||||
| Large class | Extract class |
|
||||
| Long parameter list | Introduce parameter object |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Tests exist for target code
|
||||
- [ ] Made small, focused changes
|
||||
- [ ] Tests pass after each change
|
||||
- [ ] Behavior unchanged
|
||||
- [ ] Committed with clear message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [CONTRIBUTING.md](../CONTRIBUTING.md) - General contribution guidelines
|
||||
- [project-guidelines-example](../skills/project-guidelines-example/SKILL.md) - Project-specific skill template
|
||||
- [coding-standards](../skills/coding-standards/SKILL.md) - Example of standards skill
|
||||
- [tdd-workflow](../skills/tdd-workflow/SKILL.md) - Example of workflow skill
|
||||
- [security-review](../skills/security-review/SKILL.md) - Example of domain knowledge skill
|
||||
|
||||
---
|
||||
|
||||
**Remember**: A good skill is focused, actionable, and immediately useful. Write skills you'd want to use yourself.
|
||||
@@ -409,7 +409,7 @@ claude --version
|
||||
Claude Code v2.1+は、インストール済みプラグインの`hooks/hooks.json`(規約)を自動読み込みします。`plugin.json`で明示的に宣言するとエラーが発生します:
|
||||
|
||||
```
|
||||
Duplicate hooks file detected: ./hooks/hooks.json resolves to already-loaded file
|
||||
Duplicate hook file detected: ./hooks/hooks.json is already resolved to a loaded file
|
||||
```
|
||||
|
||||
**背景:** これは本リポジトリで複数の修正/リバート循環を引き起こしました([#29](https://github.com/affaan-m/everything-claude-code/issues/29), [#52](https://github.com/affaan-m/everything-claude-code/issues/52), [#103](https://github.com/affaan-m/everything-claude-code/issues/103))。Claude Codeバージョン間で動作が変わったため混乱がありました。今後を防ぐため回帰テストがあります。
|
||||
|
||||
@@ -77,9 +77,9 @@ model: opus
|
||||
各問題について:
|
||||
```
|
||||
[CRITICAL] ハードコードされたAPIキー
|
||||
File: src/api/client.ts:42
|
||||
Issue: APIキーがソースコードに公開されている
|
||||
Fix: 環境変数に移動
|
||||
ファイル: src/api/client.ts:42
|
||||
問題: APIキーがソースコードに公開されている
|
||||
修正: 環境変数に移動
|
||||
|
||||
const apiKey = "sk-abc123"; // ❌ Bad
|
||||
const apiKey = process.env.API_KEY; // ✓ Good
|
||||
|
||||
@@ -341,20 +341,20 @@ x = x // 無意味な代入を削除
|
||||
各修正試行後:
|
||||
|
||||
```text
|
||||
[FIXED] internal/handler/user.go:42
|
||||
Error: undefined: UserService
|
||||
Fix: Added import "project/internal/service"
|
||||
[修正済] internal/handler/user.go:42
|
||||
エラー: undefined: UserService
|
||||
修正: import を追加 "project/internal/service"
|
||||
|
||||
Remaining errors: 3
|
||||
残りのエラー: 3
|
||||
```
|
||||
|
||||
最終サマリー:
|
||||
```text
|
||||
Build Status: SUCCESS/FAILED
|
||||
Errors Fixed: N
|
||||
Vet Warnings Fixed: N
|
||||
Files Modified: list
|
||||
Remaining Issues: list (if any)
|
||||
ビルドステータス: SUCCESS/FAILED
|
||||
修正済みエラー: N
|
||||
Vet 警告修正済み: N
|
||||
変更ファイル: list
|
||||
残りの問題: list (ある場合)
|
||||
```
|
||||
|
||||
## 重要な注意事項
|
||||
|
||||
@@ -228,9 +228,9 @@ model: opus
|
||||
各問題について:
|
||||
```text
|
||||
[CRITICAL] SQLインジェクション脆弱性
|
||||
File: internal/repository/user.go:42
|
||||
Issue: ユーザー入力がSQLクエリに直接連結されている
|
||||
Fix: パラメータ化クエリを使用
|
||||
ファイル: internal/repository/user.go:42
|
||||
問題: ユーザー入力がSQLクエリに直接連結されている
|
||||
修正: パラメータ化クエリを使用
|
||||
|
||||
query := "SELECT * FROM users WHERE id = " + userID // Bad
|
||||
query := "SELECT * FROM users WHERE id = $1" // Good
|
||||
|
||||
@@ -399,9 +399,9 @@ model: opus
|
||||
各問題について:
|
||||
```text
|
||||
[CRITICAL] SQLインジェクション脆弱性
|
||||
File: app/routes/user.py:42
|
||||
Issue: ユーザー入力がSQLクエリに直接補間されている
|
||||
Fix: パラメータ化クエリを使用
|
||||
ファイル: app/routes/user.py:42
|
||||
問題: ユーザー入力がSQLクエリに直接補間されている
|
||||
修正: パラメータ化クエリを使用
|
||||
|
||||
query = f"SELECT * FROM users WHERE id = {user_id}" # Bad
|
||||
query = "SELECT * FROM users WHERE id = %s" # Good
|
||||
|
||||
@@ -35,12 +35,12 @@ echo "$(date +%Y-%m-%d-%H:%M) | $CHECKPOINT_NAME | $(git rev-parse --short HEAD)
|
||||
3. レポート:
|
||||
|
||||
```
|
||||
CHECKPOINT COMPARISON: $NAME
|
||||
チェックポイント比較: $NAME
|
||||
============================
|
||||
Files changed: X
|
||||
Tests: +Y passed / -Z failed
|
||||
Coverage: +X% / -Y%
|
||||
Build: [PASS/FAIL]
|
||||
変更されたファイル: X
|
||||
テスト: +Y 合格 / -Z 失敗
|
||||
カバレッジ: +X% / -Y%
|
||||
ビルド: [PASS/FAIL]
|
||||
```
|
||||
|
||||
## チェックポイント一覧表示
|
||||
@@ -57,13 +57,13 @@ Build: [PASS/FAIL]
|
||||
一般的なチェックポイント流:
|
||||
|
||||
```
|
||||
[Start] --> /checkpoint create "feature-start"
|
||||
[開始] --> /checkpoint create "feature-start"
|
||||
|
|
||||
[Implement] --> /checkpoint create "core-done"
|
||||
[実装] --> /checkpoint create "core-done"
|
||||
|
|
||||
[Test] --> /checkpoint verify "core-done"
|
||||
[テスト] --> /checkpoint verify "core-done"
|
||||
|
|
||||
[Refactor] --> /checkpoint create "refactor-done"
|
||||
[リファクタリング] --> /checkpoint create "refactor-done"
|
||||
|
|
||||
[PR] --> /checkpoint verify "feature-start"
|
||||
```
|
||||
|
||||
@@ -38,24 +38,24 @@ e2e-runner エージェントは:
|
||||
## 使用します示例
|
||||
|
||||
````
|
||||
User: /e2e Test the market search and view flow
|
||||
User: /e2e マーケット検索と表示フローをテスト
|
||||
|
||||
Agent (e2e-runner):
|
||||
# E2E Test Generation: Market Search and View Flow
|
||||
# E2Eテスト生成: マーケット検索と表示フロー
|
||||
|
||||
## Test Scenario Identified
|
||||
## 特定されたテストシナリオ
|
||||
|
||||
**User Journey:** Market Search → View Results → Click Market → View Details
|
||||
**ユーザージャーニー:** マーケット検索 → 結果表示 → マーケットクリック → 詳細表示
|
||||
|
||||
**Test Coverage:**
|
||||
1. Navigate to markets page
|
||||
2. Perform semantic search
|
||||
3. Verify search results
|
||||
4. Click on first result
|
||||
5. Verify market details page loads
|
||||
6. Verify chart renders
|
||||
**テストカバレッジ:**
|
||||
1. マーケットページに遷移
|
||||
2. セマンティック検索を実行
|
||||
3. 検索結果を検証
|
||||
4. 最初の結果をクリック
|
||||
5. マーケット詳細ページの読み込みを検証
|
||||
6. チャートの描画を検証
|
||||
|
||||
## Generated Test Code
|
||||
## 生成されたテストコード
|
||||
|
||||
```typescript
|
||||
// tests/e2e/markets/search-and-view.spec.ts
|
||||
@@ -160,7 +160,7 @@ test.describe('Market Search and View Flow', () => {
|
||||
## テスト実行
|
||||
|
||||
```bash
|
||||
# Run the generated test
|
||||
# 生成されたテストを実行
|
||||
npx playwright test tests/e2e/markets/search-and-view.spec.ts
|
||||
|
||||
Running 3 tests using 3 workers
|
||||
@@ -181,54 +181,54 @@ Artifacts generated:
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ E2E Test Results ║
|
||||
║ E2Eテスト結果 ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Status: ✅ ALL TESTS PASSED ║
|
||||
║ Total: 3 tests ║
|
||||
║ Passed: 3 (100%) ║
|
||||
║ Failed: 0 ║
|
||||
║ Flaky: 0 ║
|
||||
║ Duration: 9.1s ║
|
||||
║ ステータス: ✅ 全テスト合格 ║
|
||||
║ 合計: 3テスト ║
|
||||
║ 合格: 3 (100%) ║
|
||||
║ 失敗: 0 ║
|
||||
║ 不安定: 0 ║
|
||||
║ 所要時間: 9.1s ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
Artifacts:
|
||||
📸 Screenshots: 2 files
|
||||
📹 Videos: 0 files (only on failure)
|
||||
🔍 Traces: 0 files (only on failure)
|
||||
📊 HTML Report: playwright-report/index.html
|
||||
アーティファクト:
|
||||
📸 スクリーンショット: 2ファイル
|
||||
📹 ビデオ: 0ファイル (失敗時のみ)
|
||||
🔍 トレース: 0ファイル (失敗時のみ)
|
||||
📊 HTMLレポート: playwright-report/index.html
|
||||
|
||||
View report: npx playwright show-report
|
||||
レポート表示: npx playwright show-report
|
||||
```
|
||||
|
||||
✅ E2E テストスイートは CI/CD 統合の準備ができました!
|
||||
|
||||
````
|
||||
|
||||
## Test Artifacts
|
||||
## テストアーティファクト
|
||||
|
||||
When tests run, the following artifacts are captured:
|
||||
テスト実行時、以下のアーティファクトがキャプチャされます:
|
||||
|
||||
**On All Tests:**
|
||||
- HTML Report with timeline and results
|
||||
- JUnit XML for CI integration
|
||||
**全テスト共通:**
|
||||
- タイムラインと結果を含むHTMLレポート
|
||||
- CI統合用のJUnit XML
|
||||
|
||||
**On Failure Only:**
|
||||
- Screenshot of the failing state
|
||||
- Video recording of the test
|
||||
- Trace file for debugging (step-by-step replay)
|
||||
- Network logs
|
||||
- Console logs
|
||||
**失敗時のみ:**
|
||||
- 失敗状態のスクリーンショット
|
||||
- テストのビデオ録画
|
||||
- デバッグ用トレースファイル (ステップバイステップ再生)
|
||||
- ネットワークログ
|
||||
- コンソールログ
|
||||
|
||||
## Viewing Artifacts
|
||||
## アーティファクトの確認
|
||||
|
||||
```bash
|
||||
# View HTML report in browser
|
||||
# ブラウザでHTMLレポートを表示
|
||||
npx playwright show-report
|
||||
|
||||
# View specific trace file
|
||||
# 特定のトレースファイルを表示
|
||||
npx playwright show-trace artifacts/trace-abc123.zip
|
||||
|
||||
# Screenshots are saved in artifacts/ directory
|
||||
# スクリーンショットはartifacts/ディレクトリに保存
|
||||
open artifacts/search-results.png
|
||||
````
|
||||
|
||||
@@ -239,18 +239,18 @@ open artifacts/search-results.png
|
||||
```
|
||||
⚠️ FLAKY TEST DETECTED: tests/e2e/markets/trade.spec.ts
|
||||
|
||||
Test passed 7/10 runs (70% pass rate)
|
||||
テストは10回中7回合格 (合格率70%)
|
||||
|
||||
Common failure:
|
||||
よくある失敗:
|
||||
"Timeout waiting for element '[data-testid="confirm-btn"]'"
|
||||
|
||||
Recommended fixes:
|
||||
1. Add explicit wait: await page.waitForSelector('[data-testid="confirm-btn"]')
|
||||
2. Increase timeout: { timeout: 10000 }
|
||||
3. Check for race conditions in component
|
||||
4. Verify element is not hidden by animation
|
||||
推奨修正:
|
||||
1. 明示的な待機を追加: await page.waitForSelector('[data-testid="confirm-btn"]')
|
||||
2. タイムアウトを増加: { timeout: 10000 }
|
||||
3. コンポーネントの競合状態を確認
|
||||
4. 要素がアニメーションで隠れていないか確認
|
||||
|
||||
Quarantine recommendation: Mark as test.fixme() until fixed
|
||||
隔離推奨: 修正されるまでtest.fixme()としてマーク
|
||||
```
|
||||
|
||||
## ブラウザ設定
|
||||
@@ -350,21 +350,21 @@ PMX の場合、以下の E2E テストを優先:
|
||||
## 快速命令
|
||||
|
||||
```bash
|
||||
# Run all E2E tests
|
||||
# 全E2Eテストを実行
|
||||
npx playwright test
|
||||
|
||||
# Run specific test file
|
||||
# 特定のテストファイルを実行
|
||||
npx playwright test tests/e2e/markets/search.spec.ts
|
||||
|
||||
# Run in headed mode (see browser)
|
||||
# ヘッドモードで実行 (ブラウザ表示)
|
||||
npx playwright test --headed
|
||||
|
||||
# Debug test
|
||||
# テストをデバッグ
|
||||
npx playwright test --debug
|
||||
|
||||
# Generate test code
|
||||
# テストコードを生成
|
||||
npx playwright codegen http://localhost:3000
|
||||
|
||||
# View report
|
||||
# レポートを表示
|
||||
npx playwright show-report
|
||||
```
|
||||
|
||||
@@ -92,36 +92,36 @@ instinctsが分離の恩恵を受ける複雑な複数ステップのプロセ
|
||||
## 出力フォーマット
|
||||
|
||||
```
|
||||
🧬 Evolve Analysis
|
||||
🧬 進化分析
|
||||
==================
|
||||
|
||||
進化の準備ができた3つのクラスターを発見:
|
||||
|
||||
## クラスター1: データベースマイグレーションワークフロー
|
||||
Instincts: new-table-migration, update-schema, regenerate-types
|
||||
Type: Command
|
||||
Confidence: 85%(12件の観測に基づく)
|
||||
タイプ: Command
|
||||
信頼度: 85%(12件の観測に基づく)
|
||||
|
||||
作成: /new-tableコマンド
|
||||
Files:
|
||||
ファイル:
|
||||
- ~/.claude/homunculus/evolved/commands/new-table.md
|
||||
|
||||
## クラスター2: 関数型コードスタイル
|
||||
Instincts: prefer-functional, use-immutable, avoid-classes, pure-functions
|
||||
Type: Skill
|
||||
Confidence: 78%(8件の観測に基づく)
|
||||
タイプ: Skill
|
||||
信頼度: 78%(8件の観測に基づく)
|
||||
|
||||
作成: functional-patternsスキル
|
||||
Files:
|
||||
ファイル:
|
||||
- ~/.claude/homunculus/evolved/skills/functional-patterns.md
|
||||
|
||||
## クラスター3: デバッグプロセス
|
||||
Instincts: debug-check-logs, debug-isolate, debug-reproduce, debug-verify
|
||||
Type: Agent
|
||||
Confidence: 72%(6件の観測に基づく)
|
||||
タイプ: Agent
|
||||
信頼度: 72%(6件の観測に基づく)
|
||||
|
||||
作成: debuggerエージェント
|
||||
Files:
|
||||
ファイル:
|
||||
- ~/.claude/homunculus/evolved/agents/debugger.md
|
||||
|
||||
---
|
||||
|
||||
@@ -62,9 +62,9 @@ internal/handler/api.go:58:2: missing return at end of function
|
||||
|
||||
## 修正1: 未定義の識別子
|
||||
|
||||
File: internal/service/user.go:25
|
||||
Error: undefined: UserRepository
|
||||
Cause: インポート欠落
|
||||
ファイル: internal/service/user.go:25
|
||||
エラー: undefined: UserRepository
|
||||
原因: インポート欠落
|
||||
|
||||
```go
|
||||
// インポートを追加
|
||||
@@ -83,8 +83,8 @@ $ go build ./...
|
||||
|
||||
## 修正2: 型の不一致
|
||||
|
||||
File: internal/handler/api.go:42
|
||||
Error: cannot use x (type string) as type int
|
||||
ファイル: internal/handler/api.go:42
|
||||
エラー: cannot use x (type string) as type int
|
||||
|
||||
```go
|
||||
// 変更前
|
||||
@@ -101,8 +101,8 @@ $ go build ./...
|
||||
|
||||
## 修正3: 戻り値の欠落
|
||||
|
||||
File: internal/handler/api.go:58
|
||||
Error: missing return at end of function
|
||||
ファイル: internal/handler/api.go:58
|
||||
エラー: missing return at end of function
|
||||
|
||||
```go
|
||||
func GetUser(id string) (*User, error) {
|
||||
|
||||
@@ -85,8 +85,8 @@ Agent:
|
||||
## 発見された問題
|
||||
|
||||
[CRITICAL] 競合状態
|
||||
File: internal/service/auth.go:45
|
||||
Issue: 同期化なしで共有マップにアクセス
|
||||
ファイル: internal/service/auth.go:45
|
||||
問題: 同期化なしで共有マップにアクセス
|
||||
```go
|
||||
var cache = map[string]*Session{} // 並行アクセス!
|
||||
|
||||
@@ -94,7 +94,7 @@ func GetSession(id string) *Session {
|
||||
return cache[id] // 競合状態
|
||||
}
|
||||
```
|
||||
Fix: sync.RWMutexまたはsync.Mapを使用
|
||||
修正: sync.RWMutexまたはsync.Mapを使用
|
||||
```go
|
||||
var (
|
||||
cache = map[string]*Session{}
|
||||
@@ -109,12 +109,12 @@ func GetSession(id string) *Session {
|
||||
```
|
||||
|
||||
[HIGH] エラーコンテキストの欠落
|
||||
File: internal/handler/user.go:28
|
||||
Issue: コンテキストなしでエラーを返す
|
||||
ファイル: internal/handler/user.go:28
|
||||
問題: コンテキストなしでエラーを返す
|
||||
```go
|
||||
return err // コンテキストなし
|
||||
```
|
||||
Fix: コンテキストでラップ
|
||||
修正: コンテキストでラップ
|
||||
```go
|
||||
return fmt.Errorf("get user %s: %w", userID, err)
|
||||
```
|
||||
|
||||
@@ -45,40 +45,40 @@ python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py import <
|
||||
## インポートプロセス
|
||||
|
||||
```
|
||||
📥 Importing instincts from: team-instincts.yaml
|
||||
📥 instinctsをインポート中: team-instincts.yaml
|
||||
================================================
|
||||
|
||||
Found 12 instincts to import.
|
||||
12件のinstinctsが見つかりました。
|
||||
|
||||
Analyzing conflicts...
|
||||
競合を分析中...
|
||||
|
||||
## New Instincts (8)
|
||||
These will be added:
|
||||
## 新規instincts (8)
|
||||
以下が追加されます:
|
||||
✓ use-zod-validation (confidence: 0.7)
|
||||
✓ prefer-named-exports (confidence: 0.65)
|
||||
✓ test-async-functions (confidence: 0.8)
|
||||
...
|
||||
|
||||
## Duplicate Instincts (3)
|
||||
Already have similar instincts:
|
||||
## 重複instincts (3)
|
||||
類似のinstinctsが既に存在:
|
||||
⚠️ prefer-functional-style
|
||||
Local: 0.8 confidence, 12 observations
|
||||
Import: 0.7 confidence
|
||||
→ Keep local (higher confidence)
|
||||
ローカル: 信頼度0.8, 12回の観測
|
||||
インポート: 信頼度0.7
|
||||
→ ローカルを保持 (信頼度が高い)
|
||||
|
||||
⚠️ test-first-workflow
|
||||
Local: 0.75 confidence
|
||||
Import: 0.9 confidence
|
||||
→ Update to import (higher confidence)
|
||||
ローカル: 信頼度0.75
|
||||
インポート: 信頼度0.9
|
||||
→ インポートに更新 (信頼度が高い)
|
||||
|
||||
## Conflicting Instincts (1)
|
||||
These contradict local instincts:
|
||||
## 競合instincts (1)
|
||||
ローカルのinstinctsと矛盾:
|
||||
❌ use-classes-for-services
|
||||
Conflicts with: avoid-classes
|
||||
→ Skip (requires manual resolution)
|
||||
競合: avoid-classes
|
||||
→ スキップ (手動解決が必要)
|
||||
|
||||
---
|
||||
Import 8 new, update 1, skip 3?
|
||||
8件を新規追加、1件を更新、3件をスキップしますか?
|
||||
```
|
||||
|
||||
## マージ戦略
|
||||
@@ -130,13 +130,13 @@ Skill Creatorからインポートする場合:
|
||||
|
||||
インポート後:
|
||||
```
|
||||
✅ Import complete!
|
||||
✅ インポート完了!
|
||||
|
||||
Added: 8 instincts
|
||||
Updated: 1 instinct
|
||||
Skipped: 3 instincts (2 duplicates, 1 conflict)
|
||||
追加: 8件のinstincts
|
||||
更新: 1件のinstinct
|
||||
スキップ: 3件のinstincts (2件の重複, 1件の競合)
|
||||
|
||||
New instincts saved to: ~/.claude/homunculus/instincts/inherited/
|
||||
新規instinctsの保存先: ~/.claude/homunculus/instincts/inherited/
|
||||
|
||||
Run /instinct-status to see all instincts.
|
||||
/instinct-statusを実行してすべてのinstinctsを確認できます。
|
||||
```
|
||||
|
||||
@@ -39,42 +39,42 @@ python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py status
|
||||
## 出力形式
|
||||
|
||||
```
|
||||
📊 Instinct Status
|
||||
📊 instinctステータス
|
||||
==================
|
||||
|
||||
## Code Style (4 instincts)
|
||||
## コードスタイル (4 instincts)
|
||||
|
||||
### prefer-functional-style
|
||||
Trigger: when writing new functions
|
||||
Action: Use functional patterns over classes
|
||||
Confidence: ████████░░ 80%
|
||||
Source: session-observation | Last updated: 2025-01-22
|
||||
トリガー: 新しい関数を書くとき
|
||||
アクション: クラスより関数型パターンを使用
|
||||
信頼度: ████████░░ 80%
|
||||
ソース: session-observation | 最終更新: 2025-01-22
|
||||
|
||||
### use-path-aliases
|
||||
Trigger: when importing modules
|
||||
Action: Use @/ path aliases instead of relative imports
|
||||
Confidence: ██████░░░░ 60%
|
||||
Source: repo-analysis (github.com/acme/webapp)
|
||||
トリガー: モジュールをインポートするとき
|
||||
アクション: 相対インポートの代わりに@/パスエイリアスを使用
|
||||
信頼度: ██████░░░░ 60%
|
||||
ソース: repo-analysis (github.com/acme/webapp)
|
||||
|
||||
## Testing (2 instincts)
|
||||
## テスト (2 instincts)
|
||||
|
||||
### test-first-workflow
|
||||
Trigger: when adding new functionality
|
||||
Action: Write test first, then implementation
|
||||
Confidence: █████████░ 90%
|
||||
Source: session-observation
|
||||
トリガー: 新しい機能を追加するとき
|
||||
アクション: テストを先に書き、次に実装
|
||||
信頼度: █████████░ 90%
|
||||
ソース: session-observation
|
||||
|
||||
## Workflow (3 instincts)
|
||||
## ワークフロー (3 instincts)
|
||||
|
||||
### grep-before-edit
|
||||
Trigger: when modifying code
|
||||
Action: Search with Grep, confirm with Read, then Edit
|
||||
Confidence: ███████░░░ 70%
|
||||
Source: session-observation
|
||||
トリガー: コードを変更するとき
|
||||
アクション: Grepで検索、Readで確認、次にEdit
|
||||
信頼度: ███████░░░ 70%
|
||||
ソース: session-observation
|
||||
|
||||
---
|
||||
Total: 9 instincts (4 personal, 5 inherited)
|
||||
Observer: Running (last analysis: 5 min ago)
|
||||
合計: 9 instincts (4個人, 5継承)
|
||||
オブザーバー: 実行中 (最終分析: 5分前)
|
||||
```
|
||||
|
||||
## フラグ
|
||||
|
||||
@@ -99,36 +99,36 @@ security-reviewer -> code-reviewer -> architect
|
||||
## 最終レポート形式
|
||||
|
||||
```
|
||||
ORCHESTRATION REPORT
|
||||
オーケストレーションレポート
|
||||
====================
|
||||
Workflow: feature
|
||||
Task: Add user authentication
|
||||
Agents: planner -> tdd-guide -> code-reviewer -> security-reviewer
|
||||
ワークフロー: feature
|
||||
タスク: ユーザー認証の追加
|
||||
エージェント: planner -> tdd-guide -> code-reviewer -> security-reviewer
|
||||
|
||||
SUMMARY
|
||||
サマリー
|
||||
-------
|
||||
[1段落の要約]
|
||||
|
||||
AGENT OUTPUTS
|
||||
エージェント出力
|
||||
-------------
|
||||
Planner: [要約]
|
||||
TDD Guide: [要約]
|
||||
Code Reviewer: [要約]
|
||||
Security Reviewer: [要約]
|
||||
|
||||
FILES CHANGED
|
||||
変更ファイル
|
||||
-------------
|
||||
[変更されたすべてのファイルをリスト]
|
||||
|
||||
TEST RESULTS
|
||||
テスト結果
|
||||
------------
|
||||
[テスト合格/不合格の要約]
|
||||
|
||||
SECURITY STATUS
|
||||
セキュリティステータス
|
||||
---------------
|
||||
[セキュリティの発見事項]
|
||||
|
||||
RECOMMENDATION
|
||||
推奨事項
|
||||
--------------
|
||||
[リリース可 / 要修正 / ブロック中]
|
||||
```
|
||||
|
||||
@@ -95,26 +95,26 @@ Agent:
|
||||
## 発見された問題
|
||||
|
||||
[CRITICAL] SQLインジェクション脆弱性
|
||||
File: app/routes/user.py:42
|
||||
Issue: ユーザー入力が直接SQLクエリに挿入されている
|
||||
ファイル: app/routes/user.py:42
|
||||
問題: ユーザー入力が直接SQLクエリに挿入されている
|
||||
```python
|
||||
query = f"SELECT * FROM users WHERE id = {user_id}" # 悪い
|
||||
```
|
||||
Fix: パラメータ化クエリを使用
|
||||
修正: パラメータ化クエリを使用
|
||||
```python
|
||||
query = "SELECT * FROM users WHERE id = %s" # 良い
|
||||
cursor.execute(query, (user_id,))
|
||||
```
|
||||
|
||||
[HIGH] 可変デフォルト引数
|
||||
File: app/services/auth.py:18
|
||||
Issue: 可変デフォルト引数が共有状態を引き起こす
|
||||
ファイル: app/services/auth.py:18
|
||||
問題: 可変デフォルト引数が共有状態を引き起こす
|
||||
```python
|
||||
def process_items(items=[]): # 悪い
|
||||
items.append("new")
|
||||
return items
|
||||
```
|
||||
Fix: デフォルトにNoneを使用
|
||||
修正: デフォルトにNoneを使用
|
||||
```python
|
||||
def process_items(items=None): # 良い
|
||||
if items is None:
|
||||
@@ -124,27 +124,27 @@ def process_items(items=None): # 良い
|
||||
```
|
||||
|
||||
[MEDIUM] 型ヒントの欠落
|
||||
File: app/services/auth.py:25
|
||||
Issue: 型アノテーションのない公開関数
|
||||
ファイル: app/services/auth.py:25
|
||||
問題: 型アノテーションのない公開関数
|
||||
```python
|
||||
def get_user(user_id): # 悪い
|
||||
return db.find(user_id)
|
||||
```
|
||||
Fix: 型ヒントを追加
|
||||
修正: 型ヒントを追加
|
||||
```python
|
||||
def get_user(user_id: str) -> Optional[User]: # 良い
|
||||
return db.find(user_id)
|
||||
```
|
||||
|
||||
[MEDIUM] コンテキストマネージャーを使用していない
|
||||
File: app/routes/user.py:55
|
||||
Issue: 例外時にファイルがクローズされない
|
||||
ファイル: app/routes/user.py:55
|
||||
問題: 例外時にファイルがクローズされない
|
||||
```python
|
||||
f = open("config.json") # 悪い
|
||||
data = f.read()
|
||||
f.close()
|
||||
```
|
||||
Fix: コンテキストマネージャーを使用
|
||||
修正: コンテキストマネージャーを使用
|
||||
```python
|
||||
with open("config.json") as f: # 良い
|
||||
data = f.read()
|
||||
|
||||
@@ -36,16 +36,16 @@
|
||||
簡潔な検証レポートを生成します:
|
||||
|
||||
```
|
||||
VERIFICATION: [PASS/FAIL]
|
||||
検証結果: [PASS/FAIL]
|
||||
|
||||
Build: [OK/FAIL]
|
||||
Types: [OK/X errors]
|
||||
Lint: [OK/X issues]
|
||||
Tests: [X/Y passed, Z% coverage]
|
||||
Secrets: [OK/X found]
|
||||
Logs: [OK/X console.logs]
|
||||
ビルド: [OK/FAIL]
|
||||
型: [OK/Xエラー]
|
||||
Lint: [OK/X件の問題]
|
||||
テスト: [X/Y合格, Z%カバレッジ]
|
||||
シークレット: [OK/X件発見]
|
||||
ログ: [OK/X件のconsole.log]
|
||||
|
||||
Ready for PR: [YES/NO]
|
||||
PR準備完了: [YES/NO]
|
||||
```
|
||||
|
||||
重大な問題がある場合は、修正案とともにリストアップします。
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
```
|
||||
src/
|
||||
|-- app/ # Next.js app router
|
||||
|-- app/ # Next.js App Router
|
||||
|-- components/ # 再利用可能なUIコンポーネント
|
||||
|-- hooks/ # カスタムReactフック
|
||||
|-- lib/ # ユーティリティライブラリ
|
||||
|
||||
@@ -234,14 +234,14 @@ setCount(count + 1) // Can be stale in async scenarios
|
||||
### REST API規約
|
||||
|
||||
```
|
||||
GET /api/markets # List all markets
|
||||
GET /api/markets/:id # Get specific market
|
||||
POST /api/markets # Create new market
|
||||
PUT /api/markets/:id # Update market (full)
|
||||
PATCH /api/markets/:id # Update market (partial)
|
||||
DELETE /api/markets/:id # Delete market
|
||||
GET /api/markets # すべてのマーケットを一覧
|
||||
GET /api/markets/:id # 特定のマーケットを取得
|
||||
POST /api/markets # 新しいマーケットを作成
|
||||
PUT /api/markets/:id # マーケットを更新(全体)
|
||||
PATCH /api/markets/:id # マーケットを更新(部分)
|
||||
DELETE /api/markets/:id # マーケットを削除
|
||||
|
||||
# Query parameters for filtering
|
||||
# フィルタリング用クエリパラメータ
|
||||
GET /api/markets?status=active&limit=10&offset=0
|
||||
```
|
||||
|
||||
@@ -312,29 +312,29 @@ export async function POST(request: Request) {
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── api/ # API routes
|
||||
│ ├── markets/ # Market pages
|
||||
│ └── (auth)/ # Auth pages (route groups)
|
||||
├── components/ # React components
|
||||
│ ├── ui/ # Generic UI components
|
||||
│ ├── forms/ # Form components
|
||||
│ └── layouts/ # Layout components
|
||||
├── hooks/ # Custom React hooks
|
||||
├── lib/ # Utilities and configs
|
||||
│ ├── api/ # API clients
|
||||
│ ├── utils/ # Helper functions
|
||||
│ └── constants/ # Constants
|
||||
├── types/ # TypeScript types
|
||||
└── styles/ # Global styles
|
||||
│ ├── api/ # API ルート
|
||||
│ ├── markets/ # マーケットページ
|
||||
│ └── (auth)/ # 認証ページ(ルートグループ)
|
||||
├── components/ # React コンポーネント
|
||||
│ ├── ui/ # 汎用 UI コンポーネント
|
||||
│ ├── forms/ # フォームコンポーネント
|
||||
│ └── layouts/ # レイアウトコンポーネント
|
||||
├── hooks/ # カスタム React フック
|
||||
├── lib/ # ユーティリティと設定
|
||||
│ ├── api/ # API クライアント
|
||||
│ ├── utils/ # ヘルパー関数
|
||||
│ └── constants/ # 定数
|
||||
├── types/ # TypeScript 型定義
|
||||
└── styles/ # グローバルスタイル
|
||||
```
|
||||
|
||||
### ファイル命名
|
||||
|
||||
```
|
||||
components/Button.tsx # PascalCase for components
|
||||
hooks/useAuth.ts # camelCase with 'use' prefix
|
||||
lib/formatDate.ts # camelCase for utilities
|
||||
types/market.types.ts # camelCase with .types suffix
|
||||
components/Button.tsx # コンポーネントは PascalCase
|
||||
hooks/useAuth.ts # フックは 'use' プレフィックス付き camelCase
|
||||
lib/formatDate.ts # ユーティリティは camelCase
|
||||
types/market.types.ts # 型定義は .types サフィックス付き camelCase
|
||||
```
|
||||
|
||||
## コメントとドキュメント
|
||||
|
||||
@@ -51,13 +51,13 @@ source: "session-observation"
|
||||
## 仕組み
|
||||
|
||||
```
|
||||
Session Activity
|
||||
セッションアクティビティ
|
||||
│
|
||||
│ フックがプロンプト + ツール使用をキャプチャ(100%信頼性)
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ observations.jsonl │
|
||||
│ (prompts, tool calls, outcomes) │
|
||||
│ (プロンプト、ツール呼び出し、結果) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
│ Observerエージェントが読み取り(バックグラウンド、Haiku)
|
||||
|
||||
@@ -22,24 +22,24 @@ Claude Codeセッションの正式な評価フレームワークで、評価駆
|
||||
Claudeが以前できなかったことができるようになったかをテスト:
|
||||
```markdown
|
||||
[CAPABILITY EVAL: feature-name]
|
||||
Task: Claudeが達成すべきことの説明
|
||||
Success Criteria:
|
||||
タスク: Claudeが達成すべきことの説明
|
||||
成功基準:
|
||||
- [ ] 基準1
|
||||
- [ ] 基準2
|
||||
- [ ] 基準3
|
||||
Expected Output: 期待される結果の説明
|
||||
期待される出力: 期待される結果の説明
|
||||
```
|
||||
|
||||
### リグレッション評価
|
||||
変更が既存の機能を破壊しないことを確認:
|
||||
```markdown
|
||||
[REGRESSION EVAL: feature-name]
|
||||
Baseline: SHAまたはチェックポイント名
|
||||
Tests:
|
||||
ベースライン: SHAまたはチェックポイント名
|
||||
テスト:
|
||||
- existing-test-1: PASS/FAIL
|
||||
- existing-test-2: PASS/FAIL
|
||||
- existing-test-3: PASS/FAIL
|
||||
Result: X/Y passed (previously Y/Y)
|
||||
結果: X/Y 成功(以前は Y/Y)
|
||||
```
|
||||
|
||||
## 評価者タイプ
|
||||
@@ -67,17 +67,17 @@ Claudeを使用して自由形式の出力を評価:
|
||||
3. エッジケースは処理されていますか?
|
||||
4. エラー処理は適切ですか?
|
||||
|
||||
Score: 1-5 (1=poor, 5=excellent)
|
||||
Reasoning: [説明]
|
||||
スコア: 1-5(1=不良、5=優秀)
|
||||
理由: [説明]
|
||||
```
|
||||
|
||||
### 3. 人間評価者
|
||||
手動レビューのためにフラグを立てる:
|
||||
```markdown
|
||||
[HUMAN REVIEW REQUIRED]
|
||||
Change: 何が変更されたかの説明
|
||||
Reason: 人間のレビューが必要な理由
|
||||
Risk Level: LOW/MEDIUM/HIGH
|
||||
変更内容: 何が変更されたかの説明
|
||||
理由: 人間のレビューが必要な理由
|
||||
リスクレベル: LOW/MEDIUM/HIGH
|
||||
```
|
||||
|
||||
## メトリクス
|
||||
@@ -98,21 +98,21 @@ Risk Level: LOW/MEDIUM/HIGH
|
||||
|
||||
### 1. 定義(コーディング前)
|
||||
```markdown
|
||||
## EVAL DEFINITION: feature-xyz
|
||||
## 評価定義: feature-xyz
|
||||
|
||||
### Capability Evals
|
||||
### 能力評価
|
||||
1. 新しいユーザーアカウントを作成できる
|
||||
2. メール形式を検証できる
|
||||
3. パスワードを安全にハッシュ化できる
|
||||
|
||||
### Regression Evals
|
||||
### リグレッション評価
|
||||
1. 既存のログインが引き続き機能する
|
||||
2. セッション管理が変更されていない
|
||||
3. ログアウトフローが維持されている
|
||||
|
||||
### Success Metrics
|
||||
- pass@3 > 90% for capability evals
|
||||
- pass^3 = 100% for regression evals
|
||||
### 成功メトリクス
|
||||
- 能力評価で pass@3 > 90%
|
||||
- リグレッション評価で pass^3 = 100%
|
||||
```
|
||||
|
||||
### 2. 実装
|
||||
@@ -131,26 +131,26 @@ npm test -- --testPathPattern="existing"
|
||||
|
||||
### 4. レポート
|
||||
```markdown
|
||||
EVAL REPORT: feature-xyz
|
||||
評価レポート: feature-xyz
|
||||
========================
|
||||
|
||||
Capability Evals:
|
||||
能力評価:
|
||||
create-user: PASS (pass@1)
|
||||
validate-email: PASS (pass@2)
|
||||
hash-password: PASS (pass@1)
|
||||
Overall: 3/3 passed
|
||||
全体: 3/3 成功
|
||||
|
||||
Regression Evals:
|
||||
リグレッション評価:
|
||||
login-flow: PASS
|
||||
session-mgmt: PASS
|
||||
logout-flow: PASS
|
||||
Overall: 3/3 passed
|
||||
全体: 3/3 成功
|
||||
|
||||
Metrics:
|
||||
メトリクス:
|
||||
pass@1: 67% (2/3)
|
||||
pass@3: 100% (3/3)
|
||||
|
||||
Status: READY FOR REVIEW
|
||||
ステータス: レビュー準備完了
|
||||
```
|
||||
|
||||
## 統合パターン
|
||||
@@ -199,29 +199,29 @@ Status: READY FOR REVIEW
|
||||
```markdown
|
||||
## EVAL: add-authentication
|
||||
|
||||
### Phase 1: Define (10 min)
|
||||
Capability Evals:
|
||||
### フェーズ 1: 定義(10分)
|
||||
能力評価:
|
||||
- [ ] ユーザーはメール/パスワードで登録できる
|
||||
- [ ] ユーザーは有効な資格情報でログインできる
|
||||
- [ ] 無効な資格情報は適切なエラーで拒否される
|
||||
- [ ] セッションはページリロード後も持続する
|
||||
- [ ] ログアウトはセッションをクリアする
|
||||
|
||||
Regression Evals:
|
||||
リグレッション評価:
|
||||
- [ ] 公開ルートは引き続きアクセス可能
|
||||
- [ ] APIレスポンスは変更されていない
|
||||
- [ ] データベーススキーマは互換性がある
|
||||
|
||||
### Phase 2: Implement (varies)
|
||||
### フェーズ 2: 実装(可変)
|
||||
[コードを書く]
|
||||
|
||||
### Phase 3: Evaluate
|
||||
### フェーズ 3: 評価
|
||||
Run: /eval check add-authentication
|
||||
|
||||
### Phase 4: Report
|
||||
EVAL REPORT: add-authentication
|
||||
### フェーズ 4: レポート
|
||||
評価レポート: add-authentication
|
||||
==============================
|
||||
Capability: 5/5 passed (pass@3: 100%)
|
||||
Regression: 3/3 passed (pass^3: 100%)
|
||||
Status: SHIP IT
|
||||
能力: 5/5 成功(pass@3: 100%)
|
||||
リグレッション: 3/3 成功(pass^3: 100%)
|
||||
ステータス: 出荷可能
|
||||
```
|
||||
|
||||
@@ -368,17 +368,17 @@ func WriteAndFlush(w io.Writer, data []byte) error {
|
||||
myproject/
|
||||
├── cmd/
|
||||
│ └── myapp/
|
||||
│ └── main.go # Entry point
|
||||
│ └── main.go # エントリポイント
|
||||
├── internal/
|
||||
│ ├── handler/ # HTTP handlers
|
||||
│ ├── service/ # Business logic
|
||||
│ ├── repository/ # Data access
|
||||
│ └── config/ # Configuration
|
||||
│ ├── handler/ # HTTP ハンドラー
|
||||
│ ├── service/ # ビジネスロジック
|
||||
│ ├── repository/ # データアクセス
|
||||
│ └── config/ # 設定
|
||||
├── pkg/
|
||||
│ └── client/ # Public API client
|
||||
│ └── client/ # 公開 API クライアント
|
||||
├── api/
|
||||
│ └── v1/ # API definitions (proto, OpenAPI)
|
||||
├── testdata/ # Test fixtures
|
||||
│ └── v1/ # API 定義(proto、OpenAPI)
|
||||
├── testdata/ # テストフィクスチャ
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
└── Makefile
|
||||
|
||||
@@ -113,7 +113,7 @@ mypackage/
|
||||
├── testdata/ # テストフィクスチャ
|
||||
│ ├── valid_user.json
|
||||
│ └── invalid_user.json
|
||||
└── export_test.go # 内部のテストのための非公開のエクスポート
|
||||
└── export_test.go # 内部テスト用の非公開エクスポート
|
||||
```
|
||||
|
||||
### テストパッケージ
|
||||
|
||||
@@ -594,18 +594,18 @@ def test_with_tmpdir(tmpdir):
|
||||
|
||||
```
|
||||
tests/
|
||||
├── conftest.py # Shared fixtures
|
||||
├── conftest.py # 共有フィクスチャ
|
||||
├── __init__.py
|
||||
├── unit/ # Unit tests
|
||||
├── unit/ # ユニットテスト
|
||||
│ ├── __init__.py
|
||||
│ ├── test_models.py
|
||||
│ ├── test_utils.py
|
||||
│ └── test_services.py
|
||||
├── integration/ # Integration tests
|
||||
├── integration/ # 統合テスト
|
||||
│ ├── __init__.py
|
||||
│ ├── test_api.py
|
||||
│ └── test_database.py
|
||||
└── e2e/ # End-to-end tests
|
||||
└── e2e/ # エンドツーエンドテスト
|
||||
├── __init__.py
|
||||
└── test_user_flow.py
|
||||
```
|
||||
|
||||
31
docs/zh-CN/commands/prune.md
Normal file
31
docs/zh-CN/commands/prune.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: prune
|
||||
description: 删除超过 30 天且从未被提升的待处理本能
|
||||
command: true
|
||||
---
|
||||
|
||||
# 清理待处理本能
|
||||
|
||||
删除那些由系统自动生成、但从未经过审查或提升的过期待处理本能。
|
||||
|
||||
## 实现
|
||||
|
||||
使用插件根目录路径运行本能 CLI:
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" prune
|
||||
```
|
||||
|
||||
或者如果 `CLAUDE_PLUGIN_ROOT` 未设置(手动安装):
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py prune
|
||||
```
|
||||
|
||||
## 用法
|
||||
|
||||
```
|
||||
/prune # 删除超过 30 天的本能
|
||||
/prune --max-age 60 # 自定义年龄阈值(天)
|
||||
/prune --dry-run # 仅预览,不实际删除
|
||||
```
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: prompt-optimizer
|
||||
description: 分析原始提示,识别意图和差距,匹配ECC组件(技能/命令/代理/钩子),并输出一个可直接粘贴的优化提示。仅提供咨询角色——绝不自行执行任务。触发时机:当用户说“优化提示”、“改进我的提示”、“如何编写提示”、“帮我优化这个指令”或明确要求提高提示质量时。中文等效表达同样触发:“优化prompt”、“改进prompt”、“怎么写prompt”、“帮我优化这个指令”。不触发时机:当用户希望直接执行任务,或说“直接做”时。不触发时机:当用户说“优化代码”、“优化性能”、“optimize performance”、“optimize this code”时——这些是重构/性能优化任务,而非提示优化。origin: community
|
||||
description: 分析原始提示,识别意图和差距,匹配ECC组件(技能/命令/代理/钩子),并输出一个可直接粘贴的优化提示。仅提供咨询角色——绝不自行执行任务。触发时机:当用户说“优化提示”、“改进我的提示”、“如何编写提示”、“帮我优化这个指令”或明确要求提高提示质量时。中文等效表达同样触发:“优化prompt”、“改进prompt”、“怎么写prompt”、“帮我优化这个指令”。不触发时机:当用户希望直接执行任务,或说“直接做”时。不触发时机:当用户说“优化代码”、“优化性能”、“optimize performance”、“optimize this code”时——这些是重构/性能优化任务,而非提示优化。
|
||||
origin: community
|
||||
metadata:
|
||||
author: YannJY02
|
||||
version: "1.0.0"
|
||||
|
||||
8
ecc2/Cargo.lock
generated
8
ecc2/Cargo.lock
generated
@@ -438,9 +438,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.19.0"
|
||||
version = "0.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
|
||||
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
@@ -725,9 +725,9 @@ checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.17.0+1.8.1"
|
||||
version = "0.18.3+1.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
|
||||
checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
|
||||
@@ -19,7 +19,7 @@ tokio = { version = "1", features = ["full"] }
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
|
||||
# Git integration
|
||||
git2 = "0.19"
|
||||
git2 = "0.20"
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -13,7 +13,10 @@ pub enum MessageType {
|
||||
/// Response to a query
|
||||
Response { answer: String },
|
||||
/// Notification of completion
|
||||
Completed { summary: String, files_changed: Vec<String> },
|
||||
Completed {
|
||||
summary: String,
|
||||
files_changed: Vec<String>,
|
||||
},
|
||||
/// Conflict detected (e.g., two agents editing the same file)
|
||||
Conflict { file: String, description: String },
|
||||
}
|
||||
|
||||
@@ -2,7 +2,25 @@ use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PaneLayout {
|
||||
#[default]
|
||||
Horizontal,
|
||||
Vertical,
|
||||
Grid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct RiskThresholds {
|
||||
pub review: f64,
|
||||
pub confirm: f64,
|
||||
pub block: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
pub db_path: PathBuf,
|
||||
pub worktree_root: PathBuf,
|
||||
@@ -11,7 +29,11 @@ pub struct Config {
|
||||
pub session_timeout_secs: u64,
|
||||
pub heartbeat_interval_secs: u64,
|
||||
pub default_agent: String,
|
||||
pub cost_budget_usd: f64,
|
||||
pub token_budget: u64,
|
||||
pub theme: Theme,
|
||||
pub pane_layout: PaneLayout,
|
||||
pub risk_thresholds: RiskThresholds,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -31,12 +53,22 @@ impl Default for Config {
|
||||
session_timeout_secs: 3600,
|
||||
heartbeat_interval_secs: 30,
|
||||
default_agent: "claude".to_string(),
|
||||
cost_budget_usd: 10.0,
|
||||
token_budget: 500_000,
|
||||
theme: Theme::Dark,
|
||||
pane_layout: PaneLayout::Horizontal,
|
||||
risk_thresholds: Self::RISK_THRESHOLDS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub const RISK_THRESHOLDS: RiskThresholds = RiskThresholds {
|
||||
review: 0.35,
|
||||
confirm: 0.60,
|
||||
block: 0.85,
|
||||
};
|
||||
|
||||
pub fn load() -> Result<Self> {
|
||||
let config_path = dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
@@ -52,3 +84,61 @@ impl Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RiskThresholds {
|
||||
fn default() -> Self {
|
||||
Config::RISK_THRESHOLDS
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Config, PaneLayout};
|
||||
|
||||
#[test]
|
||||
fn default_includes_positive_budget_thresholds() {
|
||||
let config = Config::default();
|
||||
|
||||
assert!(config.cost_budget_usd > 0.0);
|
||||
assert!(config.token_budget > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_budget_fields_fall_back_to_defaults() {
|
||||
let legacy_config = r#"
|
||||
db_path = "/tmp/ecc2.db"
|
||||
worktree_root = "/tmp/ecc-worktrees"
|
||||
max_parallel_sessions = 8
|
||||
max_parallel_worktrees = 6
|
||||
session_timeout_secs = 3600
|
||||
heartbeat_interval_secs = 30
|
||||
default_agent = "claude"
|
||||
theme = "Dark"
|
||||
"#;
|
||||
|
||||
let config: Config = toml::from_str(legacy_config).unwrap();
|
||||
let defaults = Config::default();
|
||||
|
||||
assert_eq!(config.cost_budget_usd, defaults.cost_budget_usd);
|
||||
assert_eq!(config.token_budget, defaults.token_budget);
|
||||
assert_eq!(config.pane_layout, defaults.pane_layout);
|
||||
assert_eq!(config.risk_thresholds, defaults.risk_thresholds);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_pane_layout_is_horizontal() {
|
||||
assert_eq!(Config::default().pane_layout, PaneLayout::Horizontal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pane_layout_deserializes_from_toml() {
|
||||
let config: Config = toml::from_str(r#"pane_layout = "grid""#).unwrap();
|
||||
|
||||
assert_eq!(config.pane_layout, PaneLayout::Grid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_risk_thresholds_are_applied() {
|
||||
assert_eq!(Config::default().risk_thresholds, Config::RISK_THRESHOLDS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
mod comms;
|
||||
mod config;
|
||||
mod observability;
|
||||
mod session;
|
||||
mod tui;
|
||||
mod worktree;
|
||||
mod observability;
|
||||
mod comms;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -44,8 +45,24 @@ enum Commands {
|
||||
/// Session ID or alias
|
||||
session_id: String,
|
||||
},
|
||||
/// Resume a failed or stopped session
|
||||
Resume {
|
||||
/// Session ID or alias
|
||||
session_id: String,
|
||||
},
|
||||
/// Run as background daemon
|
||||
Daemon,
|
||||
#[command(hide = true)]
|
||||
RunSession {
|
||||
#[arg(long)]
|
||||
session_id: String,
|
||||
#[arg(long)]
|
||||
task: String,
|
||||
#[arg(long)]
|
||||
agent: String,
|
||||
#[arg(long)]
|
||||
cwd: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -63,10 +80,13 @@ async fn main() -> Result<()> {
|
||||
Some(Commands::Dashboard) | None => {
|
||||
tui::app::run(db, cfg).await?;
|
||||
}
|
||||
Some(Commands::Start { task, agent, worktree: use_worktree }) => {
|
||||
let session_id = session::manager::create_session(
|
||||
&db, &cfg, &task, &agent, use_worktree,
|
||||
).await?;
|
||||
Some(Commands::Start {
|
||||
task,
|
||||
agent,
|
||||
worktree: use_worktree,
|
||||
}) => {
|
||||
let session_id =
|
||||
session::manager::create_session(&db, &cfg, &task, &agent, use_worktree).await?;
|
||||
println!("Session started: {session_id}");
|
||||
}
|
||||
Some(Commands::Sessions) => {
|
||||
@@ -84,11 +104,39 @@ async fn main() -> Result<()> {
|
||||
session::manager::stop_session(&db, &session_id).await?;
|
||||
println!("Session stopped: {session_id}");
|
||||
}
|
||||
Some(Commands::Resume { session_id }) => {
|
||||
let resumed_id = session::manager::resume_session(&db, &session_id).await?;
|
||||
println!("Session resumed: {resumed_id}");
|
||||
}
|
||||
Some(Commands::Daemon) => {
|
||||
println!("Starting ECC daemon...");
|
||||
session::daemon::run(db, cfg).await?;
|
||||
}
|
||||
Some(Commands::RunSession {
|
||||
session_id,
|
||||
task,
|
||||
agent,
|
||||
cwd,
|
||||
}) => {
|
||||
session::manager::run_session(&cfg, &session_id, &task, &agent, &cwd).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cli_parses_resume_command() {
|
||||
let cli = Cli::try_parse_from(["ecc", "resume", "deadbeef"])
|
||||
.expect("resume subcommand should parse");
|
||||
|
||||
match cli.command {
|
||||
Some(Commands::Resume { session_id }) => assert_eq!(session_id, "deadbeef"),
|
||||
_ => panic!("expected resume subcommand"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::{Config, RiskThresholds};
|
||||
use crate::session::store::StateStore;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -13,42 +14,396 @@ pub struct ToolCallEvent {
|
||||
pub risk_score: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RiskAssessment {
|
||||
pub score: f64,
|
||||
pub reasons: Vec<String>,
|
||||
pub suggested_action: SuggestedAction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SuggestedAction {
|
||||
Allow,
|
||||
Review,
|
||||
RequireConfirmation,
|
||||
Block,
|
||||
}
|
||||
|
||||
impl ToolCallEvent {
|
||||
/// Compute risk score based on tool type and input patterns.
|
||||
pub fn compute_risk(tool_name: &str, input: &str) -> f64 {
|
||||
let mut score: f64 = 0.0;
|
||||
pub fn new(
|
||||
session_id: impl Into<String>,
|
||||
tool_name: impl Into<String>,
|
||||
input_summary: impl Into<String>,
|
||||
output_summary: impl Into<String>,
|
||||
duration_ms: u64,
|
||||
) -> Self {
|
||||
let tool_name = tool_name.into();
|
||||
let input_summary = input_summary.into();
|
||||
|
||||
// Destructive tools get higher base risk
|
||||
match tool_name {
|
||||
"Bash" => score += 0.3,
|
||||
"Write" => score += 0.2,
|
||||
"Edit" => score += 0.1,
|
||||
_ => score += 0.05,
|
||||
Self {
|
||||
session_id: session_id.into(),
|
||||
risk_score: Self::compute_risk(&tool_name, &input_summary, &Config::RISK_THRESHOLDS)
|
||||
.score,
|
||||
tool_name,
|
||||
input_summary,
|
||||
output_summary: output_summary.into(),
|
||||
duration_ms,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute risk from the tool type and input characteristics.
|
||||
pub fn compute_risk(
|
||||
tool_name: &str,
|
||||
input: &str,
|
||||
thresholds: &RiskThresholds,
|
||||
) -> RiskAssessment {
|
||||
let normalized_tool = tool_name.to_ascii_lowercase();
|
||||
let normalized_input = input.to_ascii_lowercase();
|
||||
let mut score = 0.0;
|
||||
let mut reasons = Vec::new();
|
||||
|
||||
let (base_score, base_reason) = base_tool_risk(&normalized_tool);
|
||||
score += base_score;
|
||||
if let Some(reason) = base_reason {
|
||||
reasons.push(reason.to_string());
|
||||
}
|
||||
|
||||
// Dangerous patterns in bash commands
|
||||
if tool_name == "Bash" {
|
||||
if input.contains("rm -rf") || input.contains("--force") {
|
||||
score += 0.4;
|
||||
}
|
||||
if input.contains("git push") || input.contains("git reset") {
|
||||
score += 0.3;
|
||||
}
|
||||
if input.contains("sudo") || input.contains("chmod 777") {
|
||||
score += 0.5;
|
||||
}
|
||||
let (file_sensitivity_score, file_sensitivity_reason) =
|
||||
assess_file_sensitivity(&normalized_input);
|
||||
score += file_sensitivity_score;
|
||||
if let Some(reason) = file_sensitivity_reason {
|
||||
reasons.push(reason);
|
||||
}
|
||||
|
||||
score.min(1.0)
|
||||
let (blast_radius_score, blast_radius_reason) = assess_blast_radius(&normalized_input);
|
||||
score += blast_radius_score;
|
||||
if let Some(reason) = blast_radius_reason {
|
||||
reasons.push(reason);
|
||||
}
|
||||
|
||||
let (irreversibility_score, irreversibility_reason) =
|
||||
assess_irreversibility(&normalized_input);
|
||||
score += irreversibility_score;
|
||||
if let Some(reason) = irreversibility_reason {
|
||||
reasons.push(reason);
|
||||
}
|
||||
|
||||
let score = score.clamp(0.0, 1.0);
|
||||
let suggested_action = SuggestedAction::from_score(score, thresholds);
|
||||
|
||||
RiskAssessment {
|
||||
score,
|
||||
reasons,
|
||||
suggested_action,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_tool_call(db: &StateStore, event: &ToolCallEvent) -> Result<()> {
|
||||
db.send_message(
|
||||
&event.session_id,
|
||||
"observability",
|
||||
&serde_json::to_string(event)?,
|
||||
"tool_call",
|
||||
)?;
|
||||
Ok(())
|
||||
impl SuggestedAction {
|
||||
fn from_score(score: f64, thresholds: &RiskThresholds) -> Self {
|
||||
if score >= thresholds.block {
|
||||
Self::Block
|
||||
} else if score >= thresholds.confirm {
|
||||
Self::RequireConfirmation
|
||||
} else if score >= thresholds.review {
|
||||
Self::Review
|
||||
} else {
|
||||
Self::Allow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn base_tool_risk(tool_name: &str) -> (f64, Option<&'static str>) {
|
||||
match tool_name {
|
||||
"bash" => (
|
||||
0.20,
|
||||
Some("shell execution can modify local or shared state"),
|
||||
),
|
||||
"write" | "multiedit" => (0.15, Some("writes files directly")),
|
||||
"edit" => (0.10, Some("modifies existing files")),
|
||||
_ => (0.05, None),
|
||||
}
|
||||
}
|
||||
|
||||
fn assess_file_sensitivity(input: &str) -> (f64, Option<String>) {
|
||||
const SECRET_PATTERNS: &[&str] = &[
|
||||
".env",
|
||||
"secret",
|
||||
"credential",
|
||||
"token",
|
||||
"api_key",
|
||||
"apikey",
|
||||
"auth",
|
||||
"id_rsa",
|
||||
".pem",
|
||||
".key",
|
||||
];
|
||||
const SHARED_INFRA_PATTERNS: &[&str] = &[
|
||||
"cargo.toml",
|
||||
"package.json",
|
||||
"dockerfile",
|
||||
".github/workflows",
|
||||
"schema",
|
||||
"migration",
|
||||
"production",
|
||||
];
|
||||
|
||||
if contains_any(input, SECRET_PATTERNS) {
|
||||
(
|
||||
0.25,
|
||||
Some("targets a sensitive file or credential surface".to_string()),
|
||||
)
|
||||
} else if contains_any(input, SHARED_INFRA_PATTERNS) {
|
||||
(
|
||||
0.15,
|
||||
Some("targets shared infrastructure or release-critical files".to_string()),
|
||||
)
|
||||
} else {
|
||||
(0.0, None)
|
||||
}
|
||||
}
|
||||
|
||||
fn assess_blast_radius(input: &str) -> (f64, Option<String>) {
|
||||
const LARGE_SCOPE_PATTERNS: &[&str] = &[
|
||||
"**",
|
||||
"/*",
|
||||
"--all",
|
||||
"--recursive",
|
||||
"entire repo",
|
||||
"all files",
|
||||
"across src/",
|
||||
"find ",
|
||||
" xargs ",
|
||||
];
|
||||
const SHARED_STATE_PATTERNS: &[&str] = &[
|
||||
"git push --force",
|
||||
"git push -f",
|
||||
"origin main",
|
||||
"origin master",
|
||||
"rm -rf .",
|
||||
"rm -rf /",
|
||||
];
|
||||
|
||||
if contains_any(input, SHARED_STATE_PATTERNS) {
|
||||
(
|
||||
0.35,
|
||||
Some("has a broad blast radius across shared state or history".to_string()),
|
||||
)
|
||||
} else if contains_any(input, LARGE_SCOPE_PATTERNS) {
|
||||
(
|
||||
0.25,
|
||||
Some("has a broad blast radius across multiple files or directories".to_string()),
|
||||
)
|
||||
} else {
|
||||
(0.0, None)
|
||||
}
|
||||
}
|
||||
|
||||
fn assess_irreversibility(input: &str) -> (f64, Option<String>) {
|
||||
const HIGH_IRREVERSIBILITY_PATTERNS: &[&str] = &[
|
||||
"rm -rf",
|
||||
"git reset --hard",
|
||||
"git clean -fd",
|
||||
"drop database",
|
||||
"drop table",
|
||||
"truncate ",
|
||||
"shred ",
|
||||
];
|
||||
const MODERATE_IRREVERSIBILITY_PATTERNS: &[&str] =
|
||||
&["rm -f", "git push --force", "git push -f", "delete from"];
|
||||
|
||||
if contains_any(input, HIGH_IRREVERSIBILITY_PATTERNS) {
|
||||
(
|
||||
0.45,
|
||||
Some("includes an irreversible or destructive operation".to_string()),
|
||||
)
|
||||
} else if contains_any(input, MODERATE_IRREVERSIBILITY_PATTERNS) {
|
||||
(
|
||||
0.40,
|
||||
Some("includes an irreversible or difficult-to-undo operation".to_string()),
|
||||
)
|
||||
} else {
|
||||
(0.0, None)
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_any(input: &str, patterns: &[&str]) -> bool {
|
||||
patterns.iter().any(|pattern| input.contains(pattern))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ToolLogEntry {
|
||||
pub id: i64,
|
||||
pub session_id: String,
|
||||
pub tool_name: String,
|
||||
pub input_summary: String,
|
||||
pub output_summary: String,
|
||||
pub duration_ms: u64,
|
||||
pub risk_score: f64,
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ToolLogPage {
|
||||
pub entries: Vec<ToolLogEntry>,
|
||||
pub page: u64,
|
||||
pub page_size: u64,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
pub struct ToolLogger<'a> {
|
||||
db: &'a StateStore,
|
||||
}
|
||||
|
||||
impl<'a> ToolLogger<'a> {
|
||||
pub fn new(db: &'a StateStore) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub fn log(&self, event: &ToolCallEvent) -> Result<ToolLogEntry> {
|
||||
let timestamp = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
self.db.insert_tool_log(
|
||||
&event.session_id,
|
||||
&event.tool_name,
|
||||
&event.input_summary,
|
||||
&event.output_summary,
|
||||
event.duration_ms,
|
||||
event.risk_score,
|
||||
×tamp,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn query(&self, session_id: &str, page: u64, page_size: u64) -> Result<ToolLogPage> {
|
||||
if page_size == 0 {
|
||||
bail!("page_size must be greater than 0");
|
||||
}
|
||||
|
||||
self.db.query_tool_logs(session_id, page.max(1), page_size)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_tool_call(db: &StateStore, event: &ToolCallEvent) -> Result<ToolLogEntry> {
|
||||
ToolLogger::new(db).log(event)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{SuggestedAction, ToolCallEvent, ToolLogger};
|
||||
use crate::config::Config;
|
||||
use crate::session::store::StateStore;
|
||||
use crate::session::{Session, SessionMetrics, SessionState};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn test_db_path() -> PathBuf {
|
||||
std::env::temp_dir().join(format!("ecc2-observability-{}.db", uuid::Uuid::new_v4()))
|
||||
}
|
||||
|
||||
fn test_session(id: &str) -> Session {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
Session {
|
||||
id: id.to_string(),
|
||||
task: "test task".to_string(),
|
||||
agent_type: "claude".to_string(),
|
||||
state: SessionState::Pending,
|
||||
pid: None,
|
||||
worktree: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
metrics: SessionMetrics::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn computes_sensitive_file_risk() {
|
||||
let assessment = ToolCallEvent::compute_risk(
|
||||
"Write",
|
||||
"Update .env.production with rotated API token",
|
||||
&Config::RISK_THRESHOLDS,
|
||||
);
|
||||
|
||||
assert!(assessment.score >= Config::RISK_THRESHOLDS.review);
|
||||
assert_eq!(assessment.suggested_action, SuggestedAction::Review);
|
||||
assert!(assessment
|
||||
.reasons
|
||||
.iter()
|
||||
.any(|reason| reason.contains("sensitive file")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn computes_blast_radius_risk() {
|
||||
let assessment = ToolCallEvent::compute_risk(
|
||||
"Edit",
|
||||
"Apply the same replacement across src/**/*.rs",
|
||||
&Config::RISK_THRESHOLDS,
|
||||
);
|
||||
|
||||
assert!(assessment.score >= Config::RISK_THRESHOLDS.review);
|
||||
assert_eq!(assessment.suggested_action, SuggestedAction::Review);
|
||||
assert!(assessment
|
||||
.reasons
|
||||
.iter()
|
||||
.any(|reason| reason.contains("blast radius")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn computes_irreversible_risk() {
|
||||
let assessment = ToolCallEvent::compute_risk(
|
||||
"Bash",
|
||||
"rm -f /tmp/ecc-temp.txt",
|
||||
&Config::RISK_THRESHOLDS,
|
||||
);
|
||||
|
||||
assert!(assessment.score >= Config::RISK_THRESHOLDS.confirm);
|
||||
assert_eq!(
|
||||
assessment.suggested_action,
|
||||
SuggestedAction::RequireConfirmation,
|
||||
);
|
||||
assert!(assessment
|
||||
.reasons
|
||||
.iter()
|
||||
.any(|reason| reason.contains("irreversible")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocks_combined_high_risk_operations() {
|
||||
let assessment = ToolCallEvent::compute_risk(
|
||||
"Bash",
|
||||
"rm -rf . && git push --force origin main",
|
||||
&Config::RISK_THRESHOLDS,
|
||||
);
|
||||
|
||||
assert!(assessment.score >= Config::RISK_THRESHOLDS.block);
|
||||
assert_eq!(assessment.suggested_action, SuggestedAction::Block);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logger_persists_entries_and_paginates() -> anyhow::Result<()> {
|
||||
let db_path = test_db_path();
|
||||
let db = StateStore::open(&db_path)?;
|
||||
db.insert_session(&test_session("sess-1"))?;
|
||||
|
||||
let logger = ToolLogger::new(&db);
|
||||
|
||||
logger.log(&ToolCallEvent::new("sess-1", "Read", "first", "ok", 5))?;
|
||||
logger.log(&ToolCallEvent::new("sess-1", "Write", "second", "ok", 15))?;
|
||||
logger.log(&ToolCallEvent::new("sess-1", "Bash", "third", "ok", 25))?;
|
||||
|
||||
let first_page = logger.query("sess-1", 1, 2)?;
|
||||
assert_eq!(first_page.total, 3);
|
||||
assert_eq!(first_page.entries.len(), 2);
|
||||
assert_eq!(first_page.entries[0].tool_name, "Bash");
|
||||
assert_eq!(first_page.entries[1].tool_name, "Write");
|
||||
|
||||
let second_page = logger.query("sess-1", 2, 2)?;
|
||||
assert_eq!(second_page.total, 3);
|
||||
assert_eq!(second_page.entries.len(), 1);
|
||||
assert_eq!(second_page.entries[0].tool_name, "Read");
|
||||
|
||||
std::fs::remove_file(&db_path).ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::config::Config;
|
||||
/// and cleans up stale resources.
|
||||
pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
|
||||
tracing::info!("ECC daemon started");
|
||||
resume_crashed_sessions(&db)?;
|
||||
|
||||
let heartbeat_interval = Duration::from_secs(cfg.heartbeat_interval_secs);
|
||||
let timeout = Duration::from_secs(cfg.session_timeout_secs);
|
||||
@@ -23,6 +24,43 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resume_crashed_sessions(db: &StateStore) -> Result<()> {
|
||||
let failed_sessions = resume_crashed_sessions_with(db, pid_is_alive)?;
|
||||
if failed_sessions > 0 {
|
||||
tracing::warn!("Marked {failed_sessions} crashed sessions as failed during daemon startup");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resume_crashed_sessions_with<F>(db: &StateStore, is_pid_alive: F) -> Result<usize>
|
||||
where
|
||||
F: Fn(u32) -> bool,
|
||||
{
|
||||
let sessions = db.list_sessions()?;
|
||||
let mut failed_sessions = 0;
|
||||
|
||||
for session in sessions {
|
||||
if session.state != SessionState::Running {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_alive = session.pid.is_some_and(&is_pid_alive);
|
||||
if is_alive {
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::warn!(
|
||||
"Session {} was left running with stale pid {:?}; marking it failed",
|
||||
session.id,
|
||||
session.pid
|
||||
);
|
||||
db.update_state_and_pid(&session.id, &SessionState::Failed, None)?;
|
||||
failed_sessions += 1;
|
||||
}
|
||||
|
||||
Ok(failed_sessions)
|
||||
}
|
||||
|
||||
fn check_sessions(db: &StateStore, timeout: Duration) -> Result<()> {
|
||||
let sessions = db.list_sessions()?;
|
||||
|
||||
@@ -38,9 +76,102 @@ fn check_sessions(db: &StateStore, timeout: Duration) -> Result<()> {
|
||||
|
||||
if elapsed > timeout {
|
||||
tracing::warn!("Session {} timed out after {:?}", session.id, elapsed);
|
||||
db.update_state(&session.id, &SessionState::Failed)?;
|
||||
db.update_state_and_pid(&session.id, &SessionState::Failed, None)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn pid_is_alive(pid: u32) -> bool {
|
||||
if pid == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SAFETY: kill(pid, 0) probes process existence without delivering a signal.
|
||||
let result = unsafe { libc::kill(pid as libc::pid_t, 0) };
|
||||
if result == 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
matches!(
|
||||
std::io::Error::last_os_error().raw_os_error(),
|
||||
Some(code) if code == libc::EPERM
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn pid_is_alive(_pid: u32) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::session::{Session, SessionMetrics, SessionState};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn temp_db_path() -> PathBuf {
|
||||
std::env::temp_dir().join(format!("ecc2-daemon-test-{}.db", uuid::Uuid::new_v4()))
|
||||
}
|
||||
|
||||
fn sample_session(id: &str, state: SessionState, pid: Option<u32>) -> Session {
|
||||
let now = chrono::Utc::now();
|
||||
Session {
|
||||
id: id.to_string(),
|
||||
task: "Recover crashed worker".to_string(),
|
||||
agent_type: "claude".to_string(),
|
||||
state,
|
||||
pid,
|
||||
worktree: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
metrics: SessionMetrics::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_crashed_sessions_marks_dead_running_sessions_failed() -> Result<()> {
|
||||
let path = temp_db_path();
|
||||
let store = StateStore::open(&path)?;
|
||||
store.insert_session(&sample_session(
|
||||
"deadbeef",
|
||||
SessionState::Running,
|
||||
Some(4242),
|
||||
))?;
|
||||
|
||||
resume_crashed_sessions_with(&store, |_| false)?;
|
||||
|
||||
let session = store
|
||||
.get_session("deadbeef")?
|
||||
.expect("session should still exist");
|
||||
assert_eq!(session.state, SessionState::Failed);
|
||||
assert_eq!(session.pid, None);
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_crashed_sessions_keeps_live_running_sessions_running() -> Result<()> {
|
||||
let path = temp_db_path();
|
||||
let store = StateStore::open(&path)?;
|
||||
store.insert_session(&sample_session(
|
||||
"alive123",
|
||||
SessionState::Running,
|
||||
Some(7777),
|
||||
))?;
|
||||
|
||||
resume_crashed_sessions_with(&store, |_| true)?;
|
||||
|
||||
let session = store
|
||||
.get_session("alive123")?
|
||||
.expect("session should still exist");
|
||||
assert_eq!(session.state, SessionState::Running);
|
||||
assert_eq!(session.pid, Some(7777));
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use tokio::process::Command;
|
||||
|
||||
use super::output::SessionOutputStore;
|
||||
use super::runtime::capture_command_output;
|
||||
use super::store::StateStore;
|
||||
use super::{Session, SessionMetrics, SessionState};
|
||||
use crate::config::Config;
|
||||
use crate::observability::{log_tool_call, ToolCallEvent, ToolLogEntry, ToolLogPage, ToolLogger};
|
||||
use crate::worktree;
|
||||
|
||||
pub async fn create_session(
|
||||
@@ -18,18 +21,7 @@ pub async fn create_session(
|
||||
) -> Result<String> {
|
||||
let repo_root =
|
||||
std::env::current_dir().context("Failed to resolve current working directory")?;
|
||||
let agent_program = agent_program(agent_type)?;
|
||||
|
||||
create_session_in_dir(
|
||||
db,
|
||||
cfg,
|
||||
task,
|
||||
agent_type,
|
||||
use_worktree,
|
||||
&repo_root,
|
||||
&agent_program,
|
||||
)
|
||||
.await
|
||||
queue_session_in_dir(db, cfg, task, agent_type, use_worktree, &repo_root).await
|
||||
}
|
||||
|
||||
pub fn list_sessions(db: &StateStore) -> Result<Vec<Session>> {
|
||||
@@ -45,6 +37,59 @@ pub async fn stop_session(db: &StateStore, id: &str) -> Result<()> {
|
||||
stop_session_with_options(db, id, true).await
|
||||
}
|
||||
|
||||
pub fn record_tool_call(
|
||||
db: &StateStore,
|
||||
session_id: &str,
|
||||
tool_name: &str,
|
||||
input_summary: &str,
|
||||
output_summary: &str,
|
||||
duration_ms: u64,
|
||||
) -> Result<ToolLogEntry> {
|
||||
let session = db
|
||||
.get_session(session_id)?
|
||||
.ok_or_else(|| anyhow::anyhow!("Session not found: {session_id}"))?;
|
||||
|
||||
let event = ToolCallEvent::new(
|
||||
session.id.clone(),
|
||||
tool_name,
|
||||
input_summary,
|
||||
output_summary,
|
||||
duration_ms,
|
||||
);
|
||||
let entry = log_tool_call(db, &event)?;
|
||||
db.increment_tool_calls(&session.id)?;
|
||||
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
pub fn query_tool_calls(
|
||||
db: &StateStore,
|
||||
session_id: &str,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
) -> Result<ToolLogPage> {
|
||||
let session = db
|
||||
.get_session(session_id)?
|
||||
.ok_or_else(|| anyhow::anyhow!("Session not found: {session_id}"))?;
|
||||
|
||||
ToolLogger::new(db).query(&session.id, page, page_size)
|
||||
}
|
||||
|
||||
pub async fn resume_session(db: &StateStore, id: &str) -> Result<String> {
|
||||
let session = resolve_session(db, id)?;
|
||||
|
||||
if session.state == SessionState::Completed {
|
||||
anyhow::bail!("Completed sessions cannot be resumed: {}", session.id);
|
||||
}
|
||||
|
||||
if session.state == SessionState::Running {
|
||||
anyhow::bail!("Session is already running: {}", session.id);
|
||||
}
|
||||
|
||||
db.update_state_and_pid(&session.id, &SessionState::Pending, None)?;
|
||||
Ok(session.id)
|
||||
}
|
||||
|
||||
fn agent_program(agent_type: &str) -> Result<PathBuf> {
|
||||
match agent_type {
|
||||
"claude" => Ok(PathBuf::from("claude")),
|
||||
@@ -62,6 +107,97 @@ fn resolve_session(db: &StateStore, id: &str) -> Result<Session> {
|
||||
session.ok_or_else(|| anyhow::anyhow!("Session not found: {id}"))
|
||||
}
|
||||
|
||||
pub async fn run_session(
|
||||
cfg: &Config,
|
||||
session_id: &str,
|
||||
task: &str,
|
||||
agent_type: &str,
|
||||
working_dir: &Path,
|
||||
) -> Result<()> {
|
||||
let db = StateStore::open(&cfg.db_path)?;
|
||||
let session = resolve_session(&db, session_id)?;
|
||||
|
||||
if session.state != SessionState::Pending {
|
||||
tracing::info!(
|
||||
"Skipping run_session for {} because state is {}",
|
||||
session_id,
|
||||
session.state
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let agent_program = agent_program(agent_type)?;
|
||||
let command = build_agent_command(&agent_program, task, session_id, working_dir);
|
||||
capture_command_output(
|
||||
cfg.db_path.clone(),
|
||||
session_id.to_string(),
|
||||
command,
|
||||
SessionOutputStore::default(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn queue_session_in_dir(
|
||||
db: &StateStore,
|
||||
cfg: &Config,
|
||||
task: &str,
|
||||
agent_type: &str,
|
||||
use_worktree: bool,
|
||||
repo_root: &Path,
|
||||
) -> Result<String> {
|
||||
let session = build_session_record(task, agent_type, use_worktree, cfg, repo_root)?;
|
||||
db.insert_session(&session)?;
|
||||
|
||||
let working_dir = session
|
||||
.worktree
|
||||
.as_ref()
|
||||
.map(|worktree| worktree.path.as_path())
|
||||
.unwrap_or(repo_root);
|
||||
|
||||
match spawn_session_runner(task, &session.id, agent_type, working_dir).await {
|
||||
Ok(()) => Ok(session.id),
|
||||
Err(error) => {
|
||||
db.update_state(&session.id, &SessionState::Failed)?;
|
||||
|
||||
if let Some(worktree) = session.worktree.as_ref() {
|
||||
let _ = crate::worktree::remove(&worktree.path);
|
||||
}
|
||||
|
||||
Err(error.context(format!("Failed to queue session {}", session.id)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_session_record(
|
||||
task: &str,
|
||||
agent_type: &str,
|
||||
use_worktree: bool,
|
||||
cfg: &Config,
|
||||
repo_root: &Path,
|
||||
) -> Result<Session> {
|
||||
let id = uuid::Uuid::new_v4().to_string()[..8].to_string();
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let worktree = if use_worktree {
|
||||
Some(worktree::create_for_session_in_repo(&id, cfg, repo_root)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Session {
|
||||
id,
|
||||
task: task.to_string(),
|
||||
agent_type: agent_type.to_string(),
|
||||
state: SessionState::Pending,
|
||||
pid: None,
|
||||
worktree,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
metrics: SessionMetrics::default(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_session_in_dir(
|
||||
db: &StateStore,
|
||||
cfg: &Config,
|
||||
@@ -71,26 +207,7 @@ async fn create_session_in_dir(
|
||||
repo_root: &Path,
|
||||
agent_program: &Path,
|
||||
) -> Result<String> {
|
||||
let id = uuid::Uuid::new_v4().to_string()[..8].to_string();
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let wt = if use_worktree {
|
||||
Some(worktree::create_for_session_in_repo(&id, cfg, repo_root)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let session = Session {
|
||||
id: id.clone(),
|
||||
task: task.to_string(),
|
||||
agent_type: agent_type.to_string(),
|
||||
state: SessionState::Pending,
|
||||
pid: None,
|
||||
worktree: wt,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
metrics: SessionMetrics::default(),
|
||||
};
|
||||
let session = build_session_record(task, agent_type, use_worktree, cfg, repo_root)?;
|
||||
|
||||
db.insert_session(&session)?;
|
||||
|
||||
@@ -118,19 +235,60 @@ async fn create_session_in_dir(
|
||||
}
|
||||
}
|
||||
|
||||
async fn spawn_session_runner(
|
||||
task: &str,
|
||||
session_id: &str,
|
||||
agent_type: &str,
|
||||
working_dir: &Path,
|
||||
) -> Result<()> {
|
||||
let current_exe = std::env::current_exe().context("Failed to resolve ECC executable path")?;
|
||||
let child = Command::new(¤t_exe)
|
||||
.arg("run-session")
|
||||
.arg("--session-id")
|
||||
.arg(session_id)
|
||||
.arg("--task")
|
||||
.arg(task)
|
||||
.arg("--agent")
|
||||
.arg(agent_type)
|
||||
.arg("--cwd")
|
||||
.arg(working_dir)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to spawn ECC runner from {}",
|
||||
current_exe.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
child
|
||||
.id()
|
||||
.ok_or_else(|| anyhow::anyhow!("ECC runner did not expose a process id"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_agent_command(agent_program: &Path, task: &str, session_id: &str, working_dir: &Path) -> Command {
|
||||
let mut command = Command::new(agent_program);
|
||||
command
|
||||
.arg("--print")
|
||||
.arg("--name")
|
||||
.arg(format!("ecc-{session_id}"))
|
||||
.arg(task)
|
||||
.current_dir(working_dir)
|
||||
.stdin(Stdio::null());
|
||||
command
|
||||
}
|
||||
|
||||
async fn spawn_claude_code(
|
||||
agent_program: &Path,
|
||||
task: &str,
|
||||
session_id: &str,
|
||||
working_dir: &Path,
|
||||
) -> Result<u32> {
|
||||
let child = Command::new(agent_program)
|
||||
.arg("--print")
|
||||
.arg("--name")
|
||||
.arg(format!("ecc-{session_id}"))
|
||||
.arg(task)
|
||||
.current_dir(working_dir)
|
||||
.stdin(Stdio::null())
|
||||
let mut command = build_agent_command(agent_program, task, session_id, working_dir);
|
||||
let child = command
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
@@ -238,7 +396,7 @@ impl fmt::Display for SessionStatus {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::{Config, Theme};
|
||||
use crate::config::{Config, PaneLayout, Theme};
|
||||
use crate::session::{Session, SessionMetrics, SessionState};
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{Duration, Utc};
|
||||
@@ -281,7 +439,11 @@ mod tests {
|
||||
session_timeout_secs: 60,
|
||||
heartbeat_interval_secs: 5,
|
||||
default_agent: "claude".to_string(),
|
||||
cost_budget_usd: 10.0,
|
||||
token_budget: 500_000,
|
||||
theme: Theme::Dark,
|
||||
pane_layout: PaneLayout::Horizontal,
|
||||
risk_thresholds: Config::RISK_THRESHOLDS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,6 +631,36 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn resume_session_requeues_failed_session() -> Result<()> {
|
||||
let tempdir = TestDir::new("manager-resume-session")?;
|
||||
let cfg = build_config(tempdir.path());
|
||||
let db = StateStore::open(&cfg.db_path)?;
|
||||
let now = Utc::now();
|
||||
|
||||
db.insert_session(&Session {
|
||||
id: "deadbeef".to_string(),
|
||||
task: "resume previous task".to_string(),
|
||||
agent_type: "claude".to_string(),
|
||||
state: SessionState::Failed,
|
||||
pid: Some(31337),
|
||||
worktree: None,
|
||||
created_at: now - Duration::minutes(1),
|
||||
updated_at: now,
|
||||
metrics: SessionMetrics::default(),
|
||||
})?;
|
||||
|
||||
let resumed_id = resume_session(&db, "deadbeef").await?;
|
||||
let resumed = db
|
||||
.get_session(&resumed_id)?
|
||||
.context("resumed session should exist")?;
|
||||
|
||||
assert_eq!(resumed.state, SessionState::Pending);
|
||||
assert_eq!(resumed.pid, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_status_supports_latest_alias() -> Result<()> {
|
||||
let tempdir = TestDir::new("manager-latest-status")?;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod daemon;
|
||||
pub mod manager;
|
||||
pub mod output;
|
||||
pub mod runtime;
|
||||
pub mod store;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
149
ecc2/src/session/output.rs
Normal file
149
ecc2/src/session/output.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
pub const OUTPUT_BUFFER_LIMIT: usize = 1000;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum OutputStream {
|
||||
Stdout,
|
||||
Stderr,
|
||||
}
|
||||
|
||||
impl OutputStream {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Stdout => "stdout",
|
||||
Self::Stderr => "stderr",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_db_value(value: &str) -> Self {
|
||||
match value {
|
||||
"stderr" => Self::Stderr,
|
||||
_ => Self::Stdout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OutputLine {
|
||||
pub stream: OutputStream,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OutputEvent {
|
||||
pub session_id: String,
|
||||
pub line: OutputLine,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SessionOutputStore {
|
||||
capacity: usize,
|
||||
buffers: Arc<Mutex<HashMap<String, VecDeque<OutputLine>>>>,
|
||||
tx: broadcast::Sender<OutputEvent>,
|
||||
}
|
||||
|
||||
impl Default for SessionOutputStore {
|
||||
fn default() -> Self {
|
||||
Self::new(OUTPUT_BUFFER_LIMIT)
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionOutputStore {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
let capacity = capacity.max(1);
|
||||
let (tx, _) = broadcast::channel(capacity.max(16));
|
||||
|
||||
Self {
|
||||
capacity,
|
||||
buffers: Arc::new(Mutex::new(HashMap::new())),
|
||||
tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<OutputEvent> {
|
||||
self.tx.subscribe()
|
||||
}
|
||||
|
||||
pub fn push_line(&self, session_id: &str, stream: OutputStream, text: impl Into<String>) {
|
||||
let line = OutputLine {
|
||||
stream,
|
||||
text: text.into(),
|
||||
};
|
||||
|
||||
{
|
||||
let mut buffers = self.lock_buffers();
|
||||
let buffer = buffers.entry(session_id.to_string()).or_default();
|
||||
buffer.push_back(line.clone());
|
||||
|
||||
while buffer.len() > self.capacity {
|
||||
let _ = buffer.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.tx.send(OutputEvent {
|
||||
session_id: session_id.to_string(),
|
||||
line,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn replace_lines(&self, session_id: &str, lines: Vec<OutputLine>) {
|
||||
let mut buffer: VecDeque<OutputLine> = lines.into_iter().collect();
|
||||
|
||||
while buffer.len() > self.capacity {
|
||||
let _ = buffer.pop_front();
|
||||
}
|
||||
|
||||
self.lock_buffers().insert(session_id.to_string(), buffer);
|
||||
}
|
||||
|
||||
pub fn lines(&self, session_id: &str) -> Vec<OutputLine> {
|
||||
self.lock_buffers()
|
||||
.get(session_id)
|
||||
.map(|buffer| buffer.iter().cloned().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn lock_buffers(&self) -> MutexGuard<'_, HashMap<String, VecDeque<OutputLine>>> {
|
||||
self.buffers
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{OutputStream, SessionOutputStore};
|
||||
|
||||
#[test]
|
||||
fn ring_buffer_keeps_most_recent_lines() {
|
||||
let store = SessionOutputStore::new(3);
|
||||
|
||||
store.push_line("session-1", OutputStream::Stdout, "line-1");
|
||||
store.push_line("session-1", OutputStream::Stdout, "line-2");
|
||||
store.push_line("session-1", OutputStream::Stdout, "line-3");
|
||||
store.push_line("session-1", OutputStream::Stdout, "line-4");
|
||||
|
||||
let lines = store.lines("session-1");
|
||||
let texts: Vec<_> = lines.iter().map(|line| line.text.as_str()).collect();
|
||||
|
||||
assert_eq!(texts, vec!["line-2", "line-3", "line-4"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pushing_output_broadcasts_events() {
|
||||
let store = SessionOutputStore::new(8);
|
||||
let mut rx = store.subscribe();
|
||||
|
||||
store.push_line("session-1", OutputStream::Stderr, "problem");
|
||||
|
||||
let event = rx.recv().await.expect("broadcast event");
|
||||
assert_eq!(event.session_id, "session-1");
|
||||
assert_eq!(event.line.stream, OutputStream::Stderr);
|
||||
assert_eq!(event.line.text, "problem");
|
||||
}
|
||||
}
|
||||
290
ecc2/src/session/runtime.rs
Normal file
290
ecc2/src/session/runtime.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::{ExitStatus, Stdio};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
use super::output::{OutputStream, SessionOutputStore};
|
||||
use super::store::StateStore;
|
||||
use super::SessionState;
|
||||
|
||||
type DbAck = std::result::Result<(), String>;
|
||||
|
||||
enum DbMessage {
|
||||
UpdateState {
|
||||
state: SessionState,
|
||||
ack: oneshot::Sender<DbAck>,
|
||||
},
|
||||
UpdatePid {
|
||||
pid: Option<u32>,
|
||||
ack: oneshot::Sender<DbAck>,
|
||||
},
|
||||
AppendOutputLine {
|
||||
stream: OutputStream,
|
||||
line: String,
|
||||
ack: oneshot::Sender<DbAck>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DbWriter {
|
||||
tx: mpsc::UnboundedSender<DbMessage>,
|
||||
}
|
||||
|
||||
impl DbWriter {
|
||||
fn start(db_path: PathBuf, session_id: String) -> Self {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
std::thread::spawn(move || run_db_writer(db_path, session_id, rx));
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
async fn update_state(&self, state: SessionState) -> Result<()> {
|
||||
self.send(|ack| DbMessage::UpdateState { state, ack }).await
|
||||
}
|
||||
|
||||
async fn update_pid(&self, pid: Option<u32>) -> Result<()> {
|
||||
self.send(|ack| DbMessage::UpdatePid { pid, ack }).await
|
||||
}
|
||||
|
||||
async fn append_output_line(&self, stream: OutputStream, line: String) -> Result<()> {
|
||||
self.send(|ack| DbMessage::AppendOutputLine { stream, line, ack })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send<F>(&self, build: F) -> Result<()>
|
||||
where
|
||||
F: FnOnce(oneshot::Sender<DbAck>) -> DbMessage,
|
||||
{
|
||||
let (ack_tx, ack_rx) = oneshot::channel();
|
||||
self.tx
|
||||
.send(build(ack_tx))
|
||||
.map_err(|_| anyhow::anyhow!("DB writer channel closed"))?;
|
||||
|
||||
match ack_rx.await {
|
||||
Ok(Ok(())) => Ok(()),
|
||||
Ok(Err(error)) => Err(anyhow::anyhow!(error)),
|
||||
Err(_) => Err(anyhow::anyhow!("DB writer acknowledgement dropped")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_db_writer(
|
||||
db_path: PathBuf,
|
||||
session_id: String,
|
||||
mut rx: mpsc::UnboundedReceiver<DbMessage>,
|
||||
) {
|
||||
let (opened, open_error) = match StateStore::open(&db_path) {
|
||||
Ok(db) => (Some(db), None),
|
||||
Err(error) => (None, Some(error.to_string())),
|
||||
};
|
||||
|
||||
while let Some(message) = rx.blocking_recv() {
|
||||
match message {
|
||||
DbMessage::UpdateState { state, ack } => {
|
||||
let result = match opened.as_ref() {
|
||||
Some(db) => db.update_state(&session_id, &state).map_err(|error| error.to_string()),
|
||||
None => Err(open_error
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Failed to open state store".to_string())),
|
||||
};
|
||||
let _ = ack.send(result);
|
||||
}
|
||||
DbMessage::UpdatePid { pid, ack } => {
|
||||
let result = match opened.as_ref() {
|
||||
Some(db) => db.update_pid(&session_id, pid).map_err(|error| error.to_string()),
|
||||
None => Err(open_error
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Failed to open state store".to_string())),
|
||||
};
|
||||
let _ = ack.send(result);
|
||||
}
|
||||
DbMessage::AppendOutputLine { stream, line, ack } => {
|
||||
let result = match opened.as_ref() {
|
||||
Some(db) => db
|
||||
.append_output_line(&session_id, stream, &line)
|
||||
.map_err(|error| error.to_string()),
|
||||
None => Err(open_error
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Failed to open state store".to_string())),
|
||||
};
|
||||
let _ = ack.send(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn capture_command_output(
|
||||
db_path: PathBuf,
|
||||
session_id: String,
|
||||
mut command: Command,
|
||||
output_store: SessionOutputStore,
|
||||
) -> Result<ExitStatus> {
|
||||
let db_writer = DbWriter::start(db_path, session_id.clone());
|
||||
|
||||
let result = async {
|
||||
let mut child = command
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to start process for session {}", session_id))?;
|
||||
|
||||
let stdout = match child.stdout.take() {
|
||||
Some(stdout) => stdout,
|
||||
None => {
|
||||
let _ = child.kill().await;
|
||||
let _ = child.wait().await;
|
||||
anyhow::bail!("Child stdout was not piped");
|
||||
}
|
||||
};
|
||||
let stderr = match child.stderr.take() {
|
||||
Some(stderr) => stderr,
|
||||
None => {
|
||||
let _ = child.kill().await;
|
||||
let _ = child.wait().await;
|
||||
anyhow::bail!("Child stderr was not piped");
|
||||
}
|
||||
};
|
||||
|
||||
let pid = child
|
||||
.id()
|
||||
.ok_or_else(|| anyhow::anyhow!("Spawned process did not expose a process id"))?;
|
||||
db_writer.update_pid(Some(pid)).await?;
|
||||
db_writer.update_state(SessionState::Running).await?;
|
||||
|
||||
let stdout_task = tokio::spawn(capture_stream(
|
||||
session_id.clone(),
|
||||
stdout,
|
||||
OutputStream::Stdout,
|
||||
output_store.clone(),
|
||||
db_writer.clone(),
|
||||
));
|
||||
let stderr_task = tokio::spawn(capture_stream(
|
||||
session_id.clone(),
|
||||
stderr,
|
||||
OutputStream::Stderr,
|
||||
output_store,
|
||||
db_writer.clone(),
|
||||
));
|
||||
|
||||
let status = child.wait().await?;
|
||||
stdout_task.await??;
|
||||
stderr_task.await??;
|
||||
|
||||
let final_state = if status.success() {
|
||||
SessionState::Completed
|
||||
} else {
|
||||
SessionState::Failed
|
||||
};
|
||||
db_writer.update_pid(None).await?;
|
||||
db_writer.update_state(final_state).await?;
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
let _ = db_writer.update_pid(None).await;
|
||||
let _ = db_writer.update_state(SessionState::Failed).await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn capture_stream<R>(
|
||||
session_id: String,
|
||||
reader: R,
|
||||
stream: OutputStream,
|
||||
output_store: SessionOutputStore,
|
||||
db_writer: DbWriter,
|
||||
) -> Result<()>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
let mut lines = BufReader::new(reader).lines();
|
||||
|
||||
while let Some(line) = lines.next_line().await? {
|
||||
db_writer
|
||||
.append_output_line(stream, line.clone())
|
||||
.await?;
|
||||
output_store.push_line(&session_id, stream, line);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use tokio::process::Command;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::capture_command_output;
|
||||
use crate::session::output::{SessionOutputStore, OUTPUT_BUFFER_LIMIT};
|
||||
use crate::session::store::StateStore;
|
||||
use crate::session::{Session, SessionMetrics, SessionState};
|
||||
|
||||
#[tokio::test]
|
||||
async fn capture_command_output_persists_lines_and_events() -> Result<()> {
|
||||
let db_path = env::temp_dir().join(format!("ecc2-runtime-{}.db", Uuid::new_v4()));
|
||||
let db = StateStore::open(&db_path)?;
|
||||
let session_id = "session-1".to_string();
|
||||
let now = Utc::now();
|
||||
|
||||
db.insert_session(&Session {
|
||||
id: session_id.clone(),
|
||||
task: "stream output".to_string(),
|
||||
agent_type: "test".to_string(),
|
||||
state: SessionState::Pending,
|
||||
pid: None,
|
||||
worktree: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
metrics: SessionMetrics::default(),
|
||||
})?;
|
||||
|
||||
let output_store = SessionOutputStore::default();
|
||||
let mut rx = output_store.subscribe();
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command
|
||||
.arg("-c")
|
||||
.arg("printf 'alpha\\n'; printf 'beta\\n' >&2");
|
||||
|
||||
let status =
|
||||
capture_command_output(db_path.clone(), session_id.clone(), command, output_store)
|
||||
.await?;
|
||||
|
||||
assert!(status.success());
|
||||
|
||||
let db = StateStore::open(&db_path)?;
|
||||
let session = db
|
||||
.get_session(&session_id)?
|
||||
.expect("session should still exist");
|
||||
assert_eq!(session.state, SessionState::Completed);
|
||||
assert_eq!(session.pid, None);
|
||||
|
||||
let lines = db.get_output_lines(&session_id, OUTPUT_BUFFER_LIMIT)?;
|
||||
let texts: HashSet<_> = lines.iter().map(|line| line.text.as_str()).collect();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert!(texts.contains("alpha"));
|
||||
assert!(texts.contains("beta"));
|
||||
|
||||
let mut events = Vec::new();
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
events.push(event.line.text);
|
||||
}
|
||||
|
||||
assert_eq!(events.len(), 2);
|
||||
assert!(events.iter().any(|line| line == "alpha"));
|
||||
assert!(events.iter().any(|line| line == "beta"));
|
||||
|
||||
let _ = std::fs::remove_file(db_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
use anyhow::{Context, Result};
|
||||
use rusqlite::{Connection, OptionalExtension};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::observability::{ToolLogEntry, ToolLogPage};
|
||||
|
||||
use super::output::{OutputLine, OutputStream, OUTPUT_BUFFER_LIMIT};
|
||||
use super::{Session, SessionMetrics, SessionState};
|
||||
|
||||
pub struct StateStore {
|
||||
@@ -11,6 +15,8 @@ pub struct StateStore {
|
||||
impl StateStore {
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let conn = Connection::open(path)?;
|
||||
conn.execute_batch("PRAGMA foreign_keys = ON;")?;
|
||||
conn.busy_timeout(Duration::from_secs(5))?;
|
||||
let store = Self { conn };
|
||||
store.init_schema()?;
|
||||
Ok(store)
|
||||
@@ -58,9 +64,19 @@ impl StateStore {
|
||||
timestamp TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_output (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL REFERENCES sessions(id),
|
||||
stream TEXT NOT NULL,
|
||||
line TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_state ON sessions(state);
|
||||
CREATE INDEX IF NOT EXISTS idx_tool_log_session ON tool_log(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_session, read);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_output_session
|
||||
ON session_output(session_id, id);
|
||||
",
|
||||
)?;
|
||||
self.ensure_session_columns()?;
|
||||
@@ -97,7 +113,10 @@ impl StateStore {
|
||||
session.agent_type,
|
||||
session.state.to_string(),
|
||||
session.pid.map(i64::from),
|
||||
session.worktree.as_ref().map(|w| w.path.to_string_lossy().to_string()),
|
||||
session
|
||||
.worktree
|
||||
.as_ref()
|
||||
.map(|w| w.path.to_string_lossy().to_string()),
|
||||
session.worktree.as_ref().map(|w| w.branch.clone()),
|
||||
session.worktree.as_ref().map(|w| w.base_branch.clone()),
|
||||
session.created_at.to_rfc3339(),
|
||||
@@ -107,6 +126,29 @@ impl StateStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_state_and_pid(
|
||||
&self,
|
||||
session_id: &str,
|
||||
state: &SessionState,
|
||||
pid: Option<u32>,
|
||||
) -> Result<()> {
|
||||
let updated = self.conn.execute(
|
||||
"UPDATE sessions SET state = ?1, pid = ?2, updated_at = ?3 WHERE id = ?4",
|
||||
rusqlite::params![
|
||||
state.to_string(),
|
||||
pid.map(i64::from),
|
||||
chrono::Utc::now().to_rfc3339(),
|
||||
session_id,
|
||||
],
|
||||
)?;
|
||||
|
||||
if updated == 0 {
|
||||
anyhow::bail!("Session not found: {session_id}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_state(&self, session_id: &str, state: &SessionState) -> Result<()> {
|
||||
let current_state = self
|
||||
.conn
|
||||
@@ -176,6 +218,14 @@ impl StateStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn increment_tool_calls(&self, session_id: &str) -> Result<()> {
|
||||
self.conn.execute(
|
||||
"UPDATE sessions SET tool_calls = tool_calls + 1, updated_at = ?1 WHERE id = ?2",
|
||||
rusqlite::params![chrono::Utc::now().to_rfc3339(), session_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_sessions(&self) -> Result<Vec<Session>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, task, agent_type, state, pid, worktree_path, worktree_branch, worktree_base,
|
||||
@@ -190,8 +240,8 @@ impl StateStore {
|
||||
let state = SessionState::from_db_value(&state_str);
|
||||
|
||||
let worktree_path: Option<String> = row.get(5)?;
|
||||
let worktree = worktree_path.map(|p| super::WorktreeInfo {
|
||||
path: std::path::PathBuf::from(p),
|
||||
let worktree = worktree_path.map(|path| super::WorktreeInfo {
|
||||
path: PathBuf::from(path),
|
||||
branch: row.get::<_, String>(6).unwrap_or_default(),
|
||||
base_branch: row.get::<_, String>(7).unwrap_or_default(),
|
||||
});
|
||||
@@ -234,7 +284,7 @@ impl StateStore {
|
||||
let sessions = self.list_sessions()?;
|
||||
Ok(sessions
|
||||
.into_iter()
|
||||
.find(|s| s.id == id || s.id.starts_with(id)))
|
||||
.find(|session| session.id == id || session.id.starts_with(id)))
|
||||
}
|
||||
|
||||
pub fn send_message(&self, from: &str, to: &str, content: &str, msg_type: &str) -> Result<()> {
|
||||
@@ -245,15 +295,158 @@ impl StateStore {
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn append_output_line(
|
||||
&self,
|
||||
session_id: &str,
|
||||
stream: OutputStream,
|
||||
line: &str,
|
||||
) -> Result<()> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
self.conn.execute(
|
||||
"INSERT INTO session_output (session_id, stream, line, timestamp)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
rusqlite::params![session_id, stream.as_str(), line, now],
|
||||
)?;
|
||||
|
||||
self.conn.execute(
|
||||
"DELETE FROM session_output
|
||||
WHERE session_id = ?1
|
||||
AND id NOT IN (
|
||||
SELECT id
|
||||
FROM session_output
|
||||
WHERE session_id = ?1
|
||||
ORDER BY id DESC
|
||||
LIMIT ?2
|
||||
)",
|
||||
rusqlite::params![session_id, OUTPUT_BUFFER_LIMIT as i64],
|
||||
)?;
|
||||
|
||||
self.conn.execute(
|
||||
"UPDATE sessions SET updated_at = ?1 WHERE id = ?2",
|
||||
rusqlite::params![chrono::Utc::now().to_rfc3339(), session_id],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_output_lines(&self, session_id: &str, limit: usize) -> Result<Vec<OutputLine>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT stream, line
|
||||
FROM (
|
||||
SELECT id, stream, line
|
||||
FROM session_output
|
||||
WHERE session_id = ?1
|
||||
ORDER BY id DESC
|
||||
LIMIT ?2
|
||||
)
|
||||
ORDER BY id ASC",
|
||||
)?;
|
||||
|
||||
let lines = stmt
|
||||
.query_map(rusqlite::params![session_id, limit as i64], |row| {
|
||||
let stream: String = row.get(0)?;
|
||||
let text: String = row.get(1)?;
|
||||
|
||||
Ok(OutputLine {
|
||||
stream: OutputStream::from_db_value(&stream),
|
||||
text,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
pub fn insert_tool_log(
|
||||
&self,
|
||||
session_id: &str,
|
||||
tool_name: &str,
|
||||
input_summary: &str,
|
||||
output_summary: &str,
|
||||
duration_ms: u64,
|
||||
risk_score: f64,
|
||||
timestamp: &str,
|
||||
) -> Result<ToolLogEntry> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO tool_log (session_id, tool_name, input_summary, output_summary, duration_ms, risk_score, timestamp)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
rusqlite::params![
|
||||
session_id,
|
||||
tool_name,
|
||||
input_summary,
|
||||
output_summary,
|
||||
duration_ms,
|
||||
risk_score,
|
||||
timestamp,
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(ToolLogEntry {
|
||||
id: self.conn.last_insert_rowid(),
|
||||
session_id: session_id.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
input_summary: input_summary.to_string(),
|
||||
output_summary: output_summary.to_string(),
|
||||
duration_ms,
|
||||
risk_score,
|
||||
timestamp: timestamp.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_tool_logs(
|
||||
&self,
|
||||
session_id: &str,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
) -> Result<ToolLogPage> {
|
||||
let page = page.max(1);
|
||||
let offset = (page - 1) * page_size;
|
||||
|
||||
let total: u64 = self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM tool_log WHERE session_id = ?1",
|
||||
rusqlite::params![session_id],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, session_id, tool_name, input_summary, output_summary, duration_ms, risk_score, timestamp
|
||||
FROM tool_log
|
||||
WHERE session_id = ?1
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
LIMIT ?2 OFFSET ?3",
|
||||
)?;
|
||||
|
||||
let entries = stmt
|
||||
.query_map(rusqlite::params![session_id, page_size, offset], |row| {
|
||||
Ok(ToolLogEntry {
|
||||
id: row.get(0)?,
|
||||
session_id: row.get(1)?,
|
||||
tool_name: row.get(2)?,
|
||||
input_summary: row.get::<_, Option<String>>(3)?.unwrap_or_default(),
|
||||
output_summary: row.get::<_, Option<String>>(4)?.unwrap_or_default(),
|
||||
duration_ms: row.get::<_, Option<u64>>(5)?.unwrap_or_default(),
|
||||
risk_score: row.get::<_, Option<f64>>(6)?.unwrap_or_default(),
|
||||
timestamp: row.get(7)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(ToolLogPage {
|
||||
entries,
|
||||
page,
|
||||
page_size,
|
||||
total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::session::{Session, SessionMetrics, SessionState};
|
||||
use chrono::{Duration, Utc};
|
||||
use chrono::{Duration as ChronoDuration, Utc};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
struct TestDir {
|
||||
path: PathBuf,
|
||||
@@ -287,7 +480,7 @@ mod tests {
|
||||
state,
|
||||
pid: None,
|
||||
worktree: None,
|
||||
created_at: now - Duration::minutes(1),
|
||||
created_at: now - ChronoDuration::minutes(1),
|
||||
updated_at: now,
|
||||
metrics: SessionMetrics::default(),
|
||||
}
|
||||
@@ -347,4 +540,37 @@ mod tests {
|
||||
assert!(column_names.iter().any(|column| column == "pid"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_output_line_keeps_latest_buffer_window() -> Result<()> {
|
||||
let tempdir = TestDir::new("store-output")?;
|
||||
let db = StateStore::open(&tempdir.path().join("state.db"))?;
|
||||
let now = Utc::now();
|
||||
|
||||
db.insert_session(&Session {
|
||||
id: "session-1".to_string(),
|
||||
task: "buffer output".to_string(),
|
||||
agent_type: "claude".to_string(),
|
||||
state: SessionState::Running,
|
||||
pid: None,
|
||||
worktree: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
metrics: SessionMetrics::default(),
|
||||
})?;
|
||||
|
||||
for index in 0..(OUTPUT_BUFFER_LIMIT + 5) {
|
||||
db.append_output_line("session-1", OutputStream::Stdout, &format!("line-{index}"))?;
|
||||
}
|
||||
|
||||
let lines = db.get_output_lines("session-1", OUTPUT_BUFFER_LIMIT)?;
|
||||
let texts: Vec<_> = lines.iter().map(|line| line.text.as_str()).collect();
|
||||
|
||||
assert_eq!(lines.len(), OUTPUT_BUFFER_LIMIT);
|
||||
assert_eq!(texts.first().copied(), Some("line-5"));
|
||||
let expected_last_line = format!("line-{}", OUTPUT_BUFFER_LIMIT + 4);
|
||||
assert_eq!(texts.last().copied(), Some(expected_last_line.as_str()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
|
||||
(_, KeyCode::Char('q')) => break,
|
||||
(_, KeyCode::Tab) => dashboard.next_pane(),
|
||||
(KeyModifiers::SHIFT, KeyCode::BackTab) => dashboard.prev_pane(),
|
||||
(_, KeyCode::Char('+')) | (_, KeyCode::Char('=')) => {
|
||||
dashboard.increase_pane_size()
|
||||
}
|
||||
(_, KeyCode::Char('-')) => dashboard.decrease_pane_size(),
|
||||
(_, KeyCode::Char('j')) | (_, KeyCode::Down) => dashboard.scroll_down(),
|
||||
(_, KeyCode::Char('k')) | (_, KeyCode::Up) => dashboard.scroll_up(),
|
||||
(_, KeyCode::Char('n')) => dashboard.new_session(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,281 @@
|
||||
// Custom TUI widgets for ECC 2.0
|
||||
// TODO: Implement custom widgets:
|
||||
// - TokenMeter: visual token usage bar with budget threshold
|
||||
// - DiffViewer: side-by-side syntax-highlighted diff display
|
||||
// - ProgressTimeline: session timeline with tool call markers
|
||||
// - AgentTree: hierarchical view of parent/child agent sessions
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
text::{Line, Span},
|
||||
widgets::{Gauge, Paragraph, Widget},
|
||||
};
|
||||
|
||||
pub(crate) const WARNING_THRESHOLD: f64 = 0.8;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(crate) enum BudgetState {
|
||||
Unconfigured,
|
||||
Normal,
|
||||
Warning,
|
||||
OverBudget,
|
||||
}
|
||||
|
||||
impl BudgetState {
|
||||
pub(crate) const fn is_warning(self) -> bool {
|
||||
matches!(self, Self::Warning | Self::OverBudget)
|
||||
}
|
||||
|
||||
fn badge(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::Warning => Some("warning"),
|
||||
Self::OverBudget => Some("over budget"),
|
||||
Self::Unconfigured => Some("no budget"),
|
||||
Self::Normal => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn style(self) -> Style {
|
||||
let base = Style::default().fg(match self {
|
||||
Self::Unconfigured => Color::DarkGray,
|
||||
Self::Normal => Color::DarkGray,
|
||||
Self::Warning => Color::Yellow,
|
||||
Self::OverBudget => Color::Red,
|
||||
});
|
||||
|
||||
if self.is_warning() {
|
||||
base.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum MeterFormat {
|
||||
Tokens,
|
||||
Currency,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TokenMeter<'a> {
|
||||
title: &'a str,
|
||||
used: f64,
|
||||
budget: f64,
|
||||
format: MeterFormat,
|
||||
}
|
||||
|
||||
impl<'a> TokenMeter<'a> {
|
||||
pub(crate) fn tokens(title: &'a str, used: u64, budget: u64) -> Self {
|
||||
Self {
|
||||
title,
|
||||
used: used as f64,
|
||||
budget: budget as f64,
|
||||
format: MeterFormat::Tokens,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn currency(title: &'a str, used: f64, budget: f64) -> Self {
|
||||
Self {
|
||||
title,
|
||||
used,
|
||||
budget,
|
||||
format: MeterFormat::Currency,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn state(&self) -> BudgetState {
|
||||
budget_state(self.used, self.budget)
|
||||
}
|
||||
|
||||
fn ratio(&self) -> f64 {
|
||||
budget_ratio(self.used, self.budget)
|
||||
}
|
||||
|
||||
fn clamped_ratio(&self) -> f64 {
|
||||
self.ratio().clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
fn title_line(&self) -> Line<'static> {
|
||||
let mut spans = vec![Span::styled(
|
||||
self.title.to_string(),
|
||||
Style::default()
|
||||
.fg(Color::Gray)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)];
|
||||
|
||||
if let Some(badge) = self.state().badge() {
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::styled(format!("[{badge}]"), self.state().style()));
|
||||
}
|
||||
|
||||
Line::from(spans)
|
||||
}
|
||||
|
||||
fn display_label(&self) -> String {
|
||||
if self.budget <= 0.0 {
|
||||
return match self.format {
|
||||
MeterFormat::Tokens => format!("{} tok used | no budget", self.used_label()),
|
||||
MeterFormat::Currency => format!("{} spent | no budget", self.used_label()),
|
||||
};
|
||||
}
|
||||
|
||||
format!(
|
||||
"{} / {}{} ({}%)",
|
||||
self.used_label(),
|
||||
self.budget_label(),
|
||||
self.unit_suffix(),
|
||||
(self.ratio() * 100.0).round() as u64
|
||||
)
|
||||
}
|
||||
|
||||
fn used_label(&self) -> String {
|
||||
match self.format {
|
||||
MeterFormat::Tokens => format_token_count(self.used.max(0.0).round() as u64),
|
||||
MeterFormat::Currency => format_currency(self.used.max(0.0)),
|
||||
}
|
||||
}
|
||||
|
||||
fn budget_label(&self) -> String {
|
||||
match self.format {
|
||||
MeterFormat::Tokens => format_token_count(self.budget.max(0.0).round() as u64),
|
||||
MeterFormat::Currency => format_currency(self.budget.max(0.0)),
|
||||
}
|
||||
}
|
||||
|
||||
fn unit_suffix(&self) -> &'static str {
|
||||
match self.format {
|
||||
MeterFormat::Tokens => " tok",
|
||||
MeterFormat::Currency => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for TokenMeter<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut gauge_area = area;
|
||||
if area.height > 1 {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(1)])
|
||||
.split(area);
|
||||
Paragraph::new(self.title_line()).render(chunks[0], buf);
|
||||
gauge_area = chunks[1];
|
||||
}
|
||||
|
||||
Gauge::default()
|
||||
.ratio(self.clamped_ratio())
|
||||
.label(self.display_label())
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(gradient_color(self.ratio()))
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.style(Style::default().fg(Color::DarkGray))
|
||||
.use_unicode(true)
|
||||
.render(gauge_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn budget_ratio(used: f64, budget: f64) -> f64 {
|
||||
if budget <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
used / budget
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn budget_state(used: f64, budget: f64) -> BudgetState {
|
||||
if budget <= 0.0 {
|
||||
BudgetState::Unconfigured
|
||||
} else if used / budget >= 1.0 {
|
||||
BudgetState::OverBudget
|
||||
} else if used / budget >= WARNING_THRESHOLD {
|
||||
BudgetState::Warning
|
||||
} else {
|
||||
BudgetState::Normal
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn gradient_color(ratio: f64) -> Color {
|
||||
const GREEN: (u8, u8, u8) = (34, 197, 94);
|
||||
const YELLOW: (u8, u8, u8) = (234, 179, 8);
|
||||
const RED: (u8, u8, u8) = (239, 68, 68);
|
||||
|
||||
let clamped = ratio.clamp(0.0, 1.0);
|
||||
if clamped <= WARNING_THRESHOLD {
|
||||
interpolate_rgb(GREEN, YELLOW, clamped / WARNING_THRESHOLD)
|
||||
} else {
|
||||
interpolate_rgb(
|
||||
YELLOW,
|
||||
RED,
|
||||
(clamped - WARNING_THRESHOLD) / (1.0 - WARNING_THRESHOLD),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_currency(value: f64) -> String {
|
||||
format!("${value:.2}")
|
||||
}
|
||||
|
||||
pub(crate) fn format_token_count(value: u64) -> String {
|
||||
let digits = value.to_string();
|
||||
let mut formatted = String::with_capacity(digits.len() + digits.len() / 3);
|
||||
|
||||
for (index, ch) in digits.chars().rev().enumerate() {
|
||||
if index != 0 && index % 3 == 0 {
|
||||
formatted.push(',');
|
||||
}
|
||||
formatted.push(ch);
|
||||
}
|
||||
|
||||
formatted.chars().rev().collect()
|
||||
}
|
||||
|
||||
fn interpolate_rgb(from: (u8, u8, u8), to: (u8, u8, u8), ratio: f64) -> Color {
|
||||
let ratio = ratio.clamp(0.0, 1.0);
|
||||
let channel = |start: u8, end: u8| -> u8 {
|
||||
(f64::from(start) + (f64::from(end) - f64::from(start)) * ratio).round() as u8
|
||||
};
|
||||
|
||||
Color::Rgb(
|
||||
channel(from.0, to.0),
|
||||
channel(from.1, to.1),
|
||||
channel(from.2, to.2),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget};
|
||||
|
||||
use super::{gradient_color, BudgetState, TokenMeter};
|
||||
|
||||
#[test]
|
||||
fn warning_state_starts_at_eighty_percent() {
|
||||
let meter = TokenMeter::tokens("Token Budget", 80, 100);
|
||||
|
||||
assert_eq!(meter.state(), BudgetState::Warning);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradient_runs_from_green_to_yellow_to_red() {
|
||||
assert_eq!(gradient_color(0.0), Color::Rgb(34, 197, 94));
|
||||
assert_eq!(gradient_color(0.8), Color::Rgb(234, 179, 8));
|
||||
assert_eq!(gradient_color(1.0), Color::Rgb(239, 68, 68));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_meter_renders_compact_usage_label() {
|
||||
let meter = TokenMeter::tokens("Token Budget", 4_000, 10_000);
|
||||
let area = Rect::new(0, 0, 48, 2);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
meter.render(area, &mut buffer);
|
||||
|
||||
let rendered = buffer
|
||||
.content()
|
||||
.chunks(area.width as usize)
|
||||
.flat_map(|row| row.iter().map(|cell| cell.symbol()))
|
||||
.collect::<String>();
|
||||
|
||||
assert!(rendered.contains("4,000 / 10,000 tok (40%)"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ User request → Claude picks a tool → PreToolUse hook runs → Tool executes
|
||||
| **Dev server blocker** | `Bash` | Blocks `npm run dev` etc. outside tmux — ensures log access | 2 (blocks) |
|
||||
| **Tmux reminder** | `Bash` | Suggests tmux for long-running commands (npm test, cargo build, docker) | 0 (warns) |
|
||||
| **Git push reminder** | `Bash` | Reminds to review changes before `git push` | 0 (warns) |
|
||||
| **Pre-commit quality check** | `Bash` | Runs quality checks before `git commit`: lints staged files, validates commit message format when provided via `-m/--message`, detects console.log/debugger/secrets | 2 (blocks critical) / 0 (warns) |
|
||||
| **Doc file warning** | `Write` | Warns about non-standard `.md`/`.txt` files (allows README, CLAUDE, CONTRIBUTING, CHANGELOG, LICENSE, SKILL, docs/, skills/); cross-platform path handling | 0 (warns) |
|
||||
| **Strategic compact** | `Edit\|Write` | Suggests manual `/compact` at logical intervals (every ~50 tool calls) | 0 (warns) |
|
||||
| **InsAIts security monitor (opt-in)** | `Bash\|Write\|Edit\|MultiEdit` | Optional security scan for high-signal tool inputs. Disabled unless `ECC_ENABLE_INSAITS=1`. Blocks on critical findings, warns on non-critical, and writes audit log to `.insaits_audit_session.jsonl`. Requires `pip install insa-its`. [Details](../scripts/hooks/insaits-security-monitor.py) | 2 (blocks critical) / 0 (warns) |
|
||||
@@ -48,6 +49,7 @@ User request → Claude picks a tool → PreToolUse hook runs → Tool executes
|
||||
| **Session summary** | `Stop` | Persists session state when transcript path is available |
|
||||
| **Pattern extraction** | `Stop` | Evaluates session for extractable patterns (continuous learning) |
|
||||
| **Cost tracker** | `Stop` | Emits lightweight run-cost telemetry markers |
|
||||
| **Desktop notify** | `Stop` | Sends macOS desktop notification with task summary (standard+) |
|
||||
| **Session end marker** | `SessionEnd` | Lifecycle marker and cleanup log |
|
||||
|
||||
## Customizing Hooks
|
||||
|
||||
@@ -42,6 +42,16 @@
|
||||
],
|
||||
"description": "Reminder before git push to review changes"
|
||||
},
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"pre:bash:commit-quality\" \"scripts/hooks/pre-bash-commit-quality.js\" \"strict\""
|
||||
}
|
||||
],
|
||||
"description": "Pre-commit quality check: lint staged files, validate commit message format, detect console.log/debugger/secrets before committing"
|
||||
},
|
||||
{
|
||||
"matcher": "Write",
|
||||
"hooks": [
|
||||
@@ -136,7 +146,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash -lc 'input=$(cat); for root in \"${CLAUDE_PLUGIN_ROOT:-}\" \"$HOME/.claude/plugins/everything-claude-code\" \"$HOME/.claude/plugins/everything-claude-code@everything-claude-code\" \"$HOME/.claude/plugins/marketplace/everything-claude-code\"; do if [ -n \"$root\" ] && [ -f \"$root/scripts/hooks/run-with-flags.js\" ]; then printf \"%s\" \"$input\" | node \"$root/scripts/hooks/run-with-flags.js\" \"session:start\" \"scripts/hooks/session-start.js\" \"minimal,standard,strict\"; exit $?; fi; done; for parent in \"$HOME/.claude/plugins\" \"$HOME/.claude/plugins/marketplace\"; do if [ -d \"$parent\" ]; then candidate=$(find \"$parent\" -maxdepth 2 -type f -path \"*/scripts/hooks/run-with-flags.js\" 2>/dev/null | head -n 1); if [ -n \"$candidate\" ]; then root=$(dirname \"$(dirname \"$(dirname \"$candidate\")\")\"); printf \"%s\" \"$input\" | node \"$root/scripts/hooks/run-with-flags.js\" \"session:start\" \"scripts/hooks/session-start.js\" \"minimal,standard,strict\"; exit $?; fi; fi; done; echo \"[SessionStart] WARNING: could not resolve ECC plugin root; skipping session-start hook\" >&2; printf \"%s\" \"$input\"; exit 0'"
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const rel=path.join('scripts','hooks','run-with-flags.js');const hasRunnerRoot=candidate=>{const value=typeof candidate==='string'?candidate.trim():'';return value.length>0&&fs.existsSync(path.join(path.resolve(value),rel));};const root=(()=>{const envRoot=process.env.CLAUDE_PLUGIN_ROOT||'';if(hasRunnerRoot(envRoot))return path.resolve(envRoot.trim());const home=require('os').homedir();const claudeDir=path.join(home,'.claude');if(hasRunnerRoot(claudeDir))return claudeDir;for(const candidate of [path.join(claudeDir,'plugins','everything-claude-code'),path.join(claudeDir,'plugins','everything-claude-code@everything-claude-code'),path.join(claudeDir,'plugins','marketplace','everything-claude-code')]){if(hasRunnerRoot(candidate))return candidate;}try{const cacheBase=path.join(claudeDir,'plugins','cache','everything-claude-code');for(const org of fs.readdirSync(cacheBase,{withFileTypes:true})){if(!org.isDirectory())continue;for(const version of fs.readdirSync(path.join(cacheBase,org.name),{withFileTypes:true})){if(!version.isDirectory())continue;const candidate=path.join(cacheBase,org.name,version.name);if(hasRunnerRoot(candidate))return candidate;}}}catch{}return claudeDir;})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'session:start','scripts/hooks/session-start.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});const stdout=typeof result.stdout==='string'?result.stdout:'';if(stdout)process.stdout.write(stdout);else process.stdout.write(raw);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[SessionStart] ERROR: session-start hook failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);}process.stderr.write('[SessionStart] WARNING: could not resolve ECC plugin root; skipping session-start hook'+String.fromCharCode(10));process.stdout.write(raw);\""
|
||||
}
|
||||
],
|
||||
"description": "Load previous context and detect package manager on new session"
|
||||
@@ -249,7 +259,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"stop:check-console-log\" \"scripts/hooks/check-console-log.js\" \"standard,strict\""
|
||||
"command": "node -e \"const fs=require('fs');const p=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const root=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var l of [p.join(d,'plugins','everything-claude-code'),p.join(d,'plugins','everything-claude-code@everything-claude-code'),p.join(d,'plugins','marketplace','everything-claude-code')])if(f.existsSync(p.join(l,q)))return l;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}catch(x){}return d})();const script=p.join(root,'scripts','hooks','run-with-flags.js');if(!fs.existsSync(script)){process.stderr.write('[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);process.exit(0);}const result=spawnSync(process.execPath,[script,'stop:check-console-log','scripts/hooks/check-console-log.js','standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});if(typeof result.stdout==='string'&&result.stdout)process.stdout.write(result.stdout);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[Stop] ERROR: inline hook resolver failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);\""
|
||||
}
|
||||
],
|
||||
"description": "Check for console.log in modified files after each response"
|
||||
@@ -259,7 +269,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"stop:session-end\" \"scripts/hooks/session-end.js\" \"minimal,standard,strict\"",
|
||||
"command": "node -e \"const fs=require('fs');const p=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const root=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var l of [p.join(d,'plugins','everything-claude-code'),p.join(d,'plugins','everything-claude-code@everything-claude-code'),p.join(d,'plugins','marketplace','everything-claude-code')])if(f.existsSync(p.join(l,q)))return l;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}catch(x){}return d})();const script=p.join(root,'scripts','hooks','run-with-flags.js');if(!fs.existsSync(script)){process.stderr.write('[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);process.exit(0);}const result=spawnSync(process.execPath,[script,'stop:session-end','scripts/hooks/session-end.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});if(typeof result.stdout==='string'&&result.stdout)process.stdout.write(result.stdout);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[Stop] ERROR: inline hook resolver failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);\"",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
@@ -271,7 +281,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"stop:evaluate-session\" \"scripts/hooks/evaluate-session.js\" \"minimal,standard,strict\"",
|
||||
"command": "node -e \"const fs=require('fs');const p=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const root=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var l of [p.join(d,'plugins','everything-claude-code'),p.join(d,'plugins','everything-claude-code@everything-claude-code'),p.join(d,'plugins','marketplace','everything-claude-code')])if(f.existsSync(p.join(l,q)))return l;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}catch(x){}return d})();const script=p.join(root,'scripts','hooks','run-with-flags.js');if(!fs.existsSync(script)){process.stderr.write('[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);process.exit(0);}const result=spawnSync(process.execPath,[script,'stop:evaluate-session','scripts/hooks/evaluate-session.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});if(typeof result.stdout==='string'&&result.stdout)process.stdout.write(result.stdout);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[Stop] ERROR: inline hook resolver failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);\"",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
@@ -283,12 +293,24 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"stop:cost-tracker\" \"scripts/hooks/cost-tracker.js\" \"minimal,standard,strict\"",
|
||||
"command": "node -e \"const fs=require('fs');const p=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const root=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var l of [p.join(d,'plugins','everything-claude-code'),p.join(d,'plugins','everything-claude-code@everything-claude-code'),p.join(d,'plugins','marketplace','everything-claude-code')])if(f.existsSync(p.join(l,q)))return l;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}catch(x){}return d})();const script=p.join(root,'scripts','hooks','run-with-flags.js');if(!fs.existsSync(script)){process.stderr.write('[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);process.exit(0);}const result=spawnSync(process.execPath,[script,'stop:cost-tracker','scripts/hooks/cost-tracker.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});if(typeof result.stdout==='string'&&result.stdout)process.stdout.write(result.stdout);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[Stop] ERROR: inline hook resolver failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);\"",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
],
|
||||
"description": "Track token and cost metrics per session"
|
||||
},
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const p=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const root=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var l of [p.join(d,'plugins','everything-claude-code'),p.join(d,'plugins','everything-claude-code@everything-claude-code'),p.join(d,'plugins','marketplace','everything-claude-code')])if(f.existsSync(p.join(l,q)))return l;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}catch(x){}return d})();const script=p.join(root,'scripts','hooks','run-with-flags.js');if(!fs.existsSync(script)){process.stderr.write('[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);process.exit(0);}const result=spawnSync(process.execPath,[script,'stop:desktop-notify','scripts/hooks/desktop-notify.js','standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});if(typeof result.stdout==='string'&&result.stdout)process.stdout.write(result.stdout);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[Stop] ERROR: inline hook resolver failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);\"",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
],
|
||||
"description": "Send macOS desktop notification with task summary when Claude responds"
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
@@ -297,7 +319,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"session:end:marker\" \"scripts/hooks/session-end-marker.js\" \"minimal,standard,strict\"",
|
||||
"command": "node -e \"const fs=require('fs');const p=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const root=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var l of [p.join(d,'plugins','everything-claude-code'),p.join(d,'plugins','everything-claude-code@everything-claude-code'),p.join(d,'plugins','marketplace','everything-claude-code')])if(f.existsSync(p.join(l,q)))return l;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}catch(x){}return d})();const script=p.join(root,'scripts','hooks','run-with-flags.js');if(!fs.existsSync(script)){process.stderr.write('[SessionEnd] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);process.exit(0);}const result=spawnSync(process.execPath,[script,'session:end:marker','scripts/hooks/session-end-marker.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});if(typeof result.stdout==='string'&&result.stdout)process.stdout.write(result.stdout);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[SessionEnd] ERROR: inline hook resolver failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);\"",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
|
||||
15
install.ps1
15
install.ps1
@@ -34,5 +34,20 @@ while ($true) {
|
||||
$scriptDir = Split-Path -Parent $scriptPath
|
||||
$installerScript = Join-Path -Path (Join-Path -Path $scriptDir -ChildPath 'scripts') -ChildPath 'install-apply.js'
|
||||
|
||||
# Auto-install Node dependencies when running from a git clone
|
||||
$nodeModules = Join-Path -Path $scriptDir -ChildPath 'node_modules'
|
||||
if (-not (Test-Path -LiteralPath $nodeModules)) {
|
||||
Write-Host '[ECC] Installing dependencies...'
|
||||
Push-Location $scriptDir
|
||||
try {
|
||||
& npm install --no-audit --no-fund --loglevel=error
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "npm install failed with exit code $LASTEXITCODE"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
finally { Pop-Location }
|
||||
}
|
||||
|
||||
& node $installerScript @args
|
||||
exit $LASTEXITCODE
|
||||
|
||||
@@ -14,4 +14,10 @@ while [ -L "$SCRIPT_PATH" ]; do
|
||||
done
|
||||
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
||||
|
||||
# Auto-install Node dependencies when running from a git clone
|
||||
if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
|
||||
echo "[ECC] Installing dependencies..."
|
||||
(cd "$SCRIPT_DIR" && npm install --no-audit --no-fund --loglevel=error)
|
||||
fi
|
||||
|
||||
exec node "$SCRIPT_DIR/scripts/install-apply.js" "$@"
|
||||
|
||||
@@ -63,7 +63,8 @@
|
||||
"description": "Runtime hook configs and hook script helpers.",
|
||||
"paths": [
|
||||
"hooks",
|
||||
"scripts/hooks"
|
||||
"scripts/hooks",
|
||||
"scripts/lib"
|
||||
],
|
||||
"targets": [
|
||||
"claude",
|
||||
@@ -124,6 +125,7 @@
|
||||
"skills/kotlin-ktor-patterns",
|
||||
"skills/kotlin-patterns",
|
||||
"skills/kotlin-testing",
|
||||
"skills/laravel-plugin-discovery",
|
||||
"skills/laravel-patterns",
|
||||
"skills/laravel-tdd",
|
||||
"skills/laravel-verification",
|
||||
@@ -406,6 +408,7 @@
|
||||
"skills/ralphinho-rfc-pipeline",
|
||||
"skills/regex-vs-llm-structured-text",
|
||||
"skills/search-first",
|
||||
"skills/token-budget-advisor",
|
||||
"skills/team-builder"
|
||||
],
|
||||
"targets": [
|
||||
|
||||
@@ -133,6 +133,11 @@
|
||||
"args": ["-y", "token-optimizer-mcp"],
|
||||
"description": "Token optimization for 95%+ context reduction via content deduplication and compression"
|
||||
},
|
||||
"laraplugins": {
|
||||
"type": "http",
|
||||
"url": "https://laraplugins.io/mcp/plugins",
|
||||
"description": "Laravel plugin discovery — search packages by keyword, health score, Laravel/PHP version compatibility. Use with laravel-plugin-discovery skill."
|
||||
},
|
||||
"confluence": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "confluence-mcp-server"],
|
||||
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"ajv": "^8.18.0",
|
||||
"sql.js": "^1.14.1"
|
||||
},
|
||||
"bin": {
|
||||
@@ -19,7 +20,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"ajv": "^8.18.0",
|
||||
"c8": "^10.1.2",
|
||||
"eslint": "^9.39.2",
|
||||
"globals": "^17.1.0",
|
||||
@@ -449,7 +449,6 @@
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -1043,7 +1042,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
@@ -1064,7 +1062,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1491,7 +1488,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
@@ -2457,9 +2453,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2512,7 +2508,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
"AGENTS.md",
|
||||
".claude-plugin/plugin.json",
|
||||
".claude-plugin/README.md",
|
||||
".codex-plugin/plugin.json",
|
||||
".codex-plugin/README.md",
|
||||
".mcp.json",
|
||||
"install.sh",
|
||||
"install.ps1",
|
||||
"llms.txt"
|
||||
@@ -110,11 +113,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"ajv": "^8.18.0",
|
||||
"sql.js": "^1.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"ajv": "^8.18.0",
|
||||
"c8": "^10.1.2",
|
||||
"eslint": "^9.39.2",
|
||||
"globals": "^17.1.0",
|
||||
@@ -122,5 +125,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@4.9.2+sha512.1fc009bc09d13cfd0e19efa44cbfc2b9cf6ca61482725eb35bbc5e257e093ebf4130db6dfe15d604ff4b79efd8e1e8e99b25fa7d0a6197c9f9826358d4d65c3c"
|
||||
}
|
||||
|
||||
172
research/ecc2-codebase-analysis.md
Normal file
172
research/ecc2-codebase-analysis.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# ECC2 Codebase Research Report
|
||||
|
||||
**Date:** 2026-03-26
|
||||
**Subject:** `ecc-tui` v0.1.0 — Agentic IDE Control Plane
|
||||
**Total Lines:** 4,417 across 15 `.rs` files
|
||||
|
||||
## 1. Architecture Overview
|
||||
|
||||
ECC2 is a Rust TUI application that orchestrates AI coding agent sessions. It uses:
|
||||
- **ratatui 0.29** + **crossterm 0.28** for terminal UI
|
||||
- **rusqlite 0.32** (bundled) for local state persistence
|
||||
- **tokio 1** (full) for async runtime
|
||||
- **clap 4** (derive) for CLI
|
||||
|
||||
### Module Breakdown
|
||||
|
||||
| Module | Lines | Purpose |
|
||||
|--------|------:|---------|
|
||||
| `session/` | 1,974 | Session lifecycle, persistence, runtime, output |
|
||||
| `tui/` | 1,613 | Dashboard, app loop, custom widgets |
|
||||
| `observability/` | 409 | Tool call risk scoring and logging |
|
||||
| `config/` | 144 | Configuration (TOML file) |
|
||||
| `main.rs` | 142 | CLI entry point |
|
||||
| `worktree/` | 99 | Git worktree management |
|
||||
| `comms/` | 36 | Inter-agent messaging (send only) |
|
||||
|
||||
### Key Architectural Patterns
|
||||
|
||||
- **DbWriter thread** in `session/runtime.rs` — dedicated OS thread for SQLite writes from async context via `mpsc::unbounded_channel` with oneshot acknowledgements. Clean solution to the "SQLite from async" problem.
|
||||
- **Session state machine** with enforced transitions: `Pending → {Running, Failed, Stopped}`, `Running → {Idle, Completed, Failed, Stopped}`, etc.
|
||||
- **Ring buffer** for session output — `OUTPUT_BUFFER_LIMIT = 1000` lines per session with automatic eviction.
|
||||
- **Risk scoring** on tool calls — 4-axis analysis (base tool risk, file sensitivity, blast radius, irreversibility) producing composite 0.0–1.0 scores with suggested actions (Allow/Review/RequireConfirmation/Block).
|
||||
|
||||
## 2. Code Quality Metrics
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Total lines | 4,417 |
|
||||
| Test functions | 29 |
|
||||
| `unwrap()` calls | 3 |
|
||||
| `unsafe` blocks | 0 |
|
||||
| TODO/FIXME comments | 0 |
|
||||
| Max file size | 1,273 lines (`dashboard.rs`) |
|
||||
|
||||
**Assessment:** The codebase is clean. Only 3 `unwrap()` calls (2 in tests, 1 in config `default()`), zero `unsafe`, and all modules use proper `anyhow::Result` error propagation. The `dashboard.rs` file at 1,273 lines exceeds the repo's 800-line max-file guideline, but it is still manageable at the current scope.
|
||||
|
||||
## 3. Identified Gaps
|
||||
|
||||
### 3.1 Comms Module — Send Without Receive
|
||||
|
||||
`comms/mod.rs` (36 lines) has `send()` but no `receive()`, `poll()`, `inbox()`, or `subscribe()`. The `messages` table exists in SQLite, but nothing reads from it. The inter-agent messaging story is half-built.
|
||||
|
||||
**Impact:** Agents cannot coordinate. The `TaskHandoff`, `Query`, `Response`, and `Conflict` message types are defined but unusable.
|
||||
|
||||
### 3.2 New Session Dialog — Stub
|
||||
|
||||
`dashboard.rs:495` — `new_session()` logs `"New session dialog requested"` but does nothing. Users must use the CLI (`ecc start --task "..."`) to create sessions; the TUI dashboard cannot.
|
||||
|
||||
### 3.3 Single Agent Support
|
||||
|
||||
`session/manager.rs` — `agent_program()` only supports `"claude"`. The CLI accepts `--agent` but anything other than `"claude"` fails. No codex, opencode, or custom agent support.
|
||||
|
||||
### 3.4 Config — File-Only
|
||||
|
||||
`Config::load()` reads `~/.claude/ecc2.toml` only. The implementation lacks environment variable overrides (e.g., `ECC_DB_PATH`, `ECC_WORKTREE_ROOT`) and CLI flags for configuration.
|
||||
|
||||
### 3.5 Legacy Dependency Candidate: `git2`
|
||||
|
||||
`git2 = "0.20"` is still declared in `Cargo.toml`, but the `worktree` module shells out to the `git` CLI instead. That makes `git2` a strong removal candidate rather than an already-completed cleanup.
|
||||
|
||||
### 3.6 No Metrics Aggregation
|
||||
|
||||
`SessionMetrics` tracks tokens, cost, duration, tool_calls, files_changed per session. But there's no aggregate view: total cost across sessions, average duration, top tools by usage, etc. The Metrics pane in the dashboard shows per-session detail only.
|
||||
|
||||
### 3.7 Daemon — No Health Reporting
|
||||
|
||||
`session/daemon.rs` runs an infinite loop checking session timeouts. No health endpoint, no log rotation, no PID file, no signal handling for graceful shutdown. `Ctrl+C` during daemon mode kills the process uncleanly.
|
||||
|
||||
## 4. Test Coverage Analysis
|
||||
|
||||
34 test functions across 10 source modules:
|
||||
|
||||
| Module | Tests | Coverage Focus |
|
||||
|--------|------:|----------------|
|
||||
| `main.rs` | 1 | CLI parsing |
|
||||
| `config/mod.rs` | 5 | Defaults, deserialization, legacy fallback |
|
||||
| `observability/mod.rs` | 5 | Risk scoring, persistence, pagination |
|
||||
| `session/daemon.rs` | 2 | Crash recovery / liveness handling |
|
||||
| `session/manager.rs` | 4 | Session lifecycle, resume, stop, latest status |
|
||||
| `session/output.rs` | 2 | Ring buffer, broadcast |
|
||||
| `session/runtime.rs` | 1 | Output capture persistence/events |
|
||||
| `session/store.rs` | 3 | Buffer window, migration, state transitions |
|
||||
| `tui/dashboard.rs` | 8 | Rendering, selection, pane navigation, scrolling |
|
||||
| `tui/widgets.rs` | 3 | Token meter rendering and thresholds |
|
||||
|
||||
**Direct coverage gaps:**
|
||||
- `comms/mod.rs` — 0 tests
|
||||
- `worktree/mod.rs` — 0 tests
|
||||
|
||||
The core I/O-heavy paths are no longer completely untested: `manager.rs`, `runtime.rs`, and `daemon.rs` each have targeted tests. The remaining gap is breadth rather than total absence, especially around `comms/`, `worktree/`, and more adversarial process/worktree failure cases.
|
||||
|
||||
## 5. Security Observations
|
||||
|
||||
- **No secrets in code.** Config reads from TOML file, no hardcoded credentials.
|
||||
- **Process spawning** uses `tokio::process::Command` with explicit `Stdio::piped()` — no shell injection vectors.
|
||||
- **Risk scoring** is a strong feature — catches `rm -rf`, `git push --force origin main`, file access to `.env`/secrets.
|
||||
- **No input sanitization on session task strings.** The task string is passed directly to `claude --print`. If the task contains shell metacharacters, it could be exploited depending on how `Command` handles argument quoting. Currently safe (arguments are not shell-interpreted), but worth auditing.
|
||||
|
||||
## 6. Dependency Health
|
||||
|
||||
| Crate | Version | Latest | Notes |
|
||||
|-------|---------|--------|-------|
|
||||
| ratatui | 0.29 | **0.30.0** | Update available |
|
||||
| crossterm | 0.28 | **0.29.0** | Update available |
|
||||
| rusqlite | 0.32 | **0.39.0** | Update available |
|
||||
| tokio | 1 | **1.50.0** | Update available |
|
||||
| serde | 1 | **1.0.228** | Update available |
|
||||
| clap | 4 | **4.6.0** | Update available |
|
||||
| chrono | 0.4 | **0.4.44** | Update available |
|
||||
| uuid | 1 | **1.22.0** | Update available |
|
||||
|
||||
`git2` is still present in `Cargo.toml` even though the `worktree` module shells out to the `git` CLI. Several other dependencies are outdated; either remove `git2` or start using it before the next release.
|
||||
|
||||
## 7. Recommendations (Prioritized)
|
||||
|
||||
### P0 — Quick Wins
|
||||
|
||||
1. **Add environment variable support to `Config::load()`** — `ECC_DB_PATH`, `ECC_WORKTREE_ROOT`, `ECC_DEFAULT_AGENT`. Standard practice for CLI tools.
|
||||
|
||||
### P1 — Feature Completions
|
||||
|
||||
2. **Implement `comms::receive()` / `comms::poll()`** — read unread messages from the `messages` table, optionally with a `broadcast` channel for real-time delivery. Wire it into the dashboard.
|
||||
3. **Build the new-session dialog in the TUI** — modal form with task input, agent selector, worktree toggle. Should call `session::manager::create_session()`.
|
||||
4. **Add aggregate metrics** — total cost, average session duration, tool call frequency, cost per session. Show in the Metrics pane.
|
||||
|
||||
### P2 — Robustness
|
||||
|
||||
5. **Expand integration coverage for `manager.rs`, `runtime.rs`, and `daemon.rs`** — the repo now has baseline tests here, but it still needs failure-path coverage around process crashes, timeouts, and cleanup edge cases.
|
||||
6. **Add first-party tests for `worktree/mod.rs` and `comms/mod.rs`** — these are still uncovered and back important orchestration features.
|
||||
7. **Add daemon health reporting** — PID file, structured logging, graceful shutdown via signal handler.
|
||||
8. **Task string security audit** — The session task uses `claude --print` via `tokio::process::Command`. Verify arguments are never shell-interpreted. Checklist: confirm `Command` arg usage, threat-model metacharacter injection, input validation/escaping strategy, logging of raw inputs, and automated tests. Re-audit if invocation code changes.
|
||||
9. **Break up `dashboard.rs`** — extract SessionsPane, OutputPane, MetricsPane, LogPane into separate files under `tui/panes/`.
|
||||
|
||||
### P3 — Extensibility
|
||||
|
||||
10. **Multi-agent support** — make `agent_program()` pluggable. Add `codex`, `opencode`, `custom` agent types.
|
||||
11. **Config validation** — validate risk thresholds sum correctly, budget values are positive, paths exist.
|
||||
|
||||
## 8. Comparison with Ratatui 0.29 Best Practices
|
||||
|
||||
The codebase follows ratatui conventions well:
|
||||
- Uses `TableState` for stateful selection (correct pattern)
|
||||
- Custom `Widget` trait implementation for `TokenMeter` (idiomatic)
|
||||
- `tick()` method for periodic state sync (standard)
|
||||
- `broadcast::channel` for real-time output events (appropriate)
|
||||
|
||||
**Minor deviations:**
|
||||
- The `Dashboard` struct directly holds `StateStore` (SQLite connection). Ratatui best practice is to keep the state store behind an `Arc<Mutex<>>` to allow background updates. Currently the TUI owns the DB exclusively, which blocks adding a background metrics refresh task.
|
||||
- No `Clear` widget usage when rendering the help overlay — could cause rendering artifacts on some terminals.
|
||||
|
||||
## 9. Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| Dashboard file exceeds 1500 lines (projected) | High | Medium | At 1,273 lines currently (Section 2); extract panes into modules before it grows further |
|
||||
| SQLite lock contention | Low | High | DbWriter pattern already handles this |
|
||||
| No agent diversity | Medium | Medium | Pluggable agent support |
|
||||
| Task-string handling assumptions drift over time | Medium | Medium | Keep `Command` argument handling shell-free, document the threat model, and add regression tests for metacharacter-heavy task input |
|
||||
|
||||
---
|
||||
|
||||
**Bottom line:** ECC2 is a well-structured Rust project with clean error handling, good separation of concerns, and strong security features (risk scoring). The main gaps are incomplete features (comms, new-session dialog, single agent) rather than architectural problems. The codebase is ready for feature work on top of the solid foundation.
|
||||
124
rules/common/code-review.md
Normal file
124
rules/common/code-review.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Code Review Standards
|
||||
|
||||
## Purpose
|
||||
|
||||
Code review ensures quality, security, and maintainability before code is merged. This rule defines when and how to conduct code reviews.
|
||||
|
||||
## When to Review
|
||||
|
||||
**MANDATORY review triggers:**
|
||||
|
||||
- After writing or modifying code
|
||||
- Before any commit to shared branches
|
||||
- When security-sensitive code is changed (auth, payments, user data)
|
||||
- When architectural changes are made
|
||||
- Before merging pull requests
|
||||
|
||||
**Pre-Review Requirements:**
|
||||
|
||||
Before requesting review, ensure:
|
||||
|
||||
- All automated checks (CI/CD) are passing
|
||||
- Merge conflicts are resolved
|
||||
- Branch is up to date with target branch
|
||||
|
||||
## Review Checklist
|
||||
|
||||
Before marking code complete:
|
||||
|
||||
- [ ] Code is readable and well-named
|
||||
- [ ] Functions are focused (<50 lines)
|
||||
- [ ] Files are cohesive (<800 lines)
|
||||
- [ ] No deep nesting (>4 levels)
|
||||
- [ ] Errors are handled explicitly
|
||||
- [ ] No hardcoded secrets or credentials
|
||||
- [ ] No console.log or debug statements
|
||||
- [ ] Tests exist for new functionality
|
||||
- [ ] Test coverage meets 80% minimum
|
||||
|
||||
## Security Review Triggers
|
||||
|
||||
**STOP and use security-reviewer agent when:**
|
||||
|
||||
- Authentication or authorization code
|
||||
- User input handling
|
||||
- Database queries
|
||||
- File system operations
|
||||
- External API calls
|
||||
- Cryptographic operations
|
||||
- Payment or financial code
|
||||
|
||||
## Review Severity Levels
|
||||
|
||||
| Level | Meaning | Action |
|
||||
|-------|---------|--------|
|
||||
| CRITICAL | Security vulnerability or data loss risk | **BLOCK** - Must fix before merge |
|
||||
| HIGH | Bug or significant quality issue | **WARN** - Should fix before merge |
|
||||
| MEDIUM | Maintainability concern | **INFO** - Consider fixing |
|
||||
| LOW | Style or minor suggestion | **NOTE** - Optional |
|
||||
|
||||
## Agent Usage
|
||||
|
||||
Use these agents for code review:
|
||||
|
||||
| Agent | Purpose |
|
||||
|-------|---------|
|
||||
| **code-reviewer** | General code quality, patterns, best practices |
|
||||
| **security-reviewer** | Security vulnerabilities, OWASP Top 10 |
|
||||
| **typescript-reviewer** | TypeScript/JavaScript specific issues |
|
||||
| **python-reviewer** | Python specific issues |
|
||||
| **go-reviewer** | Go specific issues |
|
||||
| **rust-reviewer** | Rust specific issues |
|
||||
|
||||
## Review Workflow
|
||||
|
||||
```
|
||||
1. Run git diff to understand changes
|
||||
2. Check security checklist first
|
||||
3. Review code quality checklist
|
||||
4. Run relevant tests
|
||||
5. Verify coverage >= 80%
|
||||
6. Use appropriate agent for detailed review
|
||||
```
|
||||
|
||||
## Common Issues to Catch
|
||||
|
||||
### Security
|
||||
|
||||
- Hardcoded credentials (API keys, passwords, tokens)
|
||||
- SQL injection (string concatenation in queries)
|
||||
- XSS vulnerabilities (unescaped user input)
|
||||
- Path traversal (unsanitized file paths)
|
||||
- CSRF protection missing
|
||||
- Authentication bypasses
|
||||
|
||||
### Code Quality
|
||||
|
||||
- Large functions (>50 lines) - split into smaller
|
||||
- Large files (>800 lines) - extract modules
|
||||
- Deep nesting (>4 levels) - use early returns
|
||||
- Missing error handling - handle explicitly
|
||||
- Mutation patterns - prefer immutable operations
|
||||
- Missing tests - add test coverage
|
||||
|
||||
### Performance
|
||||
|
||||
- N+1 queries - use JOINs or batching
|
||||
- Missing pagination - add LIMIT to queries
|
||||
- Unbounded queries - add constraints
|
||||
- Missing caching - cache expensive operations
|
||||
|
||||
## Approval Criteria
|
||||
|
||||
- **Approve**: No CRITICAL or HIGH issues
|
||||
- **Warning**: Only HIGH issues (merge with caution)
|
||||
- **Block**: CRITICAL issues found
|
||||
|
||||
## Integration with Other Rules
|
||||
|
||||
This rule works with:
|
||||
|
||||
- [testing.md](testing.md) - Test coverage requirements
|
||||
- [security.md](security.md) - Security checklist
|
||||
- [git-workflow.md](git-workflow.md) - Commit standards
|
||||
- [agents.md](agents.md) - Agent delegation
|
||||
@@ -36,3 +36,9 @@ The Feature Implementation Workflow describes the development pipeline: research
|
||||
- Detailed commit messages
|
||||
- Follow conventional commits format
|
||||
- See [git-workflow.md](./git-workflow.md) for commit message format and PR process
|
||||
|
||||
5. **Pre-Review Checks**
|
||||
- Verify all automated checks (CI/CD) are passing
|
||||
- Resolve any merge conflicts
|
||||
- Ensure branch is up to date with target branch
|
||||
- Only request review after these checks pass
|
||||
|
||||
108
rules/zh/README.md
Normal file
108
rules/zh/README.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# 规则
|
||||
|
||||
## 结构
|
||||
|
||||
规则按**通用**层和**语言特定**目录组织:
|
||||
|
||||
```
|
||||
rules/
|
||||
├── common/ # 语言无关的原则(始终安装)
|
||||
│ ├── coding-style.md
|
||||
│ ├── git-workflow.md
|
||||
│ ├── testing.md
|
||||
│ ├── performance.md
|
||||
│ ├── patterns.md
|
||||
│ ├── hooks.md
|
||||
│ ├── agents.md
|
||||
│ ├── security.md
|
||||
│ ├── code-review.md
|
||||
│ └── development-workflow.md
|
||||
├── zh/ # 中文翻译版本
|
||||
│ ├── coding-style.md
|
||||
│ ├── git-workflow.md
|
||||
│ ├── testing.md
|
||||
│ ├── performance.md
|
||||
│ ├── patterns.md
|
||||
│ ├── hooks.md
|
||||
│ ├── agents.md
|
||||
│ ├── security.md
|
||||
│ ├── code-review.md
|
||||
│ └── development-workflow.md
|
||||
├── typescript/ # TypeScript/JavaScript 特定
|
||||
├── python/ # Python 特定
|
||||
├── golang/ # Go 特定
|
||||
├── swift/ # Swift 特定
|
||||
└── php/ # PHP 特定
|
||||
```
|
||||
|
||||
- **common/** 包含通用原则 — 无语言特定的代码示例。
|
||||
- **zh/** 包含 common 目录的中文翻译版本。
|
||||
- **语言目录** 扩展通用规则,包含框架特定的模式、工具和代码示例。每个文件引用其对应的通用版本。
|
||||
|
||||
## 安装
|
||||
|
||||
### 选项 1:安装脚本(推荐)
|
||||
|
||||
```bash
|
||||
# 安装通用 + 一个或多个语言特定的规则集
|
||||
./install.sh typescript
|
||||
./install.sh python
|
||||
./install.sh golang
|
||||
./install.sh swift
|
||||
./install.sh php
|
||||
|
||||
# 同时安装多种语言
|
||||
./install.sh typescript python
|
||||
```
|
||||
|
||||
### 选项 2:手动安装
|
||||
|
||||
> **重要提示:** 复制整个目录 — 不要使用 `/*` 展开。
|
||||
> 通用和语言特定目录包含同名文件。
|
||||
> 将它们展开到一个目录会导致语言特定文件覆盖通用规则,
|
||||
> 并破坏语言特定文件使用的 `../common/` 相对引用。
|
||||
|
||||
```bash
|
||||
# 创建目标目录
|
||||
mkdir -p ~/.claude/rules
|
||||
|
||||
# 安装通用规则(所有项目必需)
|
||||
cp -r rules/common ~/.claude/rules/common
|
||||
|
||||
# 安装中文翻译版本(可选)
|
||||
cp -r rules/zh ~/.claude/rules/zh
|
||||
|
||||
# 根据项目技术栈安装语言特定规则
|
||||
cp -r rules/typescript ~/.claude/rules/typescript
|
||||
cp -r rules/python ~/.claude/rules/python
|
||||
cp -r rules/golang ~/.claude/rules/golang
|
||||
cp -r rules/swift ~/.claude/rules/swift
|
||||
cp -r rules/php ~/.claude/rules/php
|
||||
```
|
||||
|
||||
## 规则 vs 技能
|
||||
|
||||
- **规则** 定义广泛适用的标准、约定和检查清单(如"80% 测试覆盖率"、"禁止硬编码密钥")。
|
||||
- **技能**(`skills/` 目录)为特定任务提供深入、可操作的参考材料(如 `python-patterns`、`golang-testing`)。
|
||||
|
||||
语言特定的规则文件在适当的地方引用相关技能。规则告诉你*做什么*;技能告诉你*怎么做*。
|
||||
|
||||
## 规则优先级
|
||||
|
||||
当语言特定规则与通用规则冲突时,**语言特定规则优先**(特定覆盖通用)。这遵循标准的分层配置模式(类似于 CSS 特异性或 `.gitignore` 优先级)。
|
||||
|
||||
- `rules/common/` 定义适用于所有项目的通用默认值。
|
||||
- `rules/golang/`、`rules/python/`、`rules/swift/`、`rules/php/`、`rules/typescript/` 等在语言习惯不同时覆盖这些默认值。
|
||||
- `rules/zh/` 是通用规则的中文翻译,与英文版本内容一致。
|
||||
|
||||
### 示例
|
||||
|
||||
`common/coding-style.md` 推荐不可变性作为默认原则。语言特定的 `golang/coding-style.md` 可以覆盖这一点:
|
||||
|
||||
> 惯用的 Go 使用指针接收器进行结构体变更 — 参见 [common/coding-style.md](../common/coding-style.md) 了解通用原则,但这里首选符合 Go 习惯的变更方式。
|
||||
|
||||
### 带覆盖说明的通用规则
|
||||
|
||||
`rules/common/` 中可能被语言特定文件覆盖的规则会被标记:
|
||||
|
||||
> **语言说明**:此规则可能会被语言特定规则覆盖;对于某些语言,该模式可能并不符合惯用写法。
|
||||
50
rules/zh/agents.md
Normal file
50
rules/zh/agents.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 代理编排
|
||||
|
||||
## 可用代理
|
||||
|
||||
位于 `~/.claude/agents/`:
|
||||
|
||||
| 代理 | 用途 | 何时使用 |
|
||||
|-------|---------|------------|
|
||||
| planner | 实现规划 | 复杂功能、重构 |
|
||||
| architect | 系统设计 | 架构决策 |
|
||||
| tdd-guide | 测试驱动开发 | 新功能、bug 修复 |
|
||||
| code-reviewer | 代码审查 | 编写代码后 |
|
||||
| security-reviewer | 安全分析 | 提交前 |
|
||||
| build-error-resolver | 修复构建错误 | 构建失败时 |
|
||||
| e2e-runner | E2E 测试 | 关键用户流程 |
|
||||
| refactor-cleaner | 死代码清理 | 代码维护 |
|
||||
| doc-updater | 文档 | 更新文档 |
|
||||
| rust-reviewer | Rust 代码审查 | Rust 项目 |
|
||||
|
||||
## 立即使用代理
|
||||
|
||||
无需用户提示:
|
||||
1. 复杂功能请求 - 使用 **planner** 代理
|
||||
2. 刚编写/修改的代码 - 使用 **code-reviewer** 代理
|
||||
3. Bug 修复或新功能 - 使用 **tdd-guide** 代理
|
||||
4. 架构决策 - 使用 **architect** 代理
|
||||
|
||||
## 并行任务执行
|
||||
|
||||
对独立操作始终使用并行 Task 执行:
|
||||
|
||||
```markdown
|
||||
# 好:并行执行
|
||||
同时启动 3 个代理:
|
||||
1. 代理 1:认证模块安全分析
|
||||
2. 代理 2:缓存系统性能审查
|
||||
3. 代理 3:工具类型检查
|
||||
|
||||
# 坏:不必要的顺序
|
||||
先代理 1,然后代理 2,然后代理 3
|
||||
```
|
||||
|
||||
## 多视角分析
|
||||
|
||||
对于复杂问题,使用分角色子代理:
|
||||
- 事实审查者
|
||||
- 高级工程师
|
||||
- 安全专家
|
||||
- 一致性审查者
|
||||
- 冗余检查者
|
||||
124
rules/zh/code-review.md
Normal file
124
rules/zh/code-review.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 代码审查标准
|
||||
|
||||
## 目的
|
||||
|
||||
代码审查确保代码合并前的质量、安全性和可维护性。此规则定义何时以及如何进行代码审查。
|
||||
|
||||
## 何时审查
|
||||
|
||||
**强制审查触发条件:**
|
||||
|
||||
- 编写或修改代码后
|
||||
- 提交到共享分支之前
|
||||
- 更改安全敏感代码时(认证、支付、用户数据)
|
||||
- 进行架构更改时
|
||||
- 合并 pull request 之前
|
||||
|
||||
**审查前要求:**
|
||||
|
||||
在请求审查之前,确保:
|
||||
|
||||
- 所有自动化检查(CI/CD)已通过
|
||||
- 合并冲突已解决
|
||||
- 分支已与目标分支同步
|
||||
|
||||
## 审查检查清单
|
||||
|
||||
在标记代码完成之前:
|
||||
|
||||
- [ ] 代码可读且命名良好
|
||||
- [ ] 函数聚焦(<50 行)
|
||||
- [ ] 文件内聚(<800 行)
|
||||
- [ ] 无深层嵌套(>4 层)
|
||||
- [ ] 错误显式处理
|
||||
- [ ] 无硬编码密钥或凭据
|
||||
- [ ] 无 console.log 或调试语句
|
||||
- [ ] 新功能有测试
|
||||
- [ ] 测试覆盖率满足 80% 最低要求
|
||||
|
||||
## 安全审查触发条件
|
||||
|
||||
**停止并使用 security-reviewer 代理当:**
|
||||
|
||||
- 认证或授权代码
|
||||
- 用户输入处理
|
||||
- 数据库查询
|
||||
- 文件系统操作
|
||||
- 外部 API 调用
|
||||
- 加密操作
|
||||
- 支付或金融代码
|
||||
|
||||
## 审查严重级别
|
||||
|
||||
| 级别 | 含义 | 行动 |
|
||||
|-------|---------|--------|
|
||||
| CRITICAL(关键) | 安全漏洞或数据丢失风险 | **阻止** - 合并前必须修复 |
|
||||
| HIGH(高) | Bug 或重大质量问题 | **警告** - 合并前应修复 |
|
||||
| MEDIUM(中) | 可维护性问题 | **信息** - 考虑修复 |
|
||||
| LOW(低) | 风格或次要建议 | **注意** - 可选 |
|
||||
|
||||
## 代理使用
|
||||
|
||||
使用这些代理进行代码审查:
|
||||
|
||||
| 代理 | 用途 |
|
||||
|-------|--------|
|
||||
| **code-reviewer** | 通用代码质量、模式、最佳实践 |
|
||||
| **security-reviewer** | 安全漏洞、OWASP Top 10 |
|
||||
| **typescript-reviewer** | TypeScript/JavaScript 特定问题 |
|
||||
| **python-reviewer** | Python 特定问题 |
|
||||
| **go-reviewer** | Go 特定问题 |
|
||||
| **rust-reviewer** | Rust 特定问题 |
|
||||
|
||||
## 审查工作流
|
||||
|
||||
```
|
||||
1. 运行 git diff 了解更改
|
||||
2. 先检查安全检查清单
|
||||
3. 审查代码质量检查清单
|
||||
4. 运行相关测试
|
||||
5. 验证覆盖率 >= 80%
|
||||
6. 使用适当的代理进行详细审查
|
||||
```
|
||||
|
||||
## 常见问题捕获
|
||||
|
||||
### 安全
|
||||
|
||||
- 硬编码凭据(API 密钥、密码、令牌)
|
||||
- SQL 注入(查询中的字符串拼接)
|
||||
- XSS 漏洞(未转义的用户输入)
|
||||
- 路径遍历(未净化的文件路径)
|
||||
- CSRF 保护缺失
|
||||
- 认证绕过
|
||||
|
||||
### 代码质量
|
||||
|
||||
- 大函数(>50 行)- 拆分为更小的
|
||||
- 大文件(>800 行)- 提取模块
|
||||
- 深层嵌套(>4 层)- 使用提前返回
|
||||
- 缺少错误处理 - 显式处理
|
||||
- 变更模式 - 优先使用不可变操作
|
||||
- 缺少测试 - 添加测试覆盖
|
||||
|
||||
### 性能
|
||||
|
||||
- N+1 查询 - 使用 JOIN 或批处理
|
||||
- 缺少分页 - 给查询添加 LIMIT
|
||||
- 无界查询 - 添加约束
|
||||
- 缺少缓存 - 缓存昂贵操作
|
||||
|
||||
## 批准标准
|
||||
|
||||
- **批准**:无关键或高优先级问题
|
||||
- **警告**:仅有高优先级问题(谨慎合并)
|
||||
- **阻止**:发现关键问题
|
||||
|
||||
## 与其他规则的集成
|
||||
|
||||
此规则与以下规则配合:
|
||||
|
||||
- [testing.md](testing.md) - 测试覆盖率要求
|
||||
- [security.md](security.md) - 安全检查清单
|
||||
- [git-workflow.md](git-workflow.md) - 提交标准
|
||||
- [agents.md](agents.md) - 代理委托
|
||||
48
rules/zh/coding-style.md
Normal file
48
rules/zh/coding-style.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 编码风格
|
||||
|
||||
## 不可变性(关键)
|
||||
|
||||
始终创建新对象,永远不要修改现有对象:
|
||||
|
||||
```
|
||||
// 伪代码
|
||||
错误: modify(original, field, value) → 就地修改 original
|
||||
正确: update(original, field, value) → 返回带有更改的新副本
|
||||
```
|
||||
|
||||
原理:不可变数据防止隐藏的副作用,使调试更容易,并启用安全的并发。
|
||||
|
||||
## 文件组织
|
||||
|
||||
多个小文件 > 少量大文件:
|
||||
- 高内聚,低耦合
|
||||
- 典型 200-400 行,最多 800 行
|
||||
- 从大模块中提取工具函数
|
||||
- 按功能/领域组织,而非按类型
|
||||
|
||||
## 错误处理
|
||||
|
||||
始终全面处理错误:
|
||||
- 在每一层显式处理错误
|
||||
- 在面向 UI 的代码中提供用户友好的错误消息
|
||||
- 在服务器端记录详细的错误上下文
|
||||
- 永远不要静默吞掉错误
|
||||
|
||||
## 输入验证
|
||||
|
||||
始终在系统边界验证:
|
||||
- 处理前验证所有用户输入
|
||||
- 在可用的情况下使用基于模式的验证
|
||||
- 快速失败并给出清晰的错误消息
|
||||
- 永远不要信任外部数据(API 响应、用户输入、文件内容)
|
||||
|
||||
## 代码质量检查清单
|
||||
|
||||
在标记工作完成前:
|
||||
- [ ] 代码可读且命名良好
|
||||
- [ ] 函数很小(<50 行)
|
||||
- [ ] 文件聚焦(<800 行)
|
||||
- [ ] 没有深层嵌套(>4 层)
|
||||
- [ ] 正确的错误处理
|
||||
- [ ] 没有硬编码值(使用常量或配置)
|
||||
- [ ] 没有变更(使用不可变模式)
|
||||
44
rules/zh/development-workflow.md
Normal file
44
rules/zh/development-workflow.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 开发工作流
|
||||
|
||||
> 此文件扩展 [common/git-workflow.md](./git-workflow.md),包含 git 操作之前的完整功能开发流程。
|
||||
|
||||
功能实现工作流描述了开发管道:研究、规划、TDD、代码审查,然后提交到 git。
|
||||
|
||||
## 功能实现工作流
|
||||
|
||||
0. **研究与重用** _(任何新实现前必需)_
|
||||
- **GitHub 代码搜索优先:** 在编写任何新代码之前,运行 `gh search repos` 和 `gh search code` 查找现有实现、模板和模式。
|
||||
- **库文档其次:** 使用 Context7 或主要供应商文档确认 API 行为、包使用和版本特定细节。
|
||||
- **仅当前两者不足时使用 Exa:** 在 GitHub 搜索和主要文档之后,使用 Exa 进行更广泛的网络研究或发现。
|
||||
- **检查包注册表:** 在编写工具代码之前搜索 npm、PyPI、crates.io 和其他注册表。首选久经考验的库而非手工编写的解决方案。
|
||||
- **搜索可适配的实现:** 寻找解决问题 80%+ 且可以分支、移植或包装的开源项目。
|
||||
- 当满足需求时,优先采用或移植经验证的方法而非从头编写新代码。
|
||||
|
||||
1. **先规划**
|
||||
- 使用 **planner** 代理创建实现计划
|
||||
- 编码前生成规划文档:PRD、架构、系统设计、技术文档、任务列表
|
||||
- 识别依赖和风险
|
||||
- 分解为阶段
|
||||
|
||||
2. **TDD 方法**
|
||||
- 使用 **tdd-guide** 代理
|
||||
- 先写测试(RED)
|
||||
- 实现以通过测试(GREEN)
|
||||
- 重构(IMPROVE)
|
||||
- 验证 80%+ 覆盖率
|
||||
|
||||
3. **代码审查**
|
||||
- 编写代码后立即使用 **code-reviewer** 代理
|
||||
- 解决关键和高优先级问题
|
||||
- 尽可能修复中优先级问题
|
||||
|
||||
4. **提交与推送**
|
||||
- 详细的提交消息
|
||||
- 遵循约定式提交格式
|
||||
- 参见 [git-workflow.md](./git-workflow.md) 了解提交消息格式和 PR 流程
|
||||
|
||||
5. **审查前检查**
|
||||
- 验证所有自动化检查(CI/CD)已通过
|
||||
- 解决任何合并冲突
|
||||
- 确保分支已与目标分支同步
|
||||
- 仅在这些检查通过后请求审查
|
||||
24
rules/zh/git-workflow.md
Normal file
24
rules/zh/git-workflow.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Git 工作流
|
||||
|
||||
## 提交消息格式
|
||||
```
|
||||
<类型>: <描述>
|
||||
|
||||
<可选正文>
|
||||
```
|
||||
|
||||
类型:feat, fix, refactor, docs, test, chore, perf, ci
|
||||
|
||||
注意:通过 ~/.claude/settings.json 全局禁用归属。
|
||||
|
||||
## Pull Request 工作流
|
||||
|
||||
创建 PR 时:
|
||||
1. 分析完整提交历史(不仅是最新提交)
|
||||
2. 使用 `git diff [base-branch]...HEAD` 查看所有更改
|
||||
3. 起草全面的 PR 摘要
|
||||
4. 包含带有 TODO 的测试计划
|
||||
5. 如果是新分支,使用 `-u` 标志推送
|
||||
|
||||
> 对于 git 操作之前的完整开发流程(规划、TDD、代码审查),
|
||||
> 参见 [development-workflow.md](./development-workflow.md)。
|
||||
30
rules/zh/hooks.md
Normal file
30
rules/zh/hooks.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# 钩子系统
|
||||
|
||||
## 钩子类型
|
||||
|
||||
- **PreToolUse**:工具执行前(验证、参数修改)
|
||||
- **PostToolUse**:工具执行后(自动格式化、检查)
|
||||
- **Stop**:会话结束时(最终验证)
|
||||
|
||||
## 自动接受权限
|
||||
|
||||
谨慎使用:
|
||||
- 为可信、定义明确的计划启用
|
||||
- 探索性工作时禁用
|
||||
- 永远不要使用 dangerously-skip-permissions 标志
|
||||
- 改为在 `~/.claude.json` 中配置 `allowedTools`
|
||||
|
||||
## TodoWrite 最佳实践
|
||||
|
||||
使用 TodoWrite 工具:
|
||||
- 跟踪多步骤任务的进度
|
||||
- 验证对指令的理解
|
||||
- 启用实时引导
|
||||
- 显示细粒度的实现步骤
|
||||
|
||||
待办列表揭示:
|
||||
- 顺序错误的步骤
|
||||
- 缺失的项目
|
||||
- 多余的不必要项目
|
||||
- 错误的粒度
|
||||
- 误解的需求
|
||||
31
rules/zh/patterns.md
Normal file
31
rules/zh/patterns.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 常用模式
|
||||
|
||||
## 骨架项目
|
||||
|
||||
实现新功能时:
|
||||
1. 搜索久经考验的骨架项目
|
||||
2. 使用并行代理评估选项:
|
||||
- 安全性评估
|
||||
- 可扩展性分析
|
||||
- 相关性评分
|
||||
- 实现规划
|
||||
3. 克隆最佳匹配作为基础
|
||||
4. 在经验证的结构内迭代
|
||||
|
||||
## 设计模式
|
||||
|
||||
### 仓储模式
|
||||
|
||||
将数据访问封装在一致的接口后面:
|
||||
- 定义标准操作:findAll、findById、create、update、delete
|
||||
- 具体实现处理存储细节(数据库、API、文件等)
|
||||
- 业务逻辑依赖抽象接口,而非存储机制
|
||||
- 便于轻松切换数据源,并简化使用模拟的测试
|
||||
|
||||
### API 响应格式
|
||||
|
||||
对所有 API 响应使用一致的信封:
|
||||
- 包含成功/状态指示器
|
||||
- 包含数据负载(错误时可为空)
|
||||
- 包含错误消息字段(成功时可为空)
|
||||
- 包含分页响应的元数据(total、page、limit)
|
||||
55
rules/zh/performance.md
Normal file
55
rules/zh/performance.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# 性能优化
|
||||
|
||||
## 模型选择策略
|
||||
|
||||
**Haiku 4.5**(Sonnet 90% 的能力,3 倍成本节省):
|
||||
- 频繁调用的轻量级代理
|
||||
- 结对编程和代码生成
|
||||
- 多代理系统中的工作者代理
|
||||
|
||||
**Sonnet 4.6**(最佳编码模型):
|
||||
- 主要开发工作
|
||||
- 编排多代理工作流
|
||||
- 复杂编码任务
|
||||
|
||||
**Opus 4.5**(最深度推理):
|
||||
- 复杂架构决策
|
||||
- 最大推理需求
|
||||
- 研究和分析任务
|
||||
|
||||
## 上下文窗口管理
|
||||
|
||||
避免在上下文窗口的最后 20% 进行以下操作:
|
||||
- 大规模重构
|
||||
- 跨多个文件的功能实现
|
||||
- 调试复杂交互
|
||||
|
||||
上下文敏感度较低的任务:
|
||||
- 单文件编辑
|
||||
- 独立工具创建
|
||||
- 文档更新
|
||||
- 简单 bug 修复
|
||||
|
||||
## 扩展思考 + 规划模式
|
||||
|
||||
扩展思考默认启用,为内部推理保留最多 31,999 个 token。
|
||||
|
||||
通过以下方式控制扩展思考:
|
||||
- **切换**:Option+T(macOS)/ Alt+T(Windows/Linux)
|
||||
- **配置**:在 `~/.claude/settings.json` 中设置 `alwaysThinkingEnabled`
|
||||
- **预算上限**:`export MAX_THINKING_TOKENS=10000`
|
||||
- **详细模式**:Ctrl+O 查看思考输出
|
||||
|
||||
对于需要深度推理的复杂任务:
|
||||
1. 确保扩展思考已启用(默认开启)
|
||||
2. 启用**规划模式**进行结构化方法
|
||||
3. 使用多轮审查进行彻底分析
|
||||
4. 使用分角色子代理获得多样化视角
|
||||
|
||||
## 构建排查
|
||||
|
||||
如果构建失败:
|
||||
1. 使用 **build-error-resolver** 代理
|
||||
2. 分析错误消息
|
||||
3. 增量修复
|
||||
4. 每次修复后验证
|
||||
29
rules/zh/security.md
Normal file
29
rules/zh/security.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 安全指南
|
||||
|
||||
## 强制安全检查
|
||||
|
||||
在任何提交之前:
|
||||
- [ ] 无硬编码密钥(API 密钥、密码、令牌)
|
||||
- [ ] 所有用户输入已验证
|
||||
- [ ] SQL 注入防护(参数化查询)
|
||||
- [ ] XSS 防护(净化 HTML)
|
||||
- [ ] CSRF 保护已启用
|
||||
- [ ] 认证/授权已验证
|
||||
- [ ] 所有端点启用速率限制
|
||||
- [ ] 错误消息不泄露敏感数据
|
||||
|
||||
## 密钥管理
|
||||
|
||||
- 永远不要在源代码中硬编码密钥
|
||||
- 始终使用环境变量或密钥管理器
|
||||
- 启动时验证所需的密钥是否存在
|
||||
- 轮换任何可能已暴露的密钥
|
||||
|
||||
## 安全响应协议
|
||||
|
||||
如果发现安全问题:
|
||||
1. 立即停止
|
||||
2. 使用 **security-reviewer** 代理
|
||||
3. 在继续之前修复关键问题
|
||||
4. 轮换任何已暴露的密钥
|
||||
5. 审查整个代码库中的类似问题
|
||||
29
rules/zh/testing.md
Normal file
29
rules/zh/testing.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 测试要求
|
||||
|
||||
## 最低测试覆盖率:80%
|
||||
|
||||
测试类型(全部必需):
|
||||
1. **单元测试** - 单个函数、工具、组件
|
||||
2. **集成测试** - API 端点、数据库操作
|
||||
3. **E2E 测试** - 关键用户流程(框架根据语言选择)
|
||||
|
||||
## 测试驱动开发
|
||||
|
||||
强制工作流:
|
||||
1. 先写测试(RED)
|
||||
2. 运行测试 - 应该失败
|
||||
3. 编写最小实现(GREEN)
|
||||
4. 运行测试 - 应该通过
|
||||
5. 重构(IMPROVE)
|
||||
6. 验证覆盖率(80%+)
|
||||
|
||||
## 测试失败排查
|
||||
|
||||
1. 使用 **tdd-guide** 代理
|
||||
2. 检查测试隔离
|
||||
3. 验证模拟是否正确
|
||||
4. 修复实现,而非测试(除非测试有误)
|
||||
|
||||
## 代理支持
|
||||
|
||||
- **tdd-guide** - 主动用于新功能,强制先写测试
|
||||
186
scripts/catalog.js
Normal file
186
scripts/catalog.js
Normal file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {
|
||||
getInstallComponent,
|
||||
listInstallComponents,
|
||||
listInstallProfiles,
|
||||
} = require('./lib/install-manifests');
|
||||
|
||||
const FAMILY_ALIASES = Object.freeze({
|
||||
baseline: 'baseline',
|
||||
baselines: 'baseline',
|
||||
language: 'language',
|
||||
languages: 'language',
|
||||
lang: 'language',
|
||||
framework: 'framework',
|
||||
frameworks: 'framework',
|
||||
capability: 'capability',
|
||||
capabilities: 'capability',
|
||||
agent: 'agent',
|
||||
agents: 'agent',
|
||||
skill: 'skill',
|
||||
skills: 'skill',
|
||||
});
|
||||
|
||||
function showHelp(exitCode = 0) {
|
||||
console.log(`
|
||||
Discover ECC install components and profiles
|
||||
|
||||
Usage:
|
||||
node scripts/catalog.js profiles [--json]
|
||||
node scripts/catalog.js components [--family <family>] [--target <target>] [--json]
|
||||
node scripts/catalog.js show <component-id> [--json]
|
||||
|
||||
Examples:
|
||||
node scripts/catalog.js profiles
|
||||
node scripts/catalog.js components --family language
|
||||
node scripts/catalog.js show framework:nextjs
|
||||
`);
|
||||
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
function normalizeFamily(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = String(value).trim().toLowerCase();
|
||||
return FAMILY_ALIASES[normalized] || normalized;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = argv.slice(2);
|
||||
const parsed = {
|
||||
command: null,
|
||||
componentId: null,
|
||||
family: null,
|
||||
target: null,
|
||||
json: false,
|
||||
help: false,
|
||||
};
|
||||
|
||||
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
||||
parsed.help = true;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
parsed.command = args[0];
|
||||
|
||||
for (let index = 1; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
parsed.help = true;
|
||||
} else if (arg === '--json') {
|
||||
parsed.json = true;
|
||||
} else if (arg === '--family') {
|
||||
if (!args[index + 1]) {
|
||||
throw new Error('Missing value for --family');
|
||||
}
|
||||
parsed.family = normalizeFamily(args[index + 1]);
|
||||
index += 1;
|
||||
} else if (arg === '--target') {
|
||||
if (!args[index + 1]) {
|
||||
throw new Error('Missing value for --target');
|
||||
}
|
||||
parsed.target = args[index + 1];
|
||||
index += 1;
|
||||
} else if (parsed.command === 'show' && !parsed.componentId) {
|
||||
parsed.componentId = arg;
|
||||
} else {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function printProfiles(profiles) {
|
||||
console.log('Install profiles:\n');
|
||||
for (const profile of profiles) {
|
||||
console.log(`- ${profile.id} (${profile.moduleCount} modules)`);
|
||||
console.log(` ${profile.description}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printComponents(components) {
|
||||
console.log('Install components:\n');
|
||||
for (const component of components) {
|
||||
console.log(`- ${component.id} [${component.family}]`);
|
||||
console.log(` targets=${component.targets.join(', ')} modules=${component.moduleIds.join(', ')}`);
|
||||
console.log(` ${component.description}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printComponent(component) {
|
||||
console.log(`Install component: ${component.id}\n`);
|
||||
console.log(`Family: ${component.family}`);
|
||||
console.log(`Targets: ${component.targets.join(', ')}`);
|
||||
console.log(`Modules: ${component.moduleIds.join(', ')}`);
|
||||
console.log(`Description: ${component.description}`);
|
||||
|
||||
if (component.modules.length > 0) {
|
||||
console.log('\nResolved modules:');
|
||||
for (const module of component.modules) {
|
||||
console.log(`- ${module.id} [${module.kind}]`);
|
||||
console.log(
|
||||
` targets=${module.targets.join(', ')} default=${module.defaultInstall} cost=${module.cost} stability=${module.stability}`
|
||||
);
|
||||
console.log(` ${module.description}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
try {
|
||||
const options = parseArgs(process.argv);
|
||||
|
||||
if (options.help) {
|
||||
showHelp(0);
|
||||
}
|
||||
|
||||
if (options.command === 'profiles') {
|
||||
const profiles = listInstallProfiles();
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({ profiles }, null, 2));
|
||||
} else {
|
||||
printProfiles(profiles);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.command === 'components') {
|
||||
const components = listInstallComponents({
|
||||
family: options.family,
|
||||
target: options.target,
|
||||
});
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({ components }, null, 2));
|
||||
} else {
|
||||
printComponents(components);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.command === 'show') {
|
||||
if (!options.componentId) {
|
||||
throw new Error('Catalog show requires an install component ID');
|
||||
}
|
||||
const component = getInstallComponent(options.componentId);
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(component, null, 2));
|
||||
} else {
|
||||
printComponent(component);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown catalog command: ${options.command}`);
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -16,6 +16,20 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Skip checks for branch deletion pushes (e.g., git push origin --delete <branch>).
|
||||
# The pre-push hook receives lines on stdin: <local ref> <local sha> <remote ref> <remote sha>.
|
||||
# For deletions, the local sha is the zero OID.
|
||||
is_delete_only=true
|
||||
while read -r _local_ref local_sha _remote_ref _remote_sha; do
|
||||
if [[ "$local_sha" != "0000000000000000000000000000000000000000" ]]; then
|
||||
is_delete_only=false
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$is_delete_only" == "true" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ran_any_check=0
|
||||
|
||||
log() {
|
||||
|
||||
37
scripts/codex/check-codex-global-state.sh
Normal file → Executable file
37
scripts/codex/check-codex-global-state.sh
Normal file → Executable file
@@ -11,7 +11,7 @@ CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
||||
CONFIG_FILE="$CODEX_HOME/config.toml"
|
||||
AGENTS_FILE="$CODEX_HOME/AGENTS.md"
|
||||
PROMPTS_DIR="$CODEX_HOME/prompts"
|
||||
SKILLS_DIR="$CODEX_HOME/skills"
|
||||
SKILLS_DIR="${AGENTS_HOME:-$HOME/.agents}/skills"
|
||||
HOOKS_DIR_EXPECT="${ECC_GLOBAL_HOOKS_DIR:-$CODEX_HOME/git-hooks}"
|
||||
|
||||
failures=0
|
||||
@@ -89,7 +89,13 @@ fi
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
check_config_pattern '^multi_agent\s*=\s*true' "multi_agent is enabled"
|
||||
check_config_absent '^\s*collab\s*=' "deprecated collab flag is absent"
|
||||
check_config_pattern '^persistent_instructions\s*=' "persistent_instructions is configured"
|
||||
# persistent_instructions is recommended but optional; warn instead of fail
|
||||
# so users who rely on AGENTS.md alone are not blocked (#967).
|
||||
if rg -n '^[[:space:]]*persistent_instructions\s*=' "$CONFIG_FILE" >/dev/null 2>&1; then
|
||||
ok "persistent_instructions is configured"
|
||||
else
|
||||
warn "persistent_instructions is not set (recommended but optional)"
|
||||
fi
|
||||
check_config_pattern '^\[profiles\.strict\]' "profiles.strict exists"
|
||||
check_config_pattern '^\[profiles\.yolo\]' "profiles.yolo exists"
|
||||
|
||||
@@ -97,7 +103,7 @@ if [[ -f "$CONFIG_FILE" ]]; then
|
||||
'mcp_servers.github' \
|
||||
'mcp_servers.memory' \
|
||||
'mcp_servers.sequential-thinking' \
|
||||
'mcp_servers.context7-mcp'
|
||||
'mcp_servers.context7'
|
||||
do
|
||||
if rg -n "^\[$section\]" "$CONFIG_FILE" >/dev/null 2>&1; then
|
||||
ok "MCP section [$section] exists"
|
||||
@@ -106,10 +112,25 @@ if [[ -f "$CONFIG_FILE" ]]; then
|
||||
fi
|
||||
done
|
||||
|
||||
has_context7_legacy=0
|
||||
has_context7_current=0
|
||||
|
||||
if rg -n '^\[mcp_servers\.context7\]' "$CONFIG_FILE" >/dev/null 2>&1; then
|
||||
warn "Duplicate [mcp_servers.context7] exists (context7-mcp is preferred)"
|
||||
has_context7_legacy=1
|
||||
fi
|
||||
|
||||
if rg -n '^\[mcp_servers\.context7-mcp\]' "$CONFIG_FILE" >/dev/null 2>&1; then
|
||||
has_context7_current=1
|
||||
fi
|
||||
|
||||
if [[ "$has_context7_legacy" -eq 1 || "$has_context7_current" -eq 1 ]]; then
|
||||
ok "MCP section [mcp_servers.context7] or [mcp_servers.context7-mcp] exists"
|
||||
else
|
||||
ok "No duplicate [mcp_servers.context7] section"
|
||||
fail "MCP section [mcp_servers.context7] or [mcp_servers.context7-mcp] missing"
|
||||
fi
|
||||
|
||||
if [[ "$has_context7_legacy" -eq 1 && "$has_context7_current" -eq 1 ]]; then
|
||||
warn "Both [mcp_servers.context7] and [mcp_servers.context7-mcp] exist; prefer one name"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -144,12 +165,12 @@ if [[ -d "$SKILLS_DIR" ]]; then
|
||||
done
|
||||
|
||||
if [[ "$missing_skills" -eq 0 ]]; then
|
||||
ok "All 16 ECC Codex skills are present"
|
||||
ok "All 16 ECC skills are present in $SKILLS_DIR"
|
||||
else
|
||||
fail "$missing_skills required skills are missing"
|
||||
warn "$missing_skills ECC skills missing from $SKILLS_DIR (install via ECC installer or npx skills)"
|
||||
fi
|
||||
else
|
||||
fail "Skills directory missing ($SKILLS_DIR)"
|
||||
warn "Skills directory missing ($SKILLS_DIR) — install via ECC installer or npx skills"
|
||||
fi
|
||||
|
||||
if [[ -f "$PROMPTS_DIR/ecc-prompts-manifest.txt" ]]; then
|
||||
|
||||
20
scripts/codex/install-global-git-hooks.sh
Normal file → Executable file
20
scripts/codex/install-global-git-hooks.sh
Normal file → Executable file
@@ -24,9 +24,11 @@ log() {
|
||||
|
||||
run_or_echo() {
|
||||
if [[ "$MODE" == "dry-run" ]]; then
|
||||
printf '[dry-run] %s\n' "$*"
|
||||
printf '[dry-run]'
|
||||
printf ' %q' "$@"
|
||||
printf '\n'
|
||||
else
|
||||
eval "$*"
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -41,14 +43,14 @@ log "Global hooks destination: $DEST_DIR"
|
||||
|
||||
if [[ -d "$DEST_DIR" ]]; then
|
||||
log "Backing up existing hooks directory to $BACKUP_DIR"
|
||||
run_or_echo "mkdir -p \"$BACKUP_DIR\""
|
||||
run_or_echo "cp -R \"$DEST_DIR\" \"$BACKUP_DIR/hooks\""
|
||||
run_or_echo mkdir -p "$BACKUP_DIR"
|
||||
run_or_echo cp -R "$DEST_DIR" "$BACKUP_DIR/hooks"
|
||||
fi
|
||||
|
||||
run_or_echo "mkdir -p \"$DEST_DIR\""
|
||||
run_or_echo "cp \"$SOURCE_DIR/pre-commit\" \"$DEST_DIR/pre-commit\""
|
||||
run_or_echo "cp \"$SOURCE_DIR/pre-push\" \"$DEST_DIR/pre-push\""
|
||||
run_or_echo "chmod +x \"$DEST_DIR/pre-commit\" \"$DEST_DIR/pre-push\""
|
||||
run_or_echo mkdir -p "$DEST_DIR"
|
||||
run_or_echo cp "$SOURCE_DIR/pre-commit" "$DEST_DIR/pre-commit"
|
||||
run_or_echo cp "$SOURCE_DIR/pre-push" "$DEST_DIR/pre-push"
|
||||
run_or_echo chmod +x "$DEST_DIR/pre-commit" "$DEST_DIR/pre-push"
|
||||
|
||||
if [[ "$MODE" == "apply" ]]; then
|
||||
prev_hooks_path="$(git config --global core.hooksPath || true)"
|
||||
@@ -56,7 +58,7 @@ if [[ "$MODE" == "apply" ]]; then
|
||||
log "Previous global hooksPath: $prev_hooks_path"
|
||||
fi
|
||||
fi
|
||||
run_or_echo "git config --global core.hooksPath \"$DEST_DIR\""
|
||||
run_or_echo git config --global core.hooksPath "$DEST_DIR"
|
||||
|
||||
log "Installed ECC global git hooks."
|
||||
log "Disable per repo by creating .ecc-hooks-disable in project root."
|
||||
|
||||
@@ -83,20 +83,23 @@ function dlxServer(name, pkg, extraFields, extraToml) {
|
||||
}
|
||||
|
||||
/** Each entry: key = section name under mcp_servers, value = { toml, fields } */
|
||||
const DEFAULT_MCP_STARTUP_TIMEOUT_SEC = 30;
|
||||
const DEFAULT_MCP_STARTUP_TIMEOUT_TOML = `startup_timeout_sec = ${DEFAULT_MCP_STARTUP_TIMEOUT_SEC}`;
|
||||
|
||||
const ECC_SERVERS = {
|
||||
supabase: dlxServer('supabase', '@supabase/mcp-server-supabase@latest', { startup_timeout_sec: 20.0, tool_timeout_sec: 120.0 }, 'startup_timeout_sec = 20.0\ntool_timeout_sec = 120.0'),
|
||||
playwright: dlxServer('playwright', '@playwright/mcp@latest'),
|
||||
'context7-mcp': dlxServer('context7-mcp', '@upstash/context7-mcp'),
|
||||
playwright: dlxServer('playwright', '@playwright/mcp@latest', { startup_timeout_sec: DEFAULT_MCP_STARTUP_TIMEOUT_SEC }, DEFAULT_MCP_STARTUP_TIMEOUT_TOML),
|
||||
context7: dlxServer('context7', '@upstash/context7-mcp@latest', { startup_timeout_sec: DEFAULT_MCP_STARTUP_TIMEOUT_SEC }, DEFAULT_MCP_STARTUP_TIMEOUT_TOML),
|
||||
exa: {
|
||||
fields: { url: 'https://mcp.exa.ai/mcp' },
|
||||
toml: `[mcp_servers.exa]\nurl = "https://mcp.exa.ai/mcp"`
|
||||
},
|
||||
github: {
|
||||
fields: { command: 'bash', args: ['-lc', GH_BOOTSTRAP] },
|
||||
toml: `[mcp_servers.github]\ncommand = "bash"\nargs = ["-lc", ${JSON.stringify(GH_BOOTSTRAP)}]`
|
||||
fields: { command: 'bash', args: ['-lc', GH_BOOTSTRAP], startup_timeout_sec: DEFAULT_MCP_STARTUP_TIMEOUT_SEC },
|
||||
toml: `[mcp_servers.github]\ncommand = "bash"\nargs = ["-lc", ${JSON.stringify(GH_BOOTSTRAP)}]\n${DEFAULT_MCP_STARTUP_TIMEOUT_TOML}`
|
||||
},
|
||||
memory: dlxServer('memory', '@modelcontextprotocol/server-memory'),
|
||||
'sequential-thinking': dlxServer('sequential-thinking', '@modelcontextprotocol/server-sequential-thinking')
|
||||
memory: dlxServer('memory', '@modelcontextprotocol/server-memory', { startup_timeout_sec: DEFAULT_MCP_STARTUP_TIMEOUT_SEC }, DEFAULT_MCP_STARTUP_TIMEOUT_TOML),
|
||||
'sequential-thinking': dlxServer('sequential-thinking', '@modelcontextprotocol/server-sequential-thinking', { startup_timeout_sec: DEFAULT_MCP_STARTUP_TIMEOUT_SEC }, DEFAULT_MCP_STARTUP_TIMEOUT_TOML)
|
||||
};
|
||||
|
||||
// Append --features arg for supabase after dlxServer builds the base
|
||||
@@ -104,9 +107,9 @@ ECC_SERVERS.supabase.fields.args.push('--features=account,docs,database,debuggin
|
||||
ECC_SERVERS.supabase.toml = ECC_SERVERS.supabase.toml.replace(/^(args = \[.*)\]$/m, '$1, "--features=account,docs,database,debugging,development,functions,storage,branching"]');
|
||||
|
||||
// Legacy section names that should be treated as an existing ECC server.
|
||||
// e.g. old configs shipped [mcp_servers.context7] instead of [mcp_servers.context7-mcp].
|
||||
// e.g. older configs shipped [mcp_servers.context7-mcp] instead of [mcp_servers.context7].
|
||||
const LEGACY_ALIASES = {
|
||||
'context7-mcp': ['context7']
|
||||
context7: ['context7-mcp']
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -254,6 +257,10 @@ function main() {
|
||||
if (resolvedLabel !== name) {
|
||||
raw = removeServerFromText(raw, name, existing);
|
||||
}
|
||||
if (legacyName && hasCanonical) {
|
||||
toRemoveLog.push(`mcp_servers.${legacyName}`);
|
||||
raw = removeServerFromText(raw, legacyName, existing);
|
||||
}
|
||||
toAppend.push(spec.toml);
|
||||
} else {
|
||||
// Add-only mode: skip, but warn about drift
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user