28 Commits

Author SHA1 Message Date
Affaan Mustafa
47f508ec21 Revert "Add Kiro IDE support (.kiro/) (#548)"
This reverts commit ce828c1c3c.
2026-03-20 01:58:19 -07:00
Himanshu Sharma
ce828c1c3c Add Kiro IDE support (.kiro/) (#548)
Co-authored-by: Sungmin Hong <hsungmin@amazon.com>
2026-03-20 01:50:35 -07:00
Ofek Gabay
c8f631b046 feat: add block-no-verify hook for Claude Code and Cursor (#649)
Adds npx block-no-verify@1.1.2 as a PreToolUse Bash hook in hooks/hooks.json
and a beforeShellExecution hook in .cursor/hooks.json to prevent AI agents
from bypassing git hooks via the hook-bypass flag.

This closes the last enforcement gap in the ECC security stack — the bypass
flag silently skips pre-commit, commit-msg, and pre-push hooks.

Closes #648

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 01:50:31 -07:00
Affaan Mustafa
8511d84042 feat(skills): add rules-distill skill (rebased #561) (#678)
* feat(skills): add rules-distill — extract cross-cutting principles from skills into rules

Applies the skill-stocktake pattern to rules maintenance:
scan skills → extract shared principles → propose rule changes.

Key design decisions:
- Deterministic collection (scan scripts) + LLM judgment (cross-read & verdict)
- 6 verdict types: Append, Revise, New Section, New File, Already Covered, Too Specific
- Anti-abstraction safeguard: 2+ skills evidence, actionable behavior test, violation risk
- Rules full text passed to LLM (no grep pre-filter) for accurate matching
- Never modifies rules automatically — always requires user approval

* fix(skills): address review feedback for rules-distill

Fixes raised by CodeRabbit, Greptile, and cubic:

- Add Prerequisites section documenting skill-stocktake dependency
- Add fallback command when skill-stocktake is not installed
- Fix shell quoting: add IFS= and -r to while-read loops
- Replace hardcoded paths with env var placeholders ($CLAUDE_RULES_DIR, $SKILL_STOCKTAKE_DIR)
- Add json language identifier to code blocks
- Add "How It Works" parent heading for Phase 1/2/3
- Add "Example" section with end-to-end run output
- Add revision.reason/before/after fields to output schema for Revise verdict
- Document timestamp format (date -u +%Y-%m-%dT%H:%M:%SZ)
- Document candidate-id format (kebab-case from principle)
- Use concrete examples in results.json schema

* fix(skills): remove skill-stocktake dependency, add self-contained scripts

Address P1 review feedback:
- Add scan-skills.sh and scan-rules.sh directly in rules-distill/scripts/
  (no external dependency on skill-stocktake)
- Remove Prerequisites section (no longer needed)
- Add cross-batch merge step to prevent 2+ skills requirement
  from being silently broken across batch boundaries
- Fix nested triple-backtick fences (use quadruple backticks)
- Remove head -100 cap (silent truncation)
- Rename "When to Activate" → "When to Use" (ECC standard)
- Remove unnecessary env var placeholders (SKILL.md is a prompt, not a script)

* fix: update skill/command counts in README.md and AGENTS.md

rules-distill added 1 skill + 1 command:
- skills: 108 → 109
- commands: 57 → 58

Updates all count references to pass CI catalog validation.

* fix(skills): address Servitor review feedback for rules-distill

1. Rename SKILL_STOCKTAKE_* env vars to RULES_DISTILL_* for consistency
2. Remove unnecessary observation counting (use_7d/use_30d) from scan-skills.sh
3. Fix header comment: scan.sh → scan-skills.sh
4. Use jq for JSON construction in scan-rules.sh to properly escape
   headings containing special characters (", \)

* fix(skills): address CodeRabbit review — portability and scan scope

1. scan-rules.sh: use jq for error JSON output (proper escaping)
2. scan-rules.sh: replace GNU-only sort -z with portable sort (BSD compat)
3. scan-rules.sh: fix pipefail crash on files without H2 headings
4. scan-skills.sh: scan only SKILL.md files (skip learned/*.md and
   auxiliary docs that lack frontmatter)
5. scan-skills.sh: add portable get_mtime helper (GNU stat/date
   fallback to BSD stat/date)

* fix: sync catalog counts with filesystem (27 agents, 114 skills, 59 commands)

---------

Co-authored-by: Tatsuya Shimomoto <shimo4228@gmail.com>
2026-03-20 01:44:55 -07:00
dependabot[bot]
8a57894394 chore(deps-dev): bump flatted (#675)
Bumps the npm_and_yarn group with 1 update in the / directory: [flatted](https://github.com/WebReflection/flatted).


Updates `flatted` from 3.3.3 to 3.4.2
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 01:42:19 -07:00
Affaan Mustafa
68484da2fc fix: auto-detect ECC root from plugin cache when CLAUDE_PLUGIN_ROOT is unset (#547) (#691)
When ECC is installed as a Claude Code plugin via the marketplace,
scripts live in the plugin cache (~/.claude/plugins/cache/...) but
commands fallback to ~/.claude/ which doesn't have the scripts.

Add resolve-ecc-root.js with a 3-step fallback chain:
  1. CLAUDE_PLUGIN_ROOT env var (existing)
  2. Standard install at ~/.claude/ (existing)
  3. NEW: auto-scan the plugin cache directory

Update sessions.md and skill-health.md commands to use the new
inline resolver. Includes 15 tests covering all fallback paths
including env var priority, standard install, cache discovery,
and the compact INLINE_RESOLVE used in command .md files.
2026-03-20 01:38:15 -07:00
Affaan Mustafa
0b0b66c02f feat: agent compression, inspection logic, governance hooks (#491, #485, #482) (#688)
Implements three roadmap features:

- Agent description compression (#491): New `agent-compress` module with
  catalog/summary/full compression modes and lazy-loading. Reduces ~26k
  token agent descriptions to ~2-3k catalog entries for context efficiency.

- Inspection logic (#485): New `inspection` module that detects recurring
  failure patterns in skill_runs. Groups by skill + normalized failure
  reason, generates structured reports with suggested remediation actions.
  Configurable threshold (default: 3 failures).

- Governance event capture hook (#482): PreToolUse/PostToolUse hook that
  detects secrets, policy violations, approval-required commands, and
  elevated privilege usage. Gated behind ECC_GOVERNANCE_CAPTURE=1 flag.
  Writes to governance_events table via JSON-line stderr output.

59 new tests (16 + 16 + 27), all passing.
2026-03-20 01:38:13 -07:00
Affaan Mustafa
28de7cc420 fix: strip ANSI escape codes from session persistence hooks (#642) (#684)
Windows terminals emit control sequences (cursor movement, screen
clearing) that leaked into session.tmp files and were injected
verbatim into Claude's context on the next session start.

Add a comprehensive stripAnsi() to utils.js that handles CSI, OSC,
charset selection, and bare ESC sequences. Apply it in session-end.js
(when extracting user messages from the transcript) and in
session-start.js (safety net before injecting session content).
2026-03-20 01:38:11 -07:00
Affaan Mustafa
9a478ad676 feat(rules): add Rust language rules (rebased #660) (#686)
* feat(rules): add Rust coding style, hooks, and patterns rules

Add language-specific rules for Rust extending the common rule set:
- coding-style.md: rustfmt, clippy, ownership idioms, error handling,
  iterator patterns, module organization, visibility
- hooks.md: PostToolUse hooks for rustfmt, clippy, cargo check
- patterns.md: trait-based repository, newtype, enum state machines,
  builder, sealed traits, API response envelope

Rules reference existing rust-patterns skill for deep content.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* feat(rules): add Rust testing and security rules

Add remaining Rust language-specific rules:
- testing.md: cargo test, rstest parameterized tests, mockall mocking
  with mock! macro, tokio async tests, cargo-llvm-cov coverage
- security.md: secrets via env vars, parameterized SQL with sqlx,
  parse-don't-validate input validation, unsafe code audit requirements,
  cargo-audit dependency scanning, proper HTTP error status codes

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* fix(rules): address review feedback on Rust rules

Fixes from Copilot, Greptile, Cubic, and CodeRabbit reviews:
- Add missing imports: use std::borrow::Cow, use anyhow::Context
- Use anyhow::Result<T> consistently (patterns.md, security.md)
- Change sqlx placeholder from ? to $1 (Postgres is most common)
- Remove Cargo.lock from hooks.md paths (auto-generated file)
- Fix tokio::test to show attribute form #[tokio::test]
- Fix mockall mock! name collision, wrap in #[cfg(test)] mod tests
- Fix --test target to match file layout (api_test, not integration)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* fix: update catalog counts in README.md and AGENTS.md

Update documented counts to match actual repository state after rebase:
- Skills: 109 → 113 (new skills merged to main)
- Commands: 57 → 58 (new command merged to main)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

---------

Co-authored-by: Chris Yau <chris@diveanddev.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
2026-03-20 01:19:42 -07:00
Affaan Mustafa
52e949a85b fix: sync catalog counts with filesystem (27 agents, 113 skills, 58 commands) (#693) 2026-03-20 01:19:36 -07:00
Affaan Mustafa
07f6156d8a feat: implement --with/--without selective install flags (#679)
Add agent: and skill: component families to the install component
catalog, enabling fine-grained selective install via CLI flags:

  ecc install --profile developer --with lang:typescript --without capability:orchestration
  ecc install --with lang:python --with agent:security-reviewer

Changes:
- Add agent: family (9 entries) and skill: family (10 entries) to
  manifests/install-components.json for granular component addressing
- Update install-components.schema.json to accept agent: and skill:
  family prefixes
- Register agent and skill family prefixes in COMPONENT_FAMILY_PREFIXES
  (scripts/lib/install-manifests.js)
- Add 41 comprehensive tests covering CLI parsing, request normalization,
  component catalog validation, plan resolution, target filtering,
  error handling, and end-to-end install with --with/--without flags

Closes #470
2026-03-20 00:43:32 -07:00
Affaan Mustafa
29277ac273 chore: prepare v1.9.0 release (#666)
- Bump version to 1.9.0 in package.json, package-lock.json, .opencode/package.json
- Add v1.9.0 changelog with 212 commits covering selective install architecture,
  6 new agents, 15+ new skills, session/state infrastructure, observer fixes,
  12 language ecosystems, and community contributions
- Update README with v1.9.0 release notes and complete agents tree (27 agents)
- Add pytorch-build-resolver to AGENTS.md agent table
- Update documentation counts to 27 agents, 109 skills, 57 commands
- Update version references in zh-CN README
- All 1421 tests passing, catalog counts verified
2026-03-20 00:29:20 -07:00
Affaan Mustafa
6836e9875d fix: resolve Windows CI failures and markdown lint (#667)
- Replace node -e with temp file execution in validator tests to avoid
  Windows shebang parsing failures (node -e cannot handle scripts that
  originally contained #!/usr/bin/env node shebangs)
- Remove duplicate blank line in skills/rust-patterns/SKILL.md (MD012)
2026-03-20 00:29:17 -07:00
vazidmansuri005
cfb3370df8 docs: add Antigravity setup and usage guide (#552)
* docs: add Antigravity setup and usage guide

Addresses #462 — users were confused about Antigravity skills setup.

Adds a comprehensive guide covering:
- Install mapping (ECC → .agent/ directory)
- Directory structure after install
- openai.yaml agent config format
- Managing installs (list, doctor, uninstall)
- Cross-target comparison table
- Troubleshooting common issues
- How to contribute skills with Antigravity support

Also links the guide from the README FAQ section.

* fix: address review feedback on Antigravity guide

- Remove spurious skills/ row from install mapping table, add note
  clarifying .agents/skills/ is static repo layout not installer-mapped
- Fix repair section: doctor.js diagnoses, repair.js restores
- Fix .agents/ → .agent/ path typo in custom skills section
- Clarify 3-step workflow for adding Antigravity skills
- Fix antigravity-project → antigravity in comparison table
- Fix "flatten" → "flattened" grammar in README
- Clarify openai.yaml full nested path structure

* fix: clarify .agents/ vs .agent/ naming and fix Cursor comparison

- Explain that .agents/ (with 's') is ECC source, .agent/ (no 's')
  is Antigravity runtime — installer copies between them
- Fix Cursor Agents/Skills column: Cursor has no explicit agents/skills
  mapping (only rules), changed from 'skills/' to 'N/A'

* fix: correct installer behavior claims and command style

- Fix .agents/ vs .agent/ note: clarify that only rules, commands, and
  agents (no dot) are explicitly mapped by the installer. The dot-prefixed
  .agents/ directory falls through to default scaffold, not a direct copy.
- Fix contributor workflow: remove false auto-deploy claim for openai.yaml.
  Clarify .agents/ is static repo layout, not installer-deployed.
- Fix uninstall command: use direct script call (node scripts/uninstall.js)
  for consistency with doctor.js, repair.js, list-installed.js.

* fix: add missing agents/ step to contributor workflow

Contributors must add an agent definition at agents/ (no dot) for the
installer to deploy it to .agent/skills/ at runtime. Without this step,
skills only exist in the static .agents/ layout and are never deployed.

---------

Co-authored-by: vazidmansuri005 <vazidmansuri005@users.noreply.github.com>
2026-03-20 00:21:37 -07:00
vazidmansuri005
d697f2ebac feat(skills): add architecture-decision-records skill (#555)
* feat(skills): add architecture-decision-records skill

Adds a skill that captures architectural decisions made during coding
sessions as structured ADR documents (Michael Nygard format).

Features:
- Auto-detects decision moments from conversation signals
- Records context, alternatives considered with pros/cons, and consequences
- Maintains numbered ADR files in docs/adr/ with an index
- Supports ADR lifecycle (proposed → accepted → deprecated/superseded)
- Categorizes decisions worth recording vs trivial ones to skip
- Integrates with planner, code-reviewer, and codebase-onboarding skills

Includes Antigravity support via .agents/skills/ and openai.yaml.

* fix: address review feedback on ADR skill

- Add missing "why did we choose X?" read-ADR trigger to .agents/ copy
- Add canonical-reference link to .agents/ SKILL.md pointing to full version
- Remove integration reference to non-existent codebase-onboarding skill

* fix: add initialization step and sync .agents/ trigger

- Add Step 1 to workflow: initialize docs/adr/ directory, README.md
  index, and template.md on first use when directory doesn't exist
- Add "API design" to .agents/ alternatives trigger to match canonical
  version

* fix: address ADR workflow gaps and implicit signal safety

- Init step: seed README.md with index table header so Step 8 can
  append rows correctly on first ADR
- Add read-path workflow: graceful handling when docs/adr/ is empty
  or absent ("No ADRs found, would you like to start?")
- Implicit signals: add "do not auto-create without user confirmation"
  guard, tighten triggers to require conclusion/rationale not just
  discussion, remove overly broad "testing strategy" trigger

* fix: require user confirmation before creating files

- Canonical SKILL.md: init step now asks user before creating docs/adr/
- .agents/ condensed version: add confirmation gate for implicit signals
  and explicit consent step before any file writes

* fix: require user approval before writing ADR file, add refusal path

* fix: remove .agents/ duplicate, keep canonical in skills/

---------

Co-authored-by: vazidmansuri005 <vazidmansuri005@users.noreply.github.com>
2026-03-20 00:20:25 -07:00
vazidmansuri005
0efd6ed914 feat(commands): add /context-budget optimizer command (#554)
* feat(commands): add /context-budget optimizer command

Adds a command that audits context window token consumption across
agents, skills, rules, MCP servers, and CLAUDE.md files.

Detects bloated agent descriptions, redundant components, MCP
over-subscription, and CLAUDE.md bloat. Produces a prioritized
report with specific token savings per optimization.

Directly relevant to #434 (agent descriptions too verbose, ~26k
tokens causing performance warnings).

* fix: address review feedback on context-budget command

- Add $ARGUMENTS to enable --verbose flag passthrough
- Fix MCP token estimate: 45 tools × ~500 tokens = ~22,500 (was ~2,200)
- Fix heavy agents example: all 3 now exceed 200-line threshold
- Fix description threshold: warning at >30 words, fail at >50 words
- Add Step 4 instructions (was empty)
- Fix audit cadence: "quarterly" → "regularly" + "monthly" consistently
- Fix Output Format heading level under Step 4
- Replace "Antigravity" with generic "harness versions"
- Recalculate total overhead to match corrected MCP numbers

* fix: correct MCP tool count and savings percentage in sample output

- Fix MCP tool count: table now shows 87 tools matching the issues
  section (was 45 in table vs 87 in issues)
- Fix savings percentage: 5,100 / 66,400 = 7.7% (was 20.6%)
- Recalculate total overhead and effective context to match

* fix: correct sample output arithmetic

- Fix total overhead: 66,400 → 66,100 to match component table sum
  (12,400 + 6,200 + 2,800 + 43,500 + 1,200 = 66,100)
- Fix MCP savings: ~1,500 → ~27,500 tokens (55 tools × 500 tokens/tool)
  to match the per-tool formula defined in Step 1
- Reorder optimizations by savings (MCP removal is now #1)
- Fix total savings and percentage (31,100 / 66,100 = 47.0%)

* fix: distinguish always-on vs on-demand agent overhead

Agent descriptions are always loaded into Task tool routing context,
but the full agent body is only loaded when invoked. The audit now
measures both: description-only tokens as always-on overhead and
full-file tokens as worst-case overhead. This resolves the
contradiction between Step 1 (counting full files) and Tip 1 (saying
only descriptions are loaded per session).

* fix: simplify agent accounting and resolve inconsistencies

- Revert to single agent overhead metric (full file tokens) — simpler
  and matches what the report actually displays
- Add back 200-line threshold for heavy agents in Step 1
- Fix heavy agents action to match issue type (split/trim, not
  description-only)
- Remove .agents/skills/ scan path (doesn't exist in ECC repo)
- Consolidate description threshold to single 30-word check

* fix: add model assumption and verbose mode activation

- Step 4: assume 200K context window by default (Claude has no way to
  introspect its model at runtime)
- Step 4: add explicit instruction to check $ARGUMENTS for --verbose
  flag and include additional output when present

* fix: handle .agents/skills/ duplicates in skill scan

Skills scan now checks .agents/skills/ for Codex harness copies and
skips identical duplicates to avoid double-counting overhead.

* fix: add savings estimate to heavy agents action for consistency

* feat(skills): add context-budget backing skill, slim command to delegator

* fix: use structurally detectable classification criteria instead of session frequency

---------

Co-authored-by: vazidmansuri005 <vazidmansuri005@users.noreply.github.com>
2026-03-20 00:20:23 -07:00
vazidmansuri005
72c013d212 feat(skills): add codebase-onboarding skill (#553)
* feat(skills): add codebase-onboarding skill

Adds a skill that systematically analyzes an unfamiliar codebase and
produces two artifacts: a structured onboarding guide and a starter
CLAUDE.md tailored to the project's conventions.

Four-phase workflow:
1. Reconnaissance — parallel detection of manifests, frameworks, entry
   points, directory structure, tooling, and test setup
2. Architecture mapping — tech stack, patterns, key directories, request
   lifecycle tracing
3. Convention detection — naming, error handling, async patterns, git
   workflow from recent history
4. Artifact generation — scannable onboarding guide + project-specific
   CLAUDE.md

Includes Antigravity support via .agents/skills/ and openai.yaml.

* fix: address review feedback on codebase-onboarding skill

- Rename headings to match skill format: When to Activate → When to Use,
  Onboarding Workflow → How It Works
- Add Examples section with 3 usage scenarios
- Mark Phase 4 Next.js paths as example with HTML comments
- Fix CLAUDE.md generation to read/enhance existing file first
- Replace abbreviated .agents/ SKILL.md with full copy per repo convention

* fix: add example marker to Common Tasks template section

Adds <!-- Example for a Node.js project --> comment to Common Tasks,
matching the markers already on Key Entry Points and Where to Look.
Syncs .agents/ copy.

* fix: add missing example markers and shorten default_prompt

- Add example comment to Tech Stack table in Phase 4 template
- Add example comment to Key Directories block in Phase 2
- Shorten openai.yaml default_prompt to match repo convention (~60 chars)
- Sync .agents/ SKILL.md copy

* fix: add empty-repo fallback and remove hardcoded output path

- Phase 3: add fallback for repos with no git history
- Example 1: remove hardcoded docs/ path assumption, output to
  conversation or project root instead
- Sync .agents/ copy

* fix: remove .agents/ duplicate, keep canonical in skills/

* fix: clarify Example 1 output destination

* fix: add shallow-clone fallback to git conventions detection

---------

Co-authored-by: vazidmansuri005 <vazidmansuri005@users.noreply.github.com>
2026-03-20 00:20:20 -07:00
Joaquin Hui
27234fb790 feat(skills): add agent-eval for head-to-head coding agent comparison (#540)
* feat(skills): add agent-eval for head-to-head coding agent comparison

* fix(skills): address PR #540 review feedback for agent-eval skill

- Remove duplicate "When to Use" section (kept "When to Activate")
- Add Installation section with pip install instructions
- Change origin from "community" to "ECC" per repo convention
- Add commit field to YAML task example for reproducibility
- Fix pass@k mislabeling to "pass rate across repeated runs"
- Soften worktree isolation language to "reproducibility isolation"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Pin agent-eval install to specific commit hash

Address PR review feedback: pin the VCS install to commit
6d062a2 to avoid supply-chain risk from unpinned external deps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Joaquin Hui Gomez <joaquinhui1995@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 00:20:18 -07:00
Affaan Mustafa
a6bd90713d Merge pull request #664 from ymdvsymd/fix/observer-sandbox-access-661
fix(clv2): add --allowedTools to observer Haiku invocation (#661)
2026-03-20 00:16:42 -07:00
Affaan Mustafa
9c58d1edb5 Merge pull request #665 from ymdvsymd/fix/worktree-project-id-mismatch
fix(clv2): use -e instead of -d for .git check in detect-project.sh
2026-03-20 00:16:34 -07:00
to.watanabe
04f8675624 fix(clv2): use -e instead of -d for .git check in detect-project.sh
In git worktrees, .git is a file (not a directory) containing a gitdir
pointer. The -d test fails for worktree checkouts, causing project
detection to fall through to the "global" fallback. Changing to -e
(exists) handles both regular repos and worktrees correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 16:02:10 +09:00
to.watanabe
f37c92cfe2 fix(clv2): add --allowedTools to observer Haiku invocation (#661)
The observer's Haiku subprocess cannot access files outside the project
sandbox (/tmp/ for observations, ~/.claude/homunculus/ for instincts).
Adding --allowedTools "Read,Write" grants the necessary file access
while keeping the subprocess constrained by --max-turns and timeout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 16:00:17 +09:00
Affaan Mustafa
fec871e1cb fix: update catalog counts and resolve lint error
- Update agent count 26→27 in README.md (quick-start + comparison table) and AGENTS.md (summary + project structure)
- Update skill count 108→109 in README.md (quick-start + comparison table) and AGENTS.md (summary)
- Rename unused variable provenance → _provenance in tests/lib/skill-dashboard.test.js
2026-03-19 22:47:46 -07:00
Muhammad Idrees
1b21e082fa feat(skills): add pytorch-patterns skill (#550)
Adds pytorch-patterns skill covering model architecture, training loops, data loading, and GPU optimization patterns.
2026-03-19 20:49:34 -07:00
Muhammad Idrees
beb11f8d02 feat(agents): add pytorch-build-resolver agent (#549)
Adds pytorch-build-resolver agent for PyTorch runtime/CUDA error resolution, following established agent format.
2026-03-19 20:49:32 -07:00
teee32
90c3486e03 feat(agents): add typescript-reviewer agent (#647)
Adds typescript-reviewer agent following the established agent format, covering type safety, async correctness, security, and React/Next.js patterns.
2026-03-19 20:49:23 -07:00
Chris Yau
9ceb699e9a feat(rules): add Java language rules (#645)
Adds Java language rules (coding-style, hooks, patterns, security, testing) following the established language rule conventions.
2026-03-19 20:49:21 -07:00
Chris Yau
a9edf54d2f fix(observe): allow sdk-ts entrypoint in observation hook (#614)
Clean surgical fix allowing sdk-ts entrypoint in observe hook for Agent SDK sessions. Has APPROVED review.
2026-03-19 20:49:15 -07:00
59 changed files with 6517 additions and 177 deletions

View File

@@ -15,6 +15,11 @@
}
],
"beforeShellExecution": [
{
"command": "npx block-no-verify@1.1.2",
"event": "beforeShellExecution",
"description": "Block git hook-bypass flag to protect pre-commit, commit-msg, and pre-push hooks from being skipped"
},
{
"command": "node .cursor/hooks/before-shell-execution.js",
"event": "beforeShellExecution",

View File

@@ -1,6 +1,6 @@
{
"name": "ecc-universal",
"version": "1.8.0",
"version": "1.9.0",
"description": "Everything Claude Code (ECC) plugin for OpenCode - agents, commands, hooks, and skills",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,6 +1,8 @@
# Everything Claude Code (ECC) — Agent Instructions
This is a **production-ready AI coding plugin** providing 25 specialized agents, 108 skills, 57 commands, and automated hook workflows for software development.
This is a **production-ready AI coding plugin** providing 27 specialized agents, 114 skills, 59 commands, and automated hook workflows for software development.
**Version:** 1.9.0
## Core Principles
@@ -23,6 +25,9 @@ This is a **production-ready AI coding plugin** providing 25 specialized agents,
| e2e-runner | End-to-end Playwright testing | Critical user flows |
| refactor-cleaner | Dead code cleanup | Code maintenance |
| doc-updater | Documentation and codemaps | Updating docs |
| docs-lookup | Documentation and API reference research | Library/API documentation questions |
| cpp-reviewer | C++ code review | C++ projects |
| cpp-build-resolver | C++ build errors | C++ build failures |
| go-reviewer | Go code review | Go projects |
| go-build-resolver | Go build errors | Go build failures |
| kotlin-reviewer | Kotlin code review | Kotlin/Android/KMP projects |
@@ -36,6 +41,8 @@ This is a **production-ready AI coding plugin** providing 25 specialized agents,
| harness-optimizer | Harness config tuning | Reliability, cost, throughput |
| rust-reviewer | Rust code review | Rust projects |
| rust-build-resolver | Rust build errors | Rust build failures |
| pytorch-build-resolver | PyTorch runtime/CUDA/training errors | PyTorch build/training failures |
| typescript-reviewer | TypeScript/JavaScript code review | TypeScript/JavaScript projects |
## Agent Orchestration
@@ -134,9 +141,9 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
## Project Structure
```
agents/ — 25 specialized subagents
skills/ — 102 workflow skills and domain knowledge
commands/ — 57 slash commands
agents/ — 27 specialized subagents
skills/ — 114 workflow skills and domain knowledge
commands/ — 59 slash commands
hooks/ — Trigger-based automations
rules/ — Always-follow guidelines (common + per-language)
scripts/ — Cross-platform Node.js utilities

View File

@@ -1,5 +1,108 @@
# Changelog
## 1.9.0 - 2026-03-20
### Highlights
- Selective install architecture with manifest-driven pipeline and SQLite state store.
- Language coverage expanded to 10+ ecosystems with 6 new agents and language-specific rules.
- Observer reliability hardened with memory throttling, sandbox fixes, and 5-layer loop guard.
- Self-improving skills foundation with skill evolution and session adapters.
### New Agents
- `typescript-reviewer` — TypeScript/JavaScript code review specialist (#647)
- `pytorch-build-resolver` — PyTorch runtime, CUDA, and training error resolution (#549)
- `java-build-resolver` — Maven/Gradle build error resolution (#538)
- `java-reviewer` — Java and Spring Boot code review (#528)
- `kotlin-reviewer` — Kotlin/Android/KMP code review (#309)
- `kotlin-build-resolver` — Kotlin/Gradle build errors (#309)
- `rust-reviewer` — Rust code review (#523)
- `rust-build-resolver` — Rust build error resolution (#523)
- `docs-lookup` — Documentation and API reference research (#529)
### New Skills
- `pytorch-patterns` — PyTorch deep learning workflows (#550)
- `documentation-lookup` — API reference and library doc research (#529)
- `bun-runtime` — Bun runtime patterns (#529)
- `nextjs-turbopack` — Next.js Turbopack workflows (#529)
- `mcp-server-patterns` — MCP server design patterns (#531)
- `data-scraper-agent` — AI-powered public data collection (#503)
- `team-builder` — Team composition skill (#501)
- `ai-regression-testing` — AI regression test workflows (#433)
- `claude-devfleet` — Multi-agent orchestration (#505)
- `blueprint` — Multi-session construction planning
- `everything-claude-code` — Self-referential ECC skill (#335)
- `prompt-optimizer` — Prompt optimization skill (#418)
- 8 Evos operational domain skills (#290)
- 3 Laravel skills (#420)
- VideoDB skills (#301)
### New Commands
- `/docs` — Documentation lookup (#530)
- `/aside` — Side conversation (#407)
- `/prompt-optimize` — Prompt optimization (#418)
- `/resume-session`, `/save-session` — Session management
- `learn-eval` improvements with checklist-based holistic verdict
### New Rules
- Java language rules (#645)
- PHP rule pack (#389)
- Perl language rules and skills (patterns, security, testing)
- Kotlin/Android/KMP rules (#309)
- C++ language support (#539)
- Rust language support (#523)
### Infrastructure
- Selective install architecture with manifest resolution (`install-plan.js`, `install-apply.js`) (#509, #512)
- SQLite state store with query CLI for tracking installed components (#510)
- Session adapters for structured session recording (#511)
- Skill evolution foundation for self-improving skills (#514)
- Orchestration harness with deterministic scoring (#524)
- Catalog count enforcement in CI (#525)
- Install manifest validation for all 109 skills (#537)
- PowerShell installer wrapper (#532)
- Antigravity IDE support via `--target antigravity` flag (#332)
- Codex CLI customization scripts (#336)
### Bug Fixes
- Resolved 19 CI test failures across 6 files (#519)
- Fixed 8 test failures in install pipeline, orchestrator, and repair (#564)
- Observer memory explosion with throttling, re-entrancy guard, and tail sampling (#536)
- Observer sandbox access fix for Haiku invocation (#661)
- Worktree project ID mismatch fix (#665)
- Observer lazy-start logic (#508)
- Observer 5-layer loop prevention guard (#399)
- Hook portability and Windows .cmd support
- Biome hook optimization — eliminated npx overhead (#359)
- InsAIts security hook made opt-in (#370)
- Windows spawnSync export fix (#431)
- UTF-8 encoding fix for instinct CLI (#353)
- Secret scrubbing in hooks (#348)
### Translations
- Korean (ko-KR) translation — README, agents, commands, skills, rules (#392)
- Chinese (zh-CN) documentation sync (#428)
### Credits
- @ymdvsymd — observer sandbox and worktree fixes
- @pythonstrup — biome hook optimization
- @Nomadu27 — InsAIts security hook
- @hahmee — Korean translation
- @zdocapp — Chinese translation sync
- @cookiee339 — Kotlin ecosystem
- @pangerlkr — CI workflow fixes
- @0xrohitgarg — VideoDB skills
- @nocodemf — Evos operational skills
- @swarnika-cmd — community contributions
## 1.8.0 - 2026-03-04
### Highlights

View File

@@ -75,6 +75,18 @@ This repo is the raw code only. The guides explain everything.
## What's New
### v1.9.0 — Selective Install & Language Expansion (Mar 2026)
- **Selective install architecture** — Manifest-driven install pipeline with `install-plan.js` and `install-apply.js` for targeted component installation. State store tracks what's installed and enables incremental updates.
- **6 new agents** — `typescript-reviewer`, `pytorch-build-resolver`, `java-build-resolver`, `java-reviewer`, `kotlin-reviewer`, `kotlin-build-resolver` expand language coverage to 10 languages.
- **New skills** — `pytorch-patterns` for deep learning workflows, `documentation-lookup` for API reference research, `bun-runtime` and `nextjs-turbopack` for modern JS toolchains, plus 8 operational domain skills and `mcp-server-patterns`.
- **Session & state infrastructure** — SQLite state store with query CLI, session adapters for structured recording, skill evolution foundation for self-improving skills.
- **Orchestration overhaul** — Harness audit scoring made deterministic, orchestration status and launcher compatibility hardened, observer loop prevention with 5-layer guard.
- **Observer reliability** — Memory explosion fix with throttling and tail sampling, sandbox access fix, lazy-start logic, and re-entrancy guard.
- **12 language ecosystems** — New rules for Java, PHP, Perl, Kotlin/Android/KMP, C++, and Rust join existing TypeScript, Python, Go, and common rules.
- **Community contributions** — Korean and Chinese translations, InsAIts security hook, biome hook optimization, VideoDB skills, Evos operational skills, PowerShell installer, Antigravity IDE support.
- **CI hardening** — 19 test failure fixes, catalog count enforcement, install manifest validation, and full test suite green.
### v1.8.0 — Harness Performance System (Mar 2026)
- **Harness-first release** — ECC is now explicitly framed as an agent harness performance system, not just a config pack.
@@ -191,7 +203,7 @@ For manual install instructions see the README in the `rules/` folder.
/plugin list everything-claude-code@everything-claude-code
```
**That's it!** You now have access to 25 agents, 108 skills, and 57 commands.
**That's it!** You now have access to 27 agents, 114 skills, and 59 commands.
---
@@ -252,7 +264,7 @@ everything-claude-code/
| |-- plugin.json # Plugin metadata and component paths
| |-- marketplace.json # Marketplace catalog for /plugin marketplace add
|
|-- agents/ # Specialized subagents for delegation
|-- agents/ # 27 specialized subagents for delegation
| |-- planner.md # Feature implementation planning
| |-- architect.md # System design decisions
| |-- tdd-guide.md # Test-driven development
@@ -262,10 +274,24 @@ everything-claude-code/
| |-- e2e-runner.md # Playwright E2E testing
| |-- refactor-cleaner.md # Dead code cleanup
| |-- doc-updater.md # Documentation sync
| |-- docs-lookup.md # Documentation/API lookup
| |-- chief-of-staff.md # Communication triage and drafts
| |-- loop-operator.md # Autonomous loop execution
| |-- harness-optimizer.md # Harness config tuning
| |-- cpp-reviewer.md # C++ code review
| |-- cpp-build-resolver.md # C++ build error resolution
| |-- go-reviewer.md # Go code review
| |-- go-build-resolver.md # Go build error resolution
| |-- python-reviewer.md # Python code review (NEW)
| |-- database-reviewer.md # Database/Supabase review (NEW)
| |-- python-reviewer.md # Python code review
| |-- database-reviewer.md # Database/Supabase review
| |-- typescript-reviewer.md # TypeScript/JavaScript code review
| |-- java-reviewer.md # Java/Spring Boot code review
| |-- java-build-resolver.md # Java/Maven/Gradle build errors
| |-- kotlin-reviewer.md # Kotlin/Android/KMP code review
| |-- kotlin-build-resolver.md # Kotlin/Gradle build errors
| |-- rust-reviewer.md # Rust code review
| |-- rust-build-resolver.md # Rust build error resolution
| |-- pytorch-build-resolver.md # PyTorch/CUDA training errors
|
|-- skills/ # Workflow definitions and domain knowledge
| |-- coding-standards/ # Language best practices
@@ -720,6 +746,7 @@ Not sure where to start? Use this quick reference:
| Update documentation | `/update-docs` | doc-updater |
| Review Go code | `/go-review` | go-reviewer |
| Review Python code | `/python-review` | python-reviewer |
| Review TypeScript/JavaScript code | *(invoke `typescript-reviewer` directly)* | typescript-reviewer |
| Audit database queries | *(auto-delegated)* | database-reviewer |
### Common Workflows
@@ -830,7 +857,7 @@ Yes. ECC is cross-platform:
- **Cursor**: Pre-translated configs in `.cursor/`. See [Cursor IDE Support](#cursor-ide-support).
- **OpenCode**: Full plugin support in `.opencode/`. See [OpenCode Support](#-opencode-support).
- **Codex**: First-class support for both macOS app and CLI, with adapter drift guards and SessionStart fallback. See PR [#257](https://github.com/affaan-m/everything-claude-code/pull/257).
- **Antigravity**: Tightly integrated setup for workflows, skills, and flatten rules in `.agent/`.
- **Antigravity**: Tightly integrated setup for workflows, skills, and flattened rules in `.agent/`. See [Antigravity Guide](docs/ANTIGRAVITY-GUIDE.md).
- **Claude Code**: Native — this is the primary target.
</details>
@@ -1042,9 +1069,9 @@ The configuration is automatically detected from `.opencode/opencode.json`.
| Feature | Claude Code | OpenCode | Status |
|---------|-------------|----------|--------|
| Agents | ✅ 25 agents | ✅ 12 agents | **Claude Code leads** |
| Commands | ✅ 57 commands | ✅ 31 commands | **Claude Code leads** |
| Skills | ✅ 108 skills | ✅ 37 skills | **Claude Code leads** |
| Agents | ✅ 27 agents | ✅ 12 agents | **Claude Code leads** |
| Commands | ✅ 59 commands | ✅ 31 commands | **Claude Code leads** |
| Skills | ✅ 114 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** |
@@ -1162,7 +1189,7 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e
| **Context File** | CLAUDE.md + AGENTS.md | AGENTS.md | AGENTS.md | AGENTS.md |
| **Secret Detection** | Hook-based | beforeSubmitPrompt hook | Sandbox-based | Hook-based |
| **Auto-Format** | PostToolUse hook | afterFileEdit hook | N/A | file.edited hook |
| **Version** | Plugin | Plugin | Reference config | 1.8.0 |
| **Version** | Plugin | Plugin | Reference config | 1.9.0 |
**Key architectural decisions:**
- **AGENTS.md** at root is the universal cross-tool file (read by all 4 tools)

View File

@@ -0,0 +1,120 @@
---
name: pytorch-build-resolver
description: PyTorch runtime, CUDA, and training error resolution specialist. Fixes tensor shape mismatches, device errors, gradient issues, DataLoader problems, and mixed precision failures with minimal changes. Use when PyTorch training or inference crashes.
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
model: sonnet
---
# PyTorch Build/Runtime Error Resolver
You are an expert PyTorch error resolution specialist. Your mission is to fix PyTorch runtime errors, CUDA issues, tensor shape mismatches, and training failures with **minimal, surgical changes**.
## Core Responsibilities
1. Diagnose PyTorch runtime and CUDA errors
2. Fix tensor shape mismatches across model layers
3. Resolve device placement issues (CPU/GPU)
4. Debug gradient computation failures
5. Fix DataLoader and data pipeline errors
6. Handle mixed precision (AMP) issues
## Diagnostic Commands
Run these in order:
```bash
python -c "import torch; print(f'PyTorch: {torch.__version__}, CUDA: {torch.cuda.is_available()}, Device: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"CPU\"}')"
python -c "import torch; print(f'cuDNN: {torch.backends.cudnn.version()}')" 2>/dev/null || echo "cuDNN not available"
pip list 2>/dev/null | grep -iE "torch|cuda|nvidia"
nvidia-smi 2>/dev/null || echo "nvidia-smi not available"
python -c "import torch; x = torch.randn(2,3).cuda(); print('CUDA tensor test: OK')" 2>&1 || echo "CUDA tensor creation failed"
```
## Resolution Workflow
```text
1. Read error traceback -> Identify failing line and error type
2. Read affected file -> Understand model/training context
3. Trace tensor shapes -> Print shapes at key points
4. Apply minimal fix -> Only what's needed
5. Run failing script -> Verify fix
6. Check gradients flow -> Ensure backward pass works
```
## Common Fix Patterns
| Error | Cause | Fix |
|-------|-------|-----|
| `RuntimeError: mat1 and mat2 shapes cannot be multiplied` | Linear layer input size mismatch | Fix `in_features` to match previous layer output |
| `RuntimeError: Expected all tensors to be on the same device` | Mixed CPU/GPU tensors | Add `.to(device)` to all tensors and model |
| `CUDA out of memory` | Batch too large or memory leak | Reduce batch size, add `torch.cuda.empty_cache()`, use gradient checkpointing |
| `RuntimeError: element 0 of tensors does not require grad` | Detached tensor in loss computation | Remove `.detach()` or `.item()` before backward |
| `ValueError: Expected input batch_size X to match target batch_size Y` | Mismatched batch dimensions | Fix DataLoader collation or model output reshape |
| `RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation` | In-place op breaks autograd | Replace `x += 1` with `x = x + 1`, avoid in-place relu |
| `RuntimeError: stack expects each tensor to be equal size` | Inconsistent tensor sizes in DataLoader | Add padding/truncation in Dataset `__getitem__` or custom `collate_fn` |
| `RuntimeError: cuDNN error: CUDNN_STATUS_INTERNAL_ERROR` | cuDNN incompatibility or corrupted state | Set `torch.backends.cudnn.enabled = False` to test, update drivers |
| `IndexError: index out of range in self` | Embedding index >= num_embeddings | Fix vocabulary size or clamp indices |
| `RuntimeError: Trying to backward through the graph a second time` | Reused computation graph | Add `retain_graph=True` or restructure forward pass |
## Shape Debugging
When shapes are unclear, inject diagnostic prints:
```python
# Add before the failing line:
print(f"tensor.shape = {tensor.shape}, dtype = {tensor.dtype}, device = {tensor.device}")
# For full model shape tracing:
from torchsummary import summary
summary(model, input_size=(C, H, W))
```
## Memory Debugging
```bash
# Check GPU memory usage
python -c "
import torch
print(f'Allocated: {torch.cuda.memory_allocated()/1e9:.2f} GB')
print(f'Cached: {torch.cuda.memory_reserved()/1e9:.2f} GB')
print(f'Max allocated: {torch.cuda.max_memory_allocated()/1e9:.2f} GB')
"
```
Common memory fixes:
- Wrap validation in `with torch.no_grad():`
- Use `del tensor; torch.cuda.empty_cache()`
- Enable gradient checkpointing: `model.gradient_checkpointing_enable()`
- Use `torch.cuda.amp.autocast()` for mixed precision
## Key Principles
- **Surgical fixes only** -- don't refactor, just fix the error
- **Never** change model architecture unless the error requires it
- **Never** silence warnings with `warnings.filterwarnings` without approval
- **Always** verify tensor shapes before and after fix
- **Always** test with a small batch first (`batch_size=2`)
- Fix root cause over suppressing symptoms
## Stop Conditions
Stop and report if:
- Same error persists after 3 fix attempts
- Fix requires changing the model architecture fundamentally
- Error is caused by hardware/driver incompatibility (recommend driver update)
- Out of memory even with `batch_size=1` (recommend smaller model or gradient checkpointing)
## Output Format
```text
[FIXED] train.py:42
Error: RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x512 and 256x10)
Fix: Changed nn.Linear(256, 10) to nn.Linear(512, 10) to match encoder output
Remaining errors: 0
```
Final: `Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
---
For PyTorch best practices, consult the [official PyTorch documentation](https://pytorch.org/docs/stable/) and [PyTorch forums](https://discuss.pytorch.org/).

View File

@@ -0,0 +1,112 @@
---
name: typescript-reviewer
description: Expert TypeScript/JavaScript code reviewer specializing in type safety, async correctness, Node/web security, and idiomatic patterns. Use for all TypeScript and JavaScript code changes. MUST BE USED for TypeScript/JavaScript projects.
tools: ["Read", "Grep", "Glob", "Bash"]
model: sonnet
---
You are a senior TypeScript engineer ensuring high standards of type-safe, idiomatic TypeScript and JavaScript.
When invoked:
1. Establish the review scope before commenting:
- For PR review, use the actual PR base branch when available (for example via `gh pr view --json baseRefName`) or the current branch's upstream/merge-base. Do not hard-code `main`.
- For local review, prefer `git diff --staged` and `git diff` first.
- If history is shallow or only a single commit is available, fall back to `git show --patch HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx'` so you still inspect code-level changes.
2. Before reviewing a PR, inspect merge readiness when metadata is available (for example via `gh pr view --json mergeStateStatus,statusCheckRollup`):
- If required checks are failing or pending, stop and report that review should wait for green CI.
- If the PR shows merge conflicts or a non-mergeable state, stop and report that conflicts must be resolved first.
- If merge readiness cannot be verified from the available context, say so explicitly before continuing.
3. Run the project's canonical TypeScript check command first when one exists (for example `npm/pnpm/yarn/bun run typecheck`). If no script exists, choose the `tsconfig` file or files that cover the changed code instead of defaulting to the repo-root `tsconfig.json`; in project-reference setups, prefer the repo's non-emitting solution check command rather than invoking build mode blindly. Otherwise use `tsc --noEmit -p <relevant-config>`. Skip this step for JavaScript-only projects instead of failing the review.
4. Run `eslint . --ext .ts,.tsx,.js,.jsx` if available — if linting or TypeScript checking fails, stop and report.
5. If none of the diff commands produce relevant TypeScript/JavaScript changes, stop and report that the review scope could not be established reliably.
6. Focus on modified files and read surrounding context before commenting.
7. Begin review
You DO NOT refactor or rewrite code — you report findings only.
## Review Priorities
### CRITICAL -- Security
- **Injection via `eval` / `new Function`**: User-controlled input passed to dynamic execution — never execute untrusted strings
- **XSS**: Unsanitised user input assigned to `innerHTML`, `dangerouslySetInnerHTML`, or `document.write`
- **SQL/NoSQL injection**: String concatenation in queries — use parameterised queries or an ORM
- **Path traversal**: User-controlled input in `fs.readFile`, `path.join` without `path.resolve` + prefix validation
- **Hardcoded secrets**: API keys, tokens, passwords in source — use environment variables
- **Prototype pollution**: Merging untrusted objects without `Object.create(null)` or schema validation
- **`child_process` with user input**: Validate and allowlist before passing to `exec`/`spawn`
### HIGH -- Type Safety
- **`any` without justification**: Disables type checking — use `unknown` and narrow, or a precise type
- **Non-null assertion abuse**: `value!` without a preceding guard — add a runtime check
- **`as` casts that bypass checks**: Casting to unrelated types to silence errors — fix the type instead
- **Relaxed compiler settings**: If `tsconfig.json` is touched and weakens strictness, call it out explicitly
### HIGH -- Async Correctness
- **Unhandled promise rejections**: `async` functions called without `await` or `.catch()`
- **Sequential awaits for independent work**: `await` inside loops when operations could safely run in parallel — consider `Promise.all`
- **Floating promises**: Fire-and-forget without error handling in event handlers or constructors
- **`async` with `forEach`**: `array.forEach(async fn)` does not await — use `for...of` or `Promise.all`
### HIGH -- Error Handling
- **Swallowed errors**: Empty `catch` blocks or `catch (e) {}` with no action
- **`JSON.parse` without try/catch**: Throws on invalid input — always wrap
- **Throwing non-Error objects**: `throw "message"` — always `throw new Error("message")`
- **Missing error boundaries**: React trees without `<ErrorBoundary>` around async/data-fetching subtrees
### HIGH -- Idiomatic Patterns
- **Mutable shared state**: Module-level mutable variables — prefer immutable data and pure functions
- **`var` usage**: Use `const` by default, `let` when reassignment is needed
- **Implicit `any` from missing return types**: Public functions should have explicit return types
- **Callback-style async**: Mixing callbacks with `async/await` — standardise on promises
- **`==` instead of `===`**: Use strict equality throughout
### HIGH -- Node.js Specifics
- **Synchronous fs in request handlers**: `fs.readFileSync` blocks the event loop — use async variants
- **Missing input validation at boundaries**: No schema validation (zod, joi, yup) on external data
- **Unvalidated `process.env` access**: Access without fallback or startup validation
- **`require()` in ESM context**: Mixing module systems without clear intent
### MEDIUM -- React / Next.js (when applicable)
- **Missing dependency arrays**: `useEffect`/`useCallback`/`useMemo` with incomplete deps — use exhaustive-deps lint rule
- **State mutation**: Mutating state directly instead of returning new objects
- **Key prop using index**: `key={index}` in dynamic lists — use stable unique IDs
- **`useEffect` for derived state**: Compute derived values during render, not in effects
- **Server/client boundary leaks**: Importing server-only modules into client components in Next.js
### MEDIUM -- Performance
- **Object/array creation in render**: Inline objects as props cause unnecessary re-renders — hoist or memoize
- **N+1 queries**: Database or API calls inside loops — batch or use `Promise.all`
- **Missing `React.memo` / `useMemo`**: Expensive computations or components re-running on every render
- **Large bundle imports**: `import _ from 'lodash'` — use named imports or tree-shakeable alternatives
### MEDIUM -- Best Practices
- **`console.log` left in production code**: Use a structured logger
- **Magic numbers/strings**: Use named constants or enums
- **Deep optional chaining without fallback**: `a?.b?.c?.d` with no default — add `?? fallback`
- **Inconsistent naming**: camelCase for variables/functions, PascalCase for types/classes/components
## Diagnostic Commands
```bash
npm run typecheck --if-present # Canonical TypeScript check when the project defines one
tsc --noEmit -p <relevant-config> # Fallback type check for the tsconfig that owns the changed files
eslint . --ext .ts,.tsx,.js,.jsx # Linting
prettier --check . # Format check
npm audit # Dependency vulnerabilities (or the equivalent yarn/pnpm/bun audit command)
vitest run # Tests (Vitest)
jest --ci # Tests (Jest)
```
## Approval Criteria
- **Approve**: No CRITICAL or HIGH issues
- **Warning**: MEDIUM issues only (can merge with caution)
- **Block**: CRITICAL or HIGH issues found
## Reference
This repo does not yet ship a dedicated `typescript-patterns` skill. For detailed TypeScript and JavaScript patterns, use `coding-standards` plus `frontend-patterns` or `backend-patterns` based on the code being reviewed.
---
Review with the mindset: "Would this code pass review at a top TypeScript shop or well-maintained open-source project?"

View File

@@ -0,0 +1,29 @@
---
description: Analyze context window usage across agents, skills, MCP servers, and rules to find optimization opportunities. Helps reduce token overhead and avoid performance warnings.
---
# Context Budget Optimizer
Analyze your Claude Code setup's context window consumption and produce actionable recommendations to reduce token overhead.
## Usage
```
/context-budget [--verbose]
```
- Default: summary with top recommendations
- `--verbose`: full breakdown per component
$ARGUMENTS
## What to Do
Run the **context-budget** skill (`skills/context-budget/SKILL.md`) with the following inputs:
1. Pass `--verbose` flag if present in `$ARGUMENTS`
2. Assume a 200K context window (Claude Sonnet default) unless the user specifies otherwise
3. Follow the skill's four phases: Inventory → Classify → Detect Issues → Report
4. Output the formatted Context Budget Report to the user
The skill handles all scanning logic, token estimation, issue detection, and report formatting.

11
commands/rules-distill.md Normal file
View File

@@ -0,0 +1,11 @@
---
description: "Scan skills to extract cross-cutting principles and distill them into rules"
---
# /rules-distill — Distill Principles from Skills into Rules
Scan installed skills, extract cross-cutting principles, and distill them into rules.
## Process
Follow the full workflow defined in the `rules-distill` skill.

View File

@@ -29,8 +29,8 @@ Use `/sessions info` when you need operator-surface context for a swarm: branch,
**Script:**
```bash
node -e "
const sm = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-manager');
const aa = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-aliases');
const sm = require((()=>{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;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q)))return c}}catch(x){}return d})()+'/scripts/lib/session-manager');
const aa = require((()=>{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;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q)))return c}}catch(x){}return d})()+'/scripts/lib/session-aliases');
const path = require('path');
const result = sm.getAllSessions({ limit: 20 });
@@ -70,8 +70,8 @@ Load and display a session's content (by ID or alias).
**Script:**
```bash
node -e "
const sm = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-manager');
const aa = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-aliases');
const sm = require((()=>{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;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q)))return c}}catch(x){}return d})()+'/scripts/lib/session-manager');
const aa = require((()=>{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;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q)))return c}}catch(x){}return d})()+'/scripts/lib/session-aliases');
const id = process.argv[1];
// First try to resolve as alias
@@ -143,8 +143,8 @@ Create a memorable alias for a session.
**Script:**
```bash
node -e "
const sm = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-manager');
const aa = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-aliases');
const sm = require((()=>{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;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q)))return c}}catch(x){}return d})()+'/scripts/lib/session-manager');
const aa = require((()=>{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;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q)))return c}}catch(x){}return d})()+'/scripts/lib/session-aliases');
const sessionId = process.argv[1];
const aliasName = process.argv[2];
@@ -183,7 +183,7 @@ Delete an existing alias.
**Script:**
```bash
node -e "
const aa = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-aliases');
const aa = require((()=>{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;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q)))return c}}catch(x){}return d})()+'/scripts/lib/session-aliases');
const aliasName = process.argv[1];
if (!aliasName) {
@@ -212,8 +212,8 @@ Show detailed information about a session.
**Script:**
```bash
node -e "
const sm = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-manager');
const aa = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-aliases');
const sm = require((()=>{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;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q)))return c}}catch(x){}return d})()+'/scripts/lib/session-manager');
const aa = require((()=>{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;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q)))return c}}catch(x){}return d})()+'/scripts/lib/session-aliases');
const id = process.argv[1];
const resolved = aa.resolveAlias(id);
@@ -262,7 +262,7 @@ Show all session aliases.
**Script:**
```bash
node -e "
const aa = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-aliases');
const aa = require((()=>{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;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q)))return c}}catch(x){}return d})()+'/scripts/lib/session-aliases');
const aliases = aa.listAliases();
console.log('Session Aliases (' + aliases.length + '):');

View File

@@ -13,19 +13,22 @@ Shows a comprehensive health dashboard for all skills in the portfolio with succ
Run the skill health CLI in dashboard mode:
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/skills-health.js" --dashboard
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "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))){try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q))){d=c;break}}}catch(x){}}console.log(d)")}"
node "$ECC_ROOT/scripts/skills-health.js" --dashboard
```
For a specific panel only:
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/skills-health.js" --dashboard --panel failures
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "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))){try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q))){d=c;break}}}catch(x){}}console.log(d)")}"
node "$ECC_ROOT/scripts/skills-health.js" --dashboard --panel failures
```
For machine-readable output:
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/skills-health.js" --dashboard --json
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "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))){try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q))){d=c;break}}}catch(x){}}console.log(d)")}"
node "$ECC_ROOT/scripts/skills-health.js" --dashboard --json
```
## Usage

156
docs/ANTIGRAVITY-GUIDE.md Normal file
View File

@@ -0,0 +1,156 @@
# Antigravity Setup and Usage Guide
Google's [Antigravity](https://antigravity.dev) is an AI coding IDE that uses a `.agent/` directory convention for configuration. ECC provides first-class support for Antigravity through its selective install system.
## Quick Start
```bash
# Install ECC with Antigravity target
./install.sh --target antigravity typescript
# Or with multiple language modules
./install.sh --target antigravity typescript python go
```
This installs ECC components into your project's `.agent/` directory, ready for Antigravity to pick up.
## How the Install Mapping Works
ECC remaps its component structure to match Antigravity's expected layout:
| ECC Source | Antigravity Destination | What It Contains |
|------------|------------------------|------------------|
| `rules/` | `.agent/rules/` | Language rules and coding standards (flattened) |
| `commands/` | `.agent/workflows/` | Slash commands become Antigravity workflows |
| `agents/` | `.agent/skills/` | Agent definitions become Antigravity skills |
> **Note on `.agents/` vs `.agent/` vs `agents/`**: The installer only handles three source paths explicitly: `rules` → `.agent/rules/`, `commands` → `.agent/workflows/`, and `agents` (no dot prefix) → `.agent/skills/`. The dot-prefixed `.agents/` directory in the ECC repo is a **static layout** for Codex/Antigravity skill definitions and `openai.yaml` configs — it is not directly mapped by the installer. Any `.agents/` path falls through to the default scaffold operation. If you want `.agents/skills/` content available in the Antigravity runtime, you must manually copy it to `.agent/skills/`.
### Key Differences from Claude Code
- **Rules are flattened**: Claude Code nests rules under subdirectories (`rules/common/`, `rules/typescript/`). Antigravity expects a flat `rules/` directory — the installer handles this automatically.
- **Commands become workflows**: ECC's `/command` files land in `.agent/workflows/`, which is Antigravity's equivalent of slash commands.
- **Agents become skills**: ECC agent definitions map to `.agent/skills/`, where Antigravity looks for skill configurations.
## Directory Structure After Install
```
your-project/
├── .agent/
│ ├── rules/
│ │ ├── coding-standards.md
│ │ ├── testing.md
│ │ ├── security.md
│ │ └── typescript.md # language-specific rules
│ ├── workflows/
│ │ ├── plan.md
│ │ ├── code-review.md
│ │ ├── tdd.md
│ │ └── ...
│ ├── skills/
│ │ ├── planner.md
│ │ ├── code-reviewer.md
│ │ ├── tdd-guide.md
│ │ └── ...
│ └── ecc-install-state.json # tracks what ECC installed
```
## The `openai.yaml` Agent Config
Each skill directory under `.agents/skills/` contains an `agents/openai.yaml` file at the path `.agents/skills/<skill-name>/agents/openai.yaml` that configures the skill for Antigravity:
```yaml
interface:
display_name: "API Design"
short_description: "REST API design patterns and best practices"
brand_color: "#F97316"
default_prompt: "Design REST API: resources, status codes, pagination"
policy:
allow_implicit_invocation: true
```
| Field | Purpose |
|-------|---------|
| `display_name` | Human-readable name shown in Antigravity's UI |
| `short_description` | Brief description of what the skill does |
| `brand_color` | Hex color for the skill's visual badge |
| `default_prompt` | Suggested prompt when the skill is invoked manually |
| `allow_implicit_invocation` | When `true`, Antigravity can activate the skill automatically based on context |
## Managing Your Installation
### Check What's Installed
```bash
node scripts/list-installed.js --target antigravity
```
### Repair a Broken Install
```bash
# First, diagnose what's wrong
node scripts/doctor.js --target antigravity
# Then, restore missing or drifted files
node scripts/repair.js --target antigravity
```
### Uninstall
```bash
node scripts/uninstall.js --target antigravity
```
### Install State
The installer writes `.agent/ecc-install-state.json` to track which files ECC owns. This enables safe uninstall and repair — ECC will never touch files it didn't create.
## Adding Custom Skills for Antigravity
If you're contributing a new skill and want it available on Antigravity:
1. Create the skill under `skills/your-skill-name/SKILL.md` as usual
2. Add an agent definition at `agents/your-skill-name.md` — this is the path the installer maps to `.agent/skills/` at runtime, making your skill available in the Antigravity harness
3. Add the Antigravity agent config at `.agents/skills/your-skill-name/agents/openai.yaml` — this is a static repo layout consumed by Codex for implicit invocation metadata
4. Mirror the `SKILL.md` content to `.agents/skills/your-skill-name/SKILL.md` — this static copy is used by Codex and serves as a reference for Antigravity
5. Mention in your PR that you added Antigravity support
> **Key distinction**: The installer deploys `agents/` (no dot) → `.agent/skills/` — this is what makes skills available at runtime. The `.agents/` (dot-prefixed) directory is a separate static layout for Codex `openai.yaml` configs and is not auto-deployed by the installer.
See [CONTRIBUTING.md](../CONTRIBUTING.md) for the full contribution guide.
## Comparison with Other Targets
| Feature | Claude Code | Cursor | Codex | Antigravity |
|---------|-------------|--------|-------|-------------|
| Install target | `claude-home` | `cursor-project` | `codex-home` | `antigravity` |
| Config root | `~/.claude/` | `.cursor/` | `~/.codex/` | `.agent/` |
| Scope | User-level | Project-level | User-level | Project-level |
| Rules format | Nested dirs | Flat | Flat | Flat |
| Commands | `commands/` | N/A | N/A | `workflows/` |
| Agents/Skills | `agents/` | N/A | N/A | `skills/` |
| Install state | `ecc-install-state.json` | `ecc-install-state.json` | `ecc-install-state.json` | `ecc-install-state.json` |
## Troubleshooting
### Skills not loading in Antigravity
- Verify the `.agent/` directory exists in your project root (not home directory)
- Check that `ecc-install-state.json` was created — if missing, re-run the installer
- Ensure files have `.md` extension and valid frontmatter
### Rules not applying
- Rules must be in `.agent/rules/`, not nested in subdirectories
- Run `node scripts/doctor.js --target antigravity` to verify the install
### Workflows not available
- Antigravity looks for workflows in `.agent/workflows/`, not `commands/`
- If you manually copied ECC commands, rename the directory
## Related Resources
- [Selective Install Architecture](./SELECTIVE-INSTALL-ARCHITECTURE.md) — how the install system works under the hood
- [Selective Install Design](./SELECTIVE-INSTALL-DESIGN.md) — design decisions and target adapter contracts
- [CONTRIBUTING.md](../CONTRIBUTING.md) — how to contribute skills, agents, and commands

View File

@@ -1,6 +1,6 @@
# Command → Agent / Skill Map
This document lists each slash command and the primary agent(s) or skills it invokes. Use it to discover which commands use which agents and to keep refactoring consistent.
This document lists each slash command and the primary agent(s) or skills it invokes, plus notable direct-invoke agents. Use it to discover which commands use which agents and to keep refactoring consistent.
| Command | Primary agent(s) | Notes |
|---------|------------------|--------|
@@ -46,6 +46,12 @@ This document lists each slash command and the primary agent(s) or skills it inv
| `/pm2` | — | PM2 service lifecycle |
| `/security-scan` | security-reviewer (skill) | AgentShield via security-scan skill |
## Direct-Use Agents
| Direct agent | Purpose | Scope | Notes |
|--------------|---------|-------|-------|
| `typescript-reviewer` | TypeScript/JavaScript code review | TypeScript/JavaScript projects | Invoke the agent directly when a review needs TS/JS-specific findings and there is no dedicated slash command yet. |
## Skills referenced by commands
- **continuous-learning**, **continuous-learning-v2**: `/learn`, `/learn-eval`, `/instinct-*`, `/evolve`, `/promote`, `/projects`

View File

@@ -1163,7 +1163,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
| **上下文文件** | CLAUDE.md + AGENTS.md | AGENTS.md | AGENTS.md | AGENTS.md |
| **秘密检测** | 基于钩子 | beforeSubmitPrompt 钩子 | 基于沙箱 | 基于钩子 |
| **自动格式化** | PostToolUse 钩子 | afterFileEdit 钩子 | N/A | file.edited 钩子 |
| **版本** | 插件 | 插件 | 参考配置 | 1.8.0 |
| **版本** | 插件 | 插件 | 参考配置 | 1.9.0 |
**关键架构决策:**

View File

@@ -2,6 +2,16 @@
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "npx block-no-verify@1.1.2"
}
],
"description": "Block git hook-bypass flag to protect pre-commit, commit-msg, and pre-push hooks from being skipped"
},
{
"matcher": "Bash",
"hooks": [
@@ -74,6 +84,17 @@
}
],
"description": "Optional InsAIts AI security monitor for Bash/Edit/Write flows. Enable with ECC_ENABLE_INSAITS=1. Requires: pip install insa-its"
},
{
"matcher": "Bash|Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"pre:governance-capture\" \"scripts/hooks/governance-capture.js\" \"standard,strict\"",
"timeout": 10
}
],
"description": "Capture governance events (secrets, policy violations, approval requests). Enable with ECC_GOVERNANCE_CAPTURE=1"
}
],
"PreCompact": [
@@ -165,6 +186,17 @@
],
"description": "Warn about console.log statements after edits"
},
{
"matcher": "Bash|Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"post:governance-capture\" \"scripts/hooks/governance-capture.js\" \"standard,strict\"",
"timeout": 10
}
],
"description": "Capture governance events from tool outputs. Enable with ECC_GOVERNANCE_CAPTURE=1"
},
{
"matcher": "*",
"hooks": [

View File

@@ -250,6 +250,158 @@
"modules": [
"document-processing"
]
},
{
"id": "agent:architect",
"family": "agent",
"description": "System design and architecture agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:code-reviewer",
"family": "agent",
"description": "Code review agent for quality and security checks.",
"modules": [
"agents-core"
]
},
{
"id": "agent:security-reviewer",
"family": "agent",
"description": "Security vulnerability analysis agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:tdd-guide",
"family": "agent",
"description": "Test-driven development guidance agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:planner",
"family": "agent",
"description": "Feature implementation planning agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:build-error-resolver",
"family": "agent",
"description": "Build error resolution agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:e2e-runner",
"family": "agent",
"description": "Playwright E2E testing agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:refactor-cleaner",
"family": "agent",
"description": "Dead code cleanup and refactoring agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:doc-updater",
"family": "agent",
"description": "Documentation update agent.",
"modules": [
"agents-core"
]
},
{
"id": "skill:tdd-workflow",
"family": "skill",
"description": "Test-driven development workflow skill.",
"modules": [
"workflow-quality"
]
},
{
"id": "skill:continuous-learning",
"family": "skill",
"description": "Session pattern extraction and continuous learning skill.",
"modules": [
"workflow-quality"
]
},
{
"id": "skill:eval-harness",
"family": "skill",
"description": "Evaluation harness for AI regression testing.",
"modules": [
"workflow-quality"
]
},
{
"id": "skill:verification-loop",
"family": "skill",
"description": "Verification loop for code quality assurance.",
"modules": [
"workflow-quality"
]
},
{
"id": "skill:strategic-compact",
"family": "skill",
"description": "Strategic context compaction for long sessions.",
"modules": [
"workflow-quality"
]
},
{
"id": "skill:coding-standards",
"family": "skill",
"description": "Language-agnostic coding standards and best practices.",
"modules": [
"framework-language"
]
},
{
"id": "skill:frontend-patterns",
"family": "skill",
"description": "React and frontend engineering patterns.",
"modules": [
"framework-language"
]
},
{
"id": "skill:backend-patterns",
"family": "skill",
"description": "API design, database, and backend engineering patterns.",
"modules": [
"framework-language"
]
},
{
"id": "skill:security-review",
"family": "skill",
"description": "Security review checklist and vulnerability analysis.",
"modules": [
"security"
]
},
{
"id": "skill:deep-research",
"family": "skill",
"description": "Deep research and investigation workflows.",
"modules": [
"research-apis"
]
}
]
}

10
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ecc-universal",
"version": "1.8.0",
"version": "1.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ecc-universal",
"version": "1.8.0",
"version": "1.9.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -1133,9 +1133,9 @@
}
},
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},

View File

@@ -1,6 +1,6 @@
{
"name": "ecc-universal",
"version": "1.8.0",
"version": "1.9.0",
"description": "Complete collection of battle-tested Claude Code configs — agents, skills, hooks, commands, and rules evolved over 10+ months of intensive daily use by an Anthropic hackathon winner",
"keywords": [
"claude-code",

114
rules/java/coding-style.md Normal file
View File

@@ -0,0 +1,114 @@
---
paths:
- "**/*.java"
---
# Java Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with Java-specific content.
## Formatting
- **google-java-format** or **Checkstyle** (Google or Sun style) for enforcement
- One public top-level type per file
- Consistent indent: 2 or 4 spaces (match project standard)
- Member order: constants, fields, constructors, public methods, protected, private
## Immutability
- Prefer `record` for value types (Java 16+)
- Mark fields `final` by default — use mutable state only when required
- Return defensive copies from public APIs: `List.copyOf()`, `Map.copyOf()`, `Set.copyOf()`
- Copy-on-write: return new instances rather than mutating existing ones
```java
// GOOD — immutable value type
public record OrderSummary(Long id, String customerName, BigDecimal total) {}
// GOOD — final fields, no setters
public class Order {
private final Long id;
private final List<LineItem> items;
public List<LineItem> getItems() {
return List.copyOf(items);
}
}
```
## Naming
Follow standard Java conventions:
- `PascalCase` for classes, interfaces, records, enums
- `camelCase` for methods, fields, parameters, local variables
- `SCREAMING_SNAKE_CASE` for `static final` constants
- Packages: all lowercase, reverse domain (`com.example.app.service`)
## Modern Java Features
Use modern language features where they improve clarity:
- **Records** for DTOs and value types (Java 16+)
- **Sealed classes** for closed type hierarchies (Java 17+)
- **Pattern matching** with `instanceof` — no explicit cast (Java 16+)
- **Text blocks** for multi-line strings — SQL, JSON templates (Java 15+)
- **Switch expressions** with arrow syntax (Java 14+)
- **Pattern matching in switch** — exhaustive sealed type handling (Java 21+)
```java
// Pattern matching instanceof
if (shape instanceof Circle c) {
return Math.PI * c.radius() * c.radius();
}
// Sealed type hierarchy
public sealed interface PaymentMethod permits CreditCard, BankTransfer, Wallet {}
// Switch expression
String label = switch (status) {
case ACTIVE -> "Active";
case SUSPENDED -> "Suspended";
case CLOSED -> "Closed";
};
```
## Optional Usage
- Return `Optional<T>` from finder methods that may have no result
- Use `map()`, `flatMap()`, `orElseThrow()` — never call `get()` without `isPresent()`
- Never use `Optional` as a field type or method parameter
```java
// GOOD
return repository.findById(id)
.map(ResponseDto::from)
.orElseThrow(() -> new OrderNotFoundException(id));
// BAD — Optional as parameter
public void process(Optional<String> name) {}
```
## Error Handling
- Prefer unchecked exceptions for domain errors
- Create domain-specific exceptions extending `RuntimeException`
- Avoid broad `catch (Exception e)` unless at top-level handlers
- Include context in exception messages
```java
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(Long id) {
super("Order not found: id=" + id);
}
}
```
## Streams
- Use streams for transformations; keep pipelines short (3-4 operations max)
- Prefer method references when readable: `.map(Order::getTotal)`
- Avoid side effects in stream operations
- For complex logic, prefer a loop over a convoluted stream pipeline
## References
See skill: `java-coding-standards` for full coding standards with examples.
See skill: `jpa-patterns` for JPA/Hibernate entity design patterns.

18
rules/java/hooks.md Normal file
View File

@@ -0,0 +1,18 @@
---
paths:
- "**/*.java"
- "**/pom.xml"
- "**/build.gradle"
- "**/build.gradle.kts"
---
# Java Hooks
> This file extends [common/hooks.md](../common/hooks.md) with Java-specific content.
## PostToolUse Hooks
Configure in `~/.claude/settings.json`:
- **google-java-format**: Auto-format `.java` files after edit
- **checkstyle**: Run style checks after editing Java files
- **./mvnw compile** or **./gradlew compileJava**: Verify compilation after changes

146
rules/java/patterns.md Normal file
View File

@@ -0,0 +1,146 @@
---
paths:
- "**/*.java"
---
# Java Patterns
> This file extends [common/patterns.md](../common/patterns.md) with Java-specific content.
## Repository Pattern
Encapsulate data access behind an interface:
```java
public interface OrderRepository {
Optional<Order> findById(Long id);
List<Order> findAll();
Order save(Order order);
void deleteById(Long id);
}
```
Concrete implementations handle storage details (JPA, JDBC, in-memory for tests).
## Service Layer
Business logic in service classes; keep controllers and repositories thin:
```java
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
public OrderService(OrderRepository orderRepository, PaymentGateway paymentGateway) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
}
public OrderSummary placeOrder(CreateOrderRequest request) {
var order = Order.from(request);
paymentGateway.charge(order.total());
var saved = orderRepository.save(order);
return OrderSummary.from(saved);
}
}
```
## Constructor Injection
Always use constructor injection — never field injection:
```java
// GOOD — constructor injection (testable, immutable)
public class NotificationService {
private final EmailSender emailSender;
public NotificationService(EmailSender emailSender) {
this.emailSender = emailSender;
}
}
// BAD — field injection (untestable without reflection, requires framework magic)
public class NotificationService {
@Inject // or @Autowired
private EmailSender emailSender;
}
```
## DTO Mapping
Use records for DTOs. Map at service/controller boundaries:
```java
public record OrderResponse(Long id, String customer, BigDecimal total) {
public static OrderResponse from(Order order) {
return new OrderResponse(order.getId(), order.getCustomerName(), order.getTotal());
}
}
```
## Builder Pattern
Use for objects with many optional parameters:
```java
public class SearchCriteria {
private final String query;
private final int page;
private final int size;
private final String sortBy;
private SearchCriteria(Builder builder) {
this.query = builder.query;
this.page = builder.page;
this.size = builder.size;
this.sortBy = builder.sortBy;
}
public static class Builder {
private String query = "";
private int page = 0;
private int size = 20;
private String sortBy = "id";
public Builder query(String query) { this.query = query; return this; }
public Builder page(int page) { this.page = page; return this; }
public Builder size(int size) { this.size = size; return this; }
public Builder sortBy(String sortBy) { this.sortBy = sortBy; return this; }
public SearchCriteria build() { return new SearchCriteria(this); }
}
}
```
## Sealed Types for Domain Models
```java
public sealed interface PaymentResult permits PaymentSuccess, PaymentFailure {
record PaymentSuccess(String transactionId, BigDecimal amount) implements PaymentResult {}
record PaymentFailure(String errorCode, String message) implements PaymentResult {}
}
// Exhaustive handling (Java 21+)
String message = switch (result) {
case PaymentSuccess s -> "Paid: " + s.transactionId();
case PaymentFailure f -> "Failed: " + f.errorCode();
};
```
## API Response Envelope
Consistent API responses:
```java
public record ApiResponse<T>(boolean success, T data, String error) {
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(true, data, null);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, null, message);
}
}
```
## References
See skill: `springboot-patterns` for Spring Boot architecture patterns.
See skill: `jpa-patterns` for entity design and query optimization.

100
rules/java/security.md Normal file
View File

@@ -0,0 +1,100 @@
---
paths:
- "**/*.java"
---
# Java Security
> This file extends [common/security.md](../common/security.md) with Java-specific content.
## Secrets Management
- Never hardcode API keys, tokens, or credentials in source code
- Use environment variables: `System.getenv("API_KEY")`
- Use a secret manager (Vault, AWS Secrets Manager) for production secrets
- Keep local config files with secrets in `.gitignore`
```java
// BAD
private static final String API_KEY = "sk-abc123...";
// GOOD — environment variable
String apiKey = System.getenv("PAYMENT_API_KEY");
Objects.requireNonNull(apiKey, "PAYMENT_API_KEY must be set");
```
## SQL Injection Prevention
- Always use parameterized queries — never concatenate user input into SQL
- Use `PreparedStatement` or your framework's parameterized query API
- Validate and sanitize any input used in native queries
```java
// BAD — SQL injection via string concatenation
Statement stmt = conn.createStatement();
String sql = "SELECT * FROM orders WHERE name = '" + name + "'";
stmt.executeQuery(sql);
// GOOD — PreparedStatement with parameterized query
PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE name = ?");
ps.setString(1, name);
// GOOD — JDBC template
jdbcTemplate.query("SELECT * FROM orders WHERE name = ?", mapper, name);
```
## Input Validation
- Validate all user input at system boundaries before processing
- Use Bean Validation (`@NotNull`, `@NotBlank`, `@Size`) on DTOs when using a validation framework
- Sanitize file paths and user-provided strings before use
- Reject input that fails validation with clear error messages
```java
// Validate manually in plain Java
public Order createOrder(String customerName, BigDecimal amount) {
if (customerName == null || customerName.isBlank()) {
throw new IllegalArgumentException("Customer name is required");
}
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
return new Order(customerName, amount);
}
```
## Authentication and Authorization
- Never implement custom auth crypto — use established libraries
- Store passwords with bcrypt or Argon2, never MD5/SHA1
- Enforce authorization checks at service boundaries
- Clear sensitive data from logs — never log passwords, tokens, or PII
## Dependency Security
- Run `mvn dependency:tree` or `./gradlew dependencies` to audit transitive dependencies
- Use OWASP Dependency-Check or Snyk to scan for known CVEs
- Keep dependencies updated — set up Dependabot or Renovate
## Error Messages
- Never expose stack traces, internal paths, or SQL errors in API responses
- Map exceptions to safe, generic client messages at handler boundaries
- Log detailed errors server-side; return generic messages to clients
```java
// Log the detail, return a generic message
try {
return orderService.findById(id);
} catch (OrderNotFoundException ex) {
log.warn("Order not found: id={}", id);
return ApiResponse.error("Resource not found"); // generic, no internals
} catch (Exception ex) {
log.error("Unexpected error processing order id={}", id, ex);
return ApiResponse.error("Internal server error"); // never expose ex.getMessage()
}
```
## References
See skill: `springboot-security` for Spring Security authentication and authorization patterns.
See skill: `security-review` for general security checklists.

131
rules/java/testing.md Normal file
View File

@@ -0,0 +1,131 @@
---
paths:
- "**/*.java"
---
# Java Testing
> This file extends [common/testing.md](../common/testing.md) with Java-specific content.
## Test Framework
- **JUnit 5** (`@Test`, `@ParameterizedTest`, `@Nested`, `@DisplayName`)
- **AssertJ** for fluent assertions (`assertThat(result).isEqualTo(expected)`)
- **Mockito** for mocking dependencies
- **Testcontainers** for integration tests requiring databases or services
## Test Organization
```
src/test/java/com/example/app/
service/ # Unit tests for service layer
controller/ # Web layer / API tests
repository/ # Data access tests
integration/ # Cross-layer integration tests
```
Mirror the `src/main/java` package structure in `src/test/java`.
## Unit Test Pattern
```java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
private OrderService orderService;
@BeforeEach
void setUp() {
orderService = new OrderService(orderRepository);
}
@Test
@DisplayName("findById returns order when exists")
void findById_existingOrder_returnsOrder() {
var order = new Order(1L, "Alice", BigDecimal.TEN);
when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
var result = orderService.findById(1L);
assertThat(result.customerName()).isEqualTo("Alice");
verify(orderRepository).findById(1L);
}
@Test
@DisplayName("findById throws when order not found")
void findById_missingOrder_throws() {
when(orderRepository.findById(99L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> orderService.findById(99L))
.isInstanceOf(OrderNotFoundException.class)
.hasMessageContaining("99");
}
}
```
## Parameterized Tests
```java
@ParameterizedTest
@CsvSource({
"100.00, 10, 90.00",
"50.00, 0, 50.00",
"200.00, 25, 150.00"
})
@DisplayName("discount applied correctly")
void applyDiscount(BigDecimal price, int pct, BigDecimal expected) {
assertThat(PricingUtils.discount(price, pct)).isEqualByComparingTo(expected);
}
```
## Integration Tests
Use Testcontainers for real database integration:
```java
@Testcontainers
class OrderRepositoryIT {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
private OrderRepository repository;
@BeforeEach
void setUp() {
var dataSource = new PGSimpleDataSource();
dataSource.setUrl(postgres.getJdbcUrl());
dataSource.setUser(postgres.getUsername());
dataSource.setPassword(postgres.getPassword());
repository = new JdbcOrderRepository(dataSource);
}
@Test
void save_and_findById() {
var saved = repository.save(new Order(null, "Bob", BigDecimal.ONE));
var found = repository.findById(saved.getId());
assertThat(found).isPresent();
}
}
```
For Spring Boot integration tests, see skill: `springboot-tdd`.
## Test Naming
Use descriptive names with `@DisplayName`:
- `methodName_scenario_expectedBehavior()` for method names
- `@DisplayName("human-readable description")` for reports
## Coverage
- Target 80%+ line coverage
- Use JaCoCo for coverage reporting
- Focus on service and domain logic — skip trivial getters/config classes
## References
See skill: `springboot-tdd` for Spring Boot TDD patterns with MockMvc and Testcontainers.
See skill: `java-coding-standards` for testing expectations.

151
rules/rust/coding-style.md Normal file
View File

@@ -0,0 +1,151 @@
---
paths:
- "**/*.rs"
---
# Rust Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with Rust-specific content.
## Formatting
- **rustfmt** for enforcement — always run `cargo fmt` before committing
- **clippy** for lints — `cargo clippy -- -D warnings` (treat warnings as errors)
- 4-space indent (rustfmt default)
- Max line width: 100 characters (rustfmt default)
## Immutability
Rust variables are immutable by default — embrace this:
- Use `let` by default; only use `let mut` when mutation is required
- Prefer returning new values over mutating in place
- Use `Cow<'_, T>` when a function may or may not need to allocate
```rust
use std::borrow::Cow;
// GOOD — immutable by default, new value returned
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input)
}
}
// BAD — unnecessary mutation
fn normalize_bad(input: &mut String) {
*input = input.replace(' ', "_");
}
```
## Naming
Follow standard Rust conventions:
- `snake_case` for functions, methods, variables, modules, crates
- `PascalCase` (UpperCamelCase) for types, traits, enums, type parameters
- `SCREAMING_SNAKE_CASE` for constants and statics
- Lifetimes: short lowercase (`'a`, `'de`) — descriptive names for complex cases (`'input`)
## Ownership and Borrowing
- Borrow (`&T`) by default; take ownership only when you need to store or consume
- Never clone to satisfy the borrow checker without understanding the root cause
- Accept `&str` over `String`, `&[T]` over `Vec<T>` in function parameters
- Use `impl Into<String>` for constructors that need to own a `String`
```rust
// GOOD — borrows when ownership isn't needed
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
// GOOD — takes ownership in constructor via Into
fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
// BAD — takes String when &str suffices
fn word_count_bad(text: String) -> usize {
text.split_whitespace().count()
}
```
## Error Handling
- Use `Result<T, E>` and `?` for propagation — never `unwrap()` in production code
- **Libraries**: define typed errors with `thiserror`
- **Applications**: use `anyhow` for flexible error context
- Add context with `.with_context(|| format!("failed to ..."))?`
- Reserve `unwrap()` / `expect()` for tests and truly unreachable states
```rust
// GOOD — library error with thiserror
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("failed to read config: {0}")]
Io(#[from] std::io::Error),
#[error("invalid config format: {0}")]
Parse(String),
}
// GOOD — application error with anyhow
use anyhow::Context;
fn load_config(path: &str) -> anyhow::Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {path}"))?;
toml::from_str(&content)
.with_context(|| format!("failed to parse {path}"))
}
```
## Iterators Over Loops
Prefer iterator chains for transformations; use loops for complex control flow:
```rust
// GOOD — declarative and composable
let active_emails: Vec<&str> = users.iter()
.filter(|u| u.is_active)
.map(|u| u.email.as_str())
.collect();
// GOOD — loop for complex logic with early returns
for user in &users {
if let Some(verified) = verify_email(&user.email)? {
send_welcome(&verified)?;
}
}
```
## Module Organization
Organize by domain, not by type:
```text
src/
├── main.rs
├── lib.rs
├── auth/ # Domain module
│ ├── mod.rs
│ ├── token.rs
│ └── middleware.rs
├── orders/ # Domain module
│ ├── mod.rs
│ ├── model.rs
│ └── service.rs
└── db/ # Infrastructure
├── mod.rs
└── pool.rs
```
## Visibility
- Default to private; use `pub(crate)` for internal sharing
- Only mark `pub` what is part of the crate's public API
- Re-export public API from `lib.rs`
## References
See skill: `rust-patterns` for comprehensive Rust idioms and patterns.

16
rules/rust/hooks.md Normal file
View File

@@ -0,0 +1,16 @@
---
paths:
- "**/*.rs"
- "**/Cargo.toml"
---
# Rust Hooks
> This file extends [common/hooks.md](../common/hooks.md) with Rust-specific content.
## PostToolUse Hooks
Configure in `~/.claude/settings.json`:
- **cargo fmt**: Auto-format `.rs` files after edit
- **cargo clippy**: Run lint checks after editing Rust files
- **cargo check**: Verify compilation after changes (faster than `cargo build`)

168
rules/rust/patterns.md Normal file
View File

@@ -0,0 +1,168 @@
---
paths:
- "**/*.rs"
---
# Rust Patterns
> This file extends [common/patterns.md](../common/patterns.md) with Rust-specific content.
## Repository Pattern with Traits
Encapsulate data access behind a trait:
```rust
pub trait OrderRepository: Send + Sync {
fn find_by_id(&self, id: u64) -> Result<Option<Order>, StorageError>;
fn find_all(&self) -> Result<Vec<Order>, StorageError>;
fn save(&self, order: &Order) -> Result<Order, StorageError>;
fn delete(&self, id: u64) -> Result<(), StorageError>;
}
```
Concrete implementations handle storage details (Postgres, SQLite, in-memory for tests).
## Service Layer
Business logic in service structs; inject dependencies via constructor:
```rust
pub struct OrderService {
repo: Box<dyn OrderRepository>,
payment: Box<dyn PaymentGateway>,
}
impl OrderService {
pub fn new(repo: Box<dyn OrderRepository>, payment: Box<dyn PaymentGateway>) -> Self {
Self { repo, payment }
}
pub fn place_order(&self, request: CreateOrderRequest) -> anyhow::Result<OrderSummary> {
let order = Order::from(request);
self.payment.charge(order.total())?;
let saved = self.repo.save(&order)?;
Ok(OrderSummary::from(saved))
}
}
```
## Newtype Pattern for Type Safety
Prevent argument mix-ups with distinct wrapper types:
```rust
struct UserId(u64);
struct OrderId(u64);
fn get_order(user: UserId, order: OrderId) -> anyhow::Result<Order> {
// Can't accidentally swap user and order IDs at call sites
todo!()
}
```
## Enum State Machines
Model states as enums — make illegal states unrepresentable:
```rust
enum ConnectionState {
Disconnected,
Connecting { attempt: u32 },
Connected { session_id: String },
Failed { reason: String, retries: u32 },
}
fn handle(state: &ConnectionState) {
match state {
ConnectionState::Disconnected => connect(),
ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
ConnectionState::Connecting { .. } => wait(),
ConnectionState::Connected { session_id } => use_session(session_id),
ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
ConnectionState::Failed { reason, .. } => log_failure(reason),
}
}
```
Always match exhaustively — no wildcard `_` for business-critical enums.
## Builder Pattern
Use for structs with many optional parameters:
```rust
pub struct ServerConfig {
host: String,
port: u16,
max_connections: usize,
}
impl ServerConfig {
pub fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
ServerConfigBuilder {
host: host.into(),
port,
max_connections: 100,
}
}
}
pub struct ServerConfigBuilder {
host: String,
port: u16,
max_connections: usize,
}
impl ServerConfigBuilder {
pub fn max_connections(mut self, n: usize) -> Self {
self.max_connections = n;
self
}
pub fn build(self) -> ServerConfig {
ServerConfig {
host: self.host,
port: self.port,
max_connections: self.max_connections,
}
}
}
```
## Sealed Traits for Extensibility Control
Use a private module to seal a trait, preventing external implementations:
```rust
mod private {
pub trait Sealed {}
}
pub trait Format: private::Sealed {
fn encode(&self, data: &[u8]) -> Vec<u8>;
}
pub struct Json;
impl private::Sealed for Json {}
impl Format for Json {
fn encode(&self, data: &[u8]) -> Vec<u8> { todo!() }
}
```
## API Response Envelope
Consistent API responses using a generic enum:
```rust
#[derive(Debug, serde::Serialize)]
#[serde(tag = "status")]
pub enum ApiResponse<T: serde::Serialize> {
#[serde(rename = "ok")]
Ok { data: T },
#[serde(rename = "error")]
Error { message: String },
}
```
## References
See skill: `rust-patterns` for comprehensive patterns including ownership, traits, generics, concurrency, and async.

141
rules/rust/security.md Normal file
View File

@@ -0,0 +1,141 @@
---
paths:
- "**/*.rs"
---
# Rust Security
> This file extends [common/security.md](../common/security.md) with Rust-specific content.
## Secrets Management
- Never hardcode API keys, tokens, or credentials in source code
- Use environment variables: `std::env::var("API_KEY")`
- Fail fast if required secrets are missing at startup
- Keep `.env` files in `.gitignore`
```rust
// BAD
const API_KEY: &str = "sk-abc123...";
// GOOD — environment variable with early validation
fn load_api_key() -> anyhow::Result<String> {
std::env::var("PAYMENT_API_KEY")
.context("PAYMENT_API_KEY must be set")
}
```
## SQL Injection Prevention
- Always use parameterized queries — never format user input into SQL strings
- Use query builder or ORM (sqlx, diesel, sea-orm) with bind parameters
```rust
// BAD — SQL injection via format string
let query = format!("SELECT * FROM users WHERE name = '{name}'");
sqlx::query(&query).fetch_one(&pool).await?;
// GOOD — parameterized query with sqlx
// Placeholder syntax varies by backend: Postgres: $1 | MySQL: ? | SQLite: $1
sqlx::query("SELECT * FROM users WHERE name = $1")
.bind(&name)
.fetch_one(&pool)
.await?;
```
## Input Validation
- Validate all user input at system boundaries before processing
- Use the type system to enforce invariants (newtype pattern)
- Parse, don't validate — convert unstructured data to typed structs at the boundary
- Reject invalid input with clear error messages
```rust
// Parse, don't validate — invalid states are unrepresentable
pub struct Email(String);
impl Email {
pub fn parse(input: &str) -> Result<Self, ValidationError> {
let trimmed = input.trim();
let at_pos = trimmed.find('@')
.filter(|&p| p > 0 && p < trimmed.len() - 1)
.ok_or_else(|| ValidationError::InvalidEmail(input.to_string()))?;
let domain = &trimmed[at_pos + 1..];
if trimmed.len() > 254 || !domain.contains('.') {
return Err(ValidationError::InvalidEmail(input.to_string()));
}
// For production use, prefer a validated email crate (e.g., `email_address`)
Ok(Self(trimmed.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
```
## Unsafe Code
- Minimize `unsafe` blocks — prefer safe abstractions
- Every `unsafe` block must have a `// SAFETY:` comment explaining the invariant
- Never use `unsafe` to bypass the borrow checker for convenience
- Audit all `unsafe` code during review — it is a red flag without justification
- Prefer `safe` FFI wrappers around C libraries
```rust
// GOOD — safety comment documents ALL required invariants
let widget: &Widget = {
// SAFETY: `ptr` is non-null, aligned, points to an initialized Widget,
// and no mutable references or mutations exist for its lifetime.
unsafe { &*ptr }
};
// BAD — no safety justification
unsafe { &*ptr }
```
## Dependency Security
- Run `cargo audit` to scan for known CVEs in dependencies
- Run `cargo deny check` for license and advisory compliance
- Use `cargo tree` to audit transitive dependencies
- Keep dependencies updated — set up Dependabot or Renovate
- Minimize dependency count — evaluate before adding new crates
```bash
# Security audit
cargo audit
# Deny advisories, duplicate versions, and restricted licenses
cargo deny check
# Inspect dependency tree
cargo tree
cargo tree -d # Show duplicates only
```
## Error Messages
- Never expose internal paths, stack traces, or database errors in API responses
- Log detailed errors server-side; return generic messages to clients
- Use `tracing` or `log` for structured server-side logging
```rust
// Map errors to appropriate status codes and generic messages
// (Example uses axum; adapt the response type to your framework)
match order_service.find_by_id(id) {
Ok(order) => Ok((StatusCode::OK, Json(order))),
Err(ServiceError::NotFound(_)) => {
tracing::info!(order_id = id, "order not found");
Err((StatusCode::NOT_FOUND, "Resource not found"))
}
Err(e) => {
tracing::error!(order_id = id, error = %e, "unexpected error");
Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"))
}
}
```
## References
See skill: `rust-patterns` for unsafe code guidelines and ownership patterns.
See skill: `security-review` for general security checklists.

154
rules/rust/testing.md Normal file
View File

@@ -0,0 +1,154 @@
---
paths:
- "**/*.rs"
---
# Rust Testing
> This file extends [common/testing.md](../common/testing.md) with Rust-specific content.
## Test Framework
- **`#[test]`** with `#[cfg(test)]` modules for unit tests
- **rstest** for parameterized tests and fixtures
- **proptest** for property-based testing
- **mockall** for trait-based mocking
- **`#[tokio::test]`** for async tests
## Test Organization
```text
my_crate/
├── src/
│ ├── lib.rs # Unit tests in #[cfg(test)] modules
│ ├── auth/
│ │ └── mod.rs # #[cfg(test)] mod tests { ... }
│ └── orders/
│ └── service.rs # #[cfg(test)] mod tests { ... }
├── tests/ # Integration tests (each file = separate binary)
│ ├── api_test.rs
│ ├── db_test.rs
│ └── common/ # Shared test utilities
│ └── mod.rs
└── benches/ # Criterion benchmarks
└── benchmark.rs
```
Unit tests go inside `#[cfg(test)]` modules in the same file. Integration tests go in `tests/`.
## Unit Test Pattern
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creates_user_with_valid_email() {
let user = User::new("Alice", "alice@example.com").unwrap();
assert_eq!(user.name, "Alice");
}
#[test]
fn rejects_invalid_email() {
let result = User::new("Bob", "not-an-email");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid email"));
}
}
```
## Parameterized Tests
```rust
use rstest::rstest;
#[rstest]
#[case("hello", 5)]
#[case("", 0)]
#[case("rust", 4)]
fn test_string_length(#[case] input: &str, #[case] expected: usize) {
assert_eq!(input.len(), expected);
}
```
## Async Tests
```rust
#[tokio::test]
async fn fetches_data_successfully() {
let client = TestClient::new().await;
let result = client.get("/data").await;
assert!(result.is_ok());
}
```
## Mocking with mockall
Define traits in production code; generate mocks in test modules:
```rust
// Production trait — pub so integration tests can import it
pub trait UserRepository {
fn find_by_id(&self, id: u64) -> Option<User>;
}
#[cfg(test)]
mod tests {
use super::*;
use mockall::predicate::eq;
mockall::mock! {
pub Repo {}
impl UserRepository for Repo {
fn find_by_id(&self, id: u64) -> Option<User>;
}
}
#[test]
fn service_returns_user_when_found() {
let mut mock = MockRepo::new();
mock.expect_find_by_id()
.with(eq(42))
.times(1)
.returning(|_| Some(User { id: 42, name: "Alice".into() }));
let service = UserService::new(Box::new(mock));
let user = service.get_user(42).unwrap();
assert_eq!(user.name, "Alice");
}
}
```
## Test Naming
Use descriptive names that explain the scenario:
- `creates_user_with_valid_email()`
- `rejects_order_when_insufficient_stock()`
- `returns_none_when_not_found()`
## Coverage
- Target 80%+ line coverage
- Use **cargo-llvm-cov** for coverage reporting
- Focus on business logic — exclude generated code and FFI bindings
```bash
cargo llvm-cov # Summary
cargo llvm-cov --html # HTML report
cargo llvm-cov --fail-under-lines 80 # Fail if below threshold
```
## Testing Commands
```bash
cargo test # Run all tests
cargo test -- --nocapture # Show println output
cargo test test_name # Run tests matching pattern
cargo test --lib # Unit tests only
cargo test --test api_test # Specific integration test (tests/api_test.rs)
cargo test --doc # Doc tests only
```
## References
See skill: `rust-testing` for comprehensive testing patterns including property-based testing, fixtures, and benchmarking with Criterion.

View File

@@ -26,7 +26,7 @@
"properties": {
"id": {
"type": "string",
"pattern": "^(baseline|lang|framework|capability):[a-z0-9-]+$"
"pattern": "^(baseline|lang|framework|capability|agent|skill):[a-z0-9-]+$"
},
"family": {
"type": "string",
@@ -34,7 +34,9 @@
"baseline",
"language",
"framework",
"capability"
"capability",
"agent",
"skill"
]
},
"description": {

View File

@@ -0,0 +1,280 @@
#!/usr/bin/env node
/**
* Governance Event Capture Hook
*
* PreToolUse/PostToolUse hook that detects governance-relevant events
* and writes them to the governance_events table in the state store.
*
* Captured event types:
* - secret_detected: Hardcoded secrets in tool input/output
* - policy_violation: Actions that violate configured policies
* - security_finding: Security-relevant tool invocations
* - approval_requested: Operations requiring explicit approval
*
* Enable: Set ECC_GOVERNANCE_CAPTURE=1
* Configure session: Set ECC_SESSION_ID for session correlation
*/
'use strict';
const crypto = require('crypto');
const MAX_STDIN = 1024 * 1024;
// Patterns that indicate potential hardcoded secrets
const SECRET_PATTERNS = [
{ name: 'aws_key', pattern: /(?:AKIA|ASIA)[A-Z0-9]{16}/i },
{ name: 'generic_secret', pattern: /(?:secret|password|token|api[_-]?key)\s*[:=]\s*["'][^"']{8,}/i },
{ name: 'private_key', pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/ },
{ name: 'jwt', pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
{ name: 'github_token', pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/ },
];
// Tool names that represent security-relevant operations
const SECURITY_RELEVANT_TOOLS = new Set([
'Bash', // Could execute arbitrary commands
]);
// Commands that require governance approval
const APPROVAL_COMMANDS = [
/git\s+push\s+.*--force/,
/git\s+reset\s+--hard/,
/rm\s+-rf?\s/,
/DROP\s+(?:TABLE|DATABASE)/i,
/DELETE\s+FROM\s+\w+\s*(?:;|$)/i,
];
// File patterns that indicate policy-sensitive paths
const SENSITIVE_PATHS = [
/\.env(?:\.|$)/,
/credentials/i,
/secrets?\./i,
/\.pem$/,
/\.key$/,
/id_rsa/,
];
/**
* Generate a unique event ID.
*/
function generateEventId() {
return `gov-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
}
/**
* Scan text content for hardcoded secrets.
* Returns array of { name, match } for each detected secret.
*/
function detectSecrets(text) {
if (!text || typeof text !== 'string') return [];
const findings = [];
for (const { name, pattern } of SECRET_PATTERNS) {
if (pattern.test(text)) {
findings.push({ name });
}
}
return findings;
}
/**
* Check if a command requires governance approval.
*/
function detectApprovalRequired(command) {
if (!command || typeof command !== 'string') return [];
const findings = [];
for (const pattern of APPROVAL_COMMANDS) {
if (pattern.test(command)) {
findings.push({ pattern: pattern.source });
}
}
return findings;
}
/**
* Check if a file path is policy-sensitive.
*/
function detectSensitivePath(filePath) {
if (!filePath || typeof filePath !== 'string') return false;
return SENSITIVE_PATHS.some(pattern => pattern.test(filePath));
}
/**
* Analyze a hook input payload and return governance events to capture.
*
* @param {Object} input - Parsed hook input (tool_name, tool_input, tool_output)
* @param {Object} [context] - Additional context (sessionId, hookPhase)
* @returns {Array<Object>} Array of governance event objects
*/
function analyzeForGovernanceEvents(input, context = {}) {
const events = [];
const toolName = input.tool_name || '';
const toolInput = input.tool_input || {};
const toolOutput = typeof input.tool_output === 'string' ? input.tool_output : '';
const sessionId = context.sessionId || null;
const hookPhase = context.hookPhase || 'unknown';
// 1. Secret detection in tool input content
const inputText = typeof toolInput === 'object'
? JSON.stringify(toolInput)
: String(toolInput);
const inputSecrets = detectSecrets(inputText);
const outputSecrets = detectSecrets(toolOutput);
const allSecrets = [...inputSecrets, ...outputSecrets];
if (allSecrets.length > 0) {
events.push({
id: generateEventId(),
sessionId,
eventType: 'secret_detected',
payload: {
toolName,
hookPhase,
secretTypes: allSecrets.map(s => s.name),
location: inputSecrets.length > 0 ? 'input' : 'output',
severity: 'critical',
},
resolvedAt: null,
resolution: null,
});
}
// 2. Approval-required commands (Bash only)
if (toolName === 'Bash') {
const command = toolInput.command || '';
const approvalFindings = detectApprovalRequired(command);
if (approvalFindings.length > 0) {
events.push({
id: generateEventId(),
sessionId,
eventType: 'approval_requested',
payload: {
toolName,
hookPhase,
command: command.slice(0, 200),
matchedPatterns: approvalFindings.map(f => f.pattern),
severity: 'high',
},
resolvedAt: null,
resolution: null,
});
}
}
// 3. Policy violation: writing to sensitive paths
const filePath = toolInput.file_path || toolInput.path || '';
if (filePath && detectSensitivePath(filePath)) {
events.push({
id: generateEventId(),
sessionId,
eventType: 'policy_violation',
payload: {
toolName,
hookPhase,
filePath: filePath.slice(0, 200),
reason: 'sensitive_file_access',
severity: 'warning',
},
resolvedAt: null,
resolution: null,
});
}
// 4. Security-relevant tool usage tracking
if (SECURITY_RELEVANT_TOOLS.has(toolName) && hookPhase === 'post') {
const command = toolInput.command || '';
const hasElevated = /sudo\s/.test(command) || /chmod\s/.test(command) || /chown\s/.test(command);
if (hasElevated) {
events.push({
id: generateEventId(),
sessionId,
eventType: 'security_finding',
payload: {
toolName,
hookPhase,
command: command.slice(0, 200),
reason: 'elevated_privilege_command',
severity: 'medium',
},
resolvedAt: null,
resolution: null,
});
}
}
return events;
}
/**
* Core hook logic — exported so run-with-flags.js can call directly.
*
* @param {string} rawInput - Raw JSON string from stdin
* @returns {string} The original input (pass-through)
*/
function run(rawInput) {
// Gate on feature flag
if (String(process.env.ECC_GOVERNANCE_CAPTURE || '').toLowerCase() !== '1') {
return rawInput;
}
try {
const input = JSON.parse(rawInput);
const sessionId = process.env.ECC_SESSION_ID || null;
const hookPhase = process.env.CLAUDE_HOOK_EVENT_NAME || 'unknown';
const events = analyzeForGovernanceEvents(input, {
sessionId,
hookPhase: hookPhase.startsWith('Pre') ? 'pre' : 'post',
});
if (events.length > 0) {
// Write events to stderr as JSON-lines for the caller to capture.
// The state store write is async and handled by a separate process
// to avoid blocking the hook pipeline.
for (const event of events) {
process.stderr.write(
`[governance] ${JSON.stringify(event)}\n`
);
}
}
} catch {
// Silently ignore parse errors — never block the tool pipeline.
}
return rawInput;
}
// ── stdin entry point ────────────────────────────────
if (require.main === module) {
let raw = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (raw.length < MAX_STDIN) {
const remaining = MAX_STDIN - raw.length;
raw += chunk.substring(0, remaining);
}
});
process.stdin.on('end', () => {
const result = run(raw);
process.stdout.write(result);
});
}
module.exports = {
APPROVAL_COMMANDS,
SECRET_PATTERNS,
SECURITY_RELEVANT_TOOLS,
SENSITIVE_PATHS,
analyzeForGovernanceEvents,
detectApprovalRequired,
detectSecrets,
detectSensitivePath,
generateEventId,
run,
};

View File

@@ -21,6 +21,7 @@ const {
readFile,
writeFile,
runCommand,
stripAnsi,
log
} = require('../lib/utils');
@@ -58,8 +59,9 @@ function extractSessionSummary(transcriptPath) {
: Array.isArray(rawContent)
? rawContent.map(c => (c && c.text) || '').join(' ')
: '';
if (text.trim()) {
userMessages.push(text.trim().slice(0, 200));
const cleaned = stripAnsi(text).trim();
if (cleaned) {
userMessages.push(cleaned.slice(0, 200));
}
}

View File

@@ -15,6 +15,7 @@ const {
findFiles,
ensureDir,
readFile,
stripAnsi,
log,
output
} = require('../lib/utils');
@@ -42,7 +43,8 @@ async function main() {
const content = readFile(latest.path);
if (content && !content.includes('[Session context goes here]')) {
// Only inject if the session has actual content (not the blank template)
output(`Previous session summary:\n${content}`);
// Strip ANSI escape codes that may have leaked from terminal output (#642)
output(`Previous session summary:\n${stripAnsi(content)}`);
}
}

View File

@@ -0,0 +1,230 @@
'use strict';
const fs = require('fs');
const path = require('path');
/**
* Parse YAML frontmatter from a markdown string.
* Returns { frontmatter: {}, body: string }.
*/
function parseFrontmatter(content) {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
if (!match) {
return { frontmatter: {}, body: content };
}
const frontmatter = {};
for (const line of match[1].split('\n')) {
const colonIdx = line.indexOf(':');
if (colonIdx === -1) continue;
const key = line.slice(0, colonIdx).trim();
let value = line.slice(colonIdx + 1).trim();
// Handle JSON arrays (e.g. tools: ["Read", "Grep"])
if (value.startsWith('[') && value.endsWith(']')) {
try {
value = JSON.parse(value);
} catch {
// keep as string
}
}
// Strip surrounding quotes
if (typeof value === 'string' && value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}
frontmatter[key] = value;
}
return { frontmatter, body: match[2] };
}
/**
* Extract the first meaningful paragraph from agent body as a summary.
* Skips headings and blank lines, returns up to maxSentences sentences.
*/
function extractSummary(body, maxSentences = 1) {
const lines = body.split('\n');
const paragraphs = [];
let current = [];
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === '') {
if (current.length > 0) {
paragraphs.push(current.join(' '));
current = [];
}
continue;
}
// Skip headings
if (trimmed.startsWith('#')) {
if (current.length > 0) {
paragraphs.push(current.join(' '));
current = [];
}
continue;
}
// Skip list items, code blocks, etc.
if (trimmed.startsWith('```') || trimmed.startsWith('- **') || trimmed.startsWith('|')) {
continue;
}
current.push(trimmed);
}
if (current.length > 0) {
paragraphs.push(current.join(' '));
}
// Find first non-empty paragraph
const firstParagraph = paragraphs.find(p => p.length > 0);
if (!firstParagraph) {
return '';
}
// Extract up to maxSentences sentences
const sentences = firstParagraph.match(/[^.!?]+[.!?]+/g) || [firstParagraph];
return sentences.slice(0, maxSentences).join(' ').trim();
}
/**
* Load and parse a single agent file.
* Returns the full agent object with frontmatter and body.
*/
function loadAgent(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const { frontmatter, body } = parseFrontmatter(content);
const fileName = path.basename(filePath, '.md');
return {
fileName,
name: frontmatter.name || fileName,
description: frontmatter.description || '',
tools: Array.isArray(frontmatter.tools) ? frontmatter.tools : [],
model: frontmatter.model || 'sonnet',
body,
byteSize: Buffer.byteLength(content, 'utf8'),
};
}
/**
* Load all agents from a directory.
*/
function loadAgents(agentsDir) {
if (!fs.existsSync(agentsDir)) {
return [];
}
return fs.readdirSync(agentsDir)
.filter(f => f.endsWith('.md'))
.sort()
.map(f => loadAgent(path.join(agentsDir, f)));
}
/**
* Compress an agent to its catalog entry (metadata only).
* This is the minimal representation needed for agent selection.
*/
function compressToCatalog(agent) {
return {
name: agent.name,
description: agent.description,
tools: agent.tools,
model: agent.model,
};
}
/**
* Compress an agent to a summary entry (metadata + first paragraph).
* More context than catalog, less than full body.
*/
function compressToSummary(agent) {
return {
name: agent.name,
description: agent.description,
tools: agent.tools,
model: agent.model,
summary: extractSummary(agent.body),
};
}
/**
* Build a full compressed catalog from a directory of agents.
*
* Modes:
* - 'catalog': name, description, tools, model only (~2-3k tokens for 27 agents)
* - 'summary': catalog + first paragraph summary (~4-5k tokens)
* - 'full': no compression, full body included
*
* Returns { agents: [], stats: { totalAgents, originalBytes, compressedTokenEstimate } }
*/
function buildAgentCatalog(agentsDir, options = {}) {
const mode = options.mode || 'catalog';
const filter = options.filter || null;
let agents = loadAgents(agentsDir);
if (typeof filter === 'function') {
agents = agents.filter(filter);
}
const originalBytes = agents.reduce((sum, a) => sum + a.byteSize, 0);
let compressed;
if (mode === 'catalog') {
compressed = agents.map(compressToCatalog);
} else if (mode === 'summary') {
compressed = agents.map(compressToSummary);
} else {
compressed = agents.map(a => ({
name: a.name,
description: a.description,
tools: a.tools,
model: a.model,
body: a.body,
}));
}
const compressedJson = JSON.stringify(compressed);
// Rough token estimate: ~4 chars per token for English text
const compressedTokenEstimate = Math.ceil(compressedJson.length / 4);
return {
agents: compressed,
stats: {
totalAgents: agents.length,
originalBytes,
compressedBytes: Buffer.byteLength(compressedJson, 'utf8'),
compressedTokenEstimate,
mode,
},
};
}
/**
* Lazy-load a single agent's full content by name from a directory.
* Returns null if not found.
*/
function lazyLoadAgent(agentsDir, agentName) {
const filePath = path.join(agentsDir, `${agentName}.md`);
if (!fs.existsSync(filePath)) {
return null;
}
return loadAgent(filePath);
}
module.exports = {
buildAgentCatalog,
compressToCatalog,
compressToSummary,
extractSummary,
lazyLoadAgent,
loadAgent,
loadAgents,
parseFrontmatter,
};

212
scripts/lib/inspection.js Normal file
View File

@@ -0,0 +1,212 @@
'use strict';
const DEFAULT_FAILURE_THRESHOLD = 3;
const DEFAULT_WINDOW_SIZE = 50;
const FAILURE_OUTCOMES = new Set(['failure', 'failed', 'error']);
/**
* Normalize a failure reason string for grouping.
* Strips timestamps, UUIDs, file paths, and numeric suffixes.
*/
function normalizeFailureReason(reason) {
if (!reason || typeof reason !== 'string') {
return 'unknown';
}
return reason
.trim()
.toLowerCase()
// Strip ISO timestamps (note: already lowercased, so t/z not T/Z)
.replace(/\d{4}-\d{2}-\d{2}[t ]\d{2}:\d{2}:\d{2}[.\dz]*/g, '<timestamp>')
// Strip UUIDs (already lowercased)
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, '<uuid>')
// Strip file paths
.replace(/\/[\w./-]+/g, '<path>')
// Collapse whitespace
.replace(/\s+/g, ' ')
.trim();
}
/**
* Group skill runs by skill ID and normalized failure reason.
*
* @param {Array} skillRuns - Array of skill run objects
* @returns {Map<string, { skillId: string, normalizedReason: string, runs: Array }>}
*/
function groupFailures(skillRuns) {
const groups = new Map();
for (const run of skillRuns) {
const outcome = String(run.outcome || '').toLowerCase();
if (!FAILURE_OUTCOMES.has(outcome)) {
continue;
}
const normalizedReason = normalizeFailureReason(run.failureReason);
const key = `${run.skillId}::${normalizedReason}`;
if (!groups.has(key)) {
groups.set(key, {
skillId: run.skillId,
normalizedReason,
runs: [],
});
}
groups.get(key).runs.push(run);
}
return groups;
}
/**
* Detect recurring failure patterns from skill runs.
*
* @param {Array} skillRuns - Array of skill run objects (newest first)
* @param {Object} [options]
* @param {number} [options.threshold=3] - Minimum failure count to trigger pattern detection
* @returns {Array<Object>} Array of detected patterns sorted by count descending
*/
function detectPatterns(skillRuns, options = {}) {
const threshold = options.threshold ?? DEFAULT_FAILURE_THRESHOLD;
const groups = groupFailures(skillRuns);
const patterns = [];
for (const [, group] of groups) {
if (group.runs.length < threshold) {
continue;
}
const sortedRuns = [...group.runs].sort(
(a, b) => (b.createdAt || '').localeCompare(a.createdAt || '')
);
const firstSeen = sortedRuns[sortedRuns.length - 1].createdAt || null;
const lastSeen = sortedRuns[0].createdAt || null;
const sessionIds = [...new Set(sortedRuns.map(r => r.sessionId).filter(Boolean))];
const versions = [...new Set(sortedRuns.map(r => r.skillVersion).filter(Boolean))];
// Collect unique raw failure reasons for this normalized group
const rawReasons = [...new Set(sortedRuns.map(r => r.failureReason).filter(Boolean))];
patterns.push({
skillId: group.skillId,
normalizedReason: group.normalizedReason,
count: group.runs.length,
firstSeen,
lastSeen,
sessionIds,
versions,
rawReasons,
runIds: sortedRuns.map(r => r.id),
});
}
// Sort by count descending, then by lastSeen descending
return patterns.sort((a, b) => {
if (b.count !== a.count) return b.count - a.count;
return (b.lastSeen || '').localeCompare(a.lastSeen || '');
});
}
/**
* Generate an inspection report from detected patterns.
*
* @param {Array} patterns - Output from detectPatterns()
* @param {Object} [options]
* @param {string} [options.generatedAt] - ISO timestamp for the report
* @returns {Object} Inspection report
*/
function generateReport(patterns, options = {}) {
const generatedAt = options.generatedAt || new Date().toISOString();
if (patterns.length === 0) {
return {
generatedAt,
status: 'clean',
patternCount: 0,
patterns: [],
summary: 'No recurring failure patterns detected.',
};
}
const totalFailures = patterns.reduce((sum, p) => sum + p.count, 0);
const affectedSkills = [...new Set(patterns.map(p => p.skillId))];
return {
generatedAt,
status: 'attention_needed',
patternCount: patterns.length,
totalFailures,
affectedSkills,
patterns: patterns.map(p => ({
skillId: p.skillId,
normalizedReason: p.normalizedReason,
count: p.count,
firstSeen: p.firstSeen,
lastSeen: p.lastSeen,
sessionIds: p.sessionIds,
versions: p.versions,
rawReasons: p.rawReasons.slice(0, 5),
suggestedAction: suggestAction(p),
})),
summary: `Found ${patterns.length} recurring failure pattern(s) across ${affectedSkills.length} skill(s) (${totalFailures} total failures).`,
};
}
/**
* Suggest a remediation action based on pattern characteristics.
*/
function suggestAction(pattern) {
const reason = pattern.normalizedReason;
if (reason.includes('timeout')) {
return 'Increase timeout or optimize skill execution time.';
}
if (reason.includes('permission') || reason.includes('denied') || reason.includes('auth')) {
return 'Check tool permissions and authentication configuration.';
}
if (reason.includes('not found') || reason.includes('missing')) {
return 'Verify required files/dependencies exist before skill execution.';
}
if (reason.includes('parse') || reason.includes('syntax') || reason.includes('json')) {
return 'Review input/output format expectations and add validation.';
}
if (pattern.versions.length > 1) {
return 'Failure spans multiple versions. Consider rollback to last stable version.';
}
return 'Investigate root cause and consider adding error handling.';
}
/**
* Run full inspection pipeline: query skill runs, detect patterns, generate report.
*
* @param {Object} store - State store instance with listRecentSessions, getSessionDetail
* @param {Object} [options]
* @param {number} [options.threshold] - Minimum failure count
* @param {number} [options.windowSize] - Number of recent skill runs to analyze
* @returns {Object} Inspection report
*/
function inspect(store, options = {}) {
const windowSize = options.windowSize ?? DEFAULT_WINDOW_SIZE;
const threshold = options.threshold ?? DEFAULT_FAILURE_THRESHOLD;
const status = store.getStatus({ recentSkillRunLimit: windowSize });
const skillRuns = status.skillRuns.recent || [];
const patterns = detectPatterns(skillRuns, { threshold });
return generateReport(patterns, { generatedAt: status.generatedAt });
}
module.exports = {
DEFAULT_FAILURE_THRESHOLD,
DEFAULT_WINDOW_SIZE,
detectPatterns,
generateReport,
groupFailures,
inspect,
normalizeFailureReason,
suggestAction,
};

View File

@@ -10,6 +10,8 @@ const COMPONENT_FAMILY_PREFIXES = {
language: 'lang:',
framework: 'framework:',
capability: 'capability:',
agent: 'agent:',
skill: 'skill:',
};
const LEGACY_COMPAT_BASE_MODULE_IDS_BY_TARGET = Object.freeze({
claude: [

View File

@@ -0,0 +1,89 @@
'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
/**
* Resolve the ECC source root directory.
*
* Tries, in order:
* 1. CLAUDE_PLUGIN_ROOT env var (set by Claude Code for hooks, or by user)
* 2. Standard install location (~/.claude/) — when scripts exist there
* 3. Plugin cache auto-detection — scans ~/.claude/plugins/cache/everything-claude-code/
* 4. Fallback to ~/.claude/ (original behaviour)
*
* @param {object} [options]
* @param {string} [options.homeDir] Override home directory (for testing)
* @param {string} [options.envRoot] Override CLAUDE_PLUGIN_ROOT (for testing)
* @param {string} [options.probe] Relative path used to verify a candidate root
* contains ECC scripts. Default: 'scripts/lib/utils.js'
* @returns {string} Resolved ECC root path
*/
function resolveEccRoot(options = {}) {
const envRoot = options.envRoot !== undefined
? options.envRoot
: (process.env.CLAUDE_PLUGIN_ROOT || '');
if (envRoot && envRoot.trim()) {
return envRoot.trim();
}
const homeDir = options.homeDir || os.homedir();
const claudeDir = path.join(homeDir, '.claude');
const probe = options.probe || path.join('scripts', 'lib', 'utils.js');
// Standard install — files are copied directly into ~/.claude/
if (fs.existsSync(path.join(claudeDir, probe))) {
return claudeDir;
}
// Plugin cache — Claude Code stores marketplace plugins under
// ~/.claude/plugins/cache/<plugin-name>/<org>/<version>/
try {
const cacheBase = path.join(claudeDir, 'plugins', 'cache', 'everything-claude-code');
const orgDirs = fs.readdirSync(cacheBase, { withFileTypes: true });
for (const orgEntry of orgDirs) {
if (!orgEntry.isDirectory()) continue;
const orgPath = path.join(cacheBase, orgEntry.name);
let versionDirs;
try {
versionDirs = fs.readdirSync(orgPath, { withFileTypes: true });
} catch {
continue;
}
for (const verEntry of versionDirs) {
if (!verEntry.isDirectory()) continue;
const candidate = path.join(orgPath, verEntry.name);
if (fs.existsSync(path.join(candidate, probe))) {
return candidate;
}
}
}
} catch {
// Plugin cache doesn't exist or isn't readable — continue to fallback
}
return claudeDir;
}
/**
* Compact inline version for embedding in command .md code blocks.
*
* This is the minified form of resolveEccRoot() suitable for use in
* node -e "..." scripts where require() is not available before the
* root is known.
*
* Usage in commands:
* const _r = <paste INLINE_RESOLVE>;
* const sm = require(_r + '/scripts/lib/session-manager');
*/
const INLINE_RESOLVE = `(()=>{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;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q)))return c}}catch(x){}return d})()`;
module.exports = {
resolveEccRoot,
INLINE_RESOLVE,
};

View File

@@ -464,6 +464,24 @@ function countInFile(filePath, pattern) {
return matches ? matches.length : 0;
}
/**
* Strip all ANSI escape sequences from a string.
*
* Handles:
* - CSI sequences: \x1b[ … <letter> (colors, cursor movement, erase, etc.)
* - OSC sequences: \x1b] … BEL/ST (window titles, hyperlinks)
* - Charset selection: \x1b(B
* - Bare ESC + single letter: \x1b <letter> (e.g. \x1bM for reverse index)
*
* @param {string} str - Input string possibly containing ANSI codes
* @returns {string} Cleaned string with all escape sequences removed
*/
function stripAnsi(str) {
if (typeof str !== 'string') return '';
// eslint-disable-next-line no-control-regex
return str.replace(/\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|\([A-Z]|[A-Z])/g, '');
}
/**
* Search for pattern in file and return matching lines with line numbers
*/
@@ -530,6 +548,9 @@ module.exports = {
countInFile,
grepFile,
// String sanitisation
stripAnsi,
// Hook I/O
readStdinJson,
log,

148
skills/agent-eval/SKILL.md Normal file
View File

@@ -0,0 +1,148 @@
---
name: agent-eval
description: Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics
origin: ECC
tools: Read, Write, Edit, Bash, Grep, Glob
---
# Agent Eval Skill
A lightweight CLI tool for comparing coding agents head-to-head on reproducible tasks. Every "which coding agent is best?" comparison runs on vibes — this tool systematizes it.
## When to Activate
- Comparing coding agents (Claude Code, Aider, Codex, etc.) on your own codebase
- Measuring agent performance before adopting a new tool or model
- Running regression checks when an agent updates its model or tooling
- Producing data-backed agent selection decisions for a team
## Installation
```bash
# pinned to v0.1.0 — latest stable commit
pip install git+https://github.com/joaquinhuigomez/agent-eval.git@6d062a2f5cda6ea443bf5d458d361892c04e749b
```
## Core Concepts
### YAML Task Definitions
Define tasks declaratively. Each task specifies what to do, which files to touch, and how to judge success:
```yaml
name: add-retry-logic
description: Add exponential backoff retry to the HTTP client
repo: ./my-project
files:
- src/http_client.py
prompt: |
Add retry logic with exponential backoff to all HTTP requests.
Max 3 retries. Initial delay 1s, max delay 30s.
judge:
- type: pytest
command: pytest tests/test_http_client.py -v
- type: grep
pattern: "exponential_backoff|retry"
files: src/http_client.py
commit: "abc1234" # pin to specific commit for reproducibility
```
### Git Worktree Isolation
Each agent run gets its own git worktree — no Docker required. This provides reproducibility isolation so agents cannot interfere with each other or corrupt the base repo.
### Metrics Collected
| Metric | What It Measures |
|--------|-----------------|
| Pass rate | Did the agent produce code that passes the judge? |
| Cost | API spend per task (when available) |
| Time | Wall-clock seconds to completion |
| Consistency | Pass rate across repeated runs (e.g., 3/3 = 100%) |
## Workflow
### 1. Define Tasks
Create a `tasks/` directory with YAML files, one per task:
```bash
mkdir tasks
# Write task definitions (see template above)
```
### 2. Run Agents
Execute agents against your tasks:
```bash
agent-eval run --task tasks/add-retry-logic.yaml --agent claude-code --agent aider --runs 3
```
Each run:
1. Creates a fresh git worktree from the specified commit
2. Hands the prompt to the agent
3. Runs the judge criteria
4. Records pass/fail, cost, and time
### 3. Compare Results
Generate a comparison report:
```bash
agent-eval report --format table
```
```
Task: add-retry-logic (3 runs each)
┌──────────────┬───────────┬────────┬────────┬─────────────┐
│ Agent │ Pass Rate │ Cost │ Time │ Consistency │
├──────────────┼───────────┼────────┼────────┼─────────────┤
│ claude-code │ 3/3 │ $0.12 │ 45s │ 100% │
│ aider │ 2/3 │ $0.08 │ 38s │ 67% │
└──────────────┴───────────┴────────┴────────┴─────────────┘
```
## Judge Types
### Code-Based (deterministic)
```yaml
judge:
- type: pytest
command: pytest tests/ -v
- type: command
command: npm run build
```
### Pattern-Based
```yaml
judge:
- type: grep
pattern: "class.*Retry"
files: src/**/*.py
```
### Model-Based (LLM-as-judge)
```yaml
judge:
- type: llm
prompt: |
Does this implementation correctly handle exponential backoff?
Check for: max retries, increasing delays, jitter.
```
## Best Practices
- **Start with 3-5 tasks** that represent your real workload, not toy examples
- **Run at least 3 trials** per agent to capture variance — agents are non-deterministic
- **Pin the commit** in your task YAML so results are reproducible across days/weeks
- **Include at least one deterministic judge** (tests, build) per task — LLM judges add noise
- **Track cost alongside pass rate** — a 95% agent at 10x the cost may not be the right choice
- **Version your task definitions** — they are test fixtures, treat them as code
## Links
- Repository: [github.com/joaquinhuigomez/agent-eval](https://github.com/joaquinhuigomez/agent-eval)

View File

@@ -0,0 +1,179 @@
---
name: architecture-decision-records
description: Capture architectural decisions made during Claude Code sessions as structured ADRs. Auto-detects decision moments, records context, alternatives considered, and rationale. Maintains an ADR log so future developers understand why the codebase is shaped the way it is.
origin: ECC
---
# Architecture Decision Records
Capture architectural decisions as they happen during coding sessions. Instead of decisions living only in Slack threads, PR comments, or someone's memory, this skill produces structured ADR documents that live alongside the code.
## When to Activate
- User explicitly says "let's record this decision" or "ADR this"
- User chooses between significant alternatives (framework, library, pattern, database, API design)
- User says "we decided to..." or "the reason we're doing X instead of Y is..."
- User asks "why did we choose X?" (read existing ADRs)
- During planning phases when architectural trade-offs are discussed
## ADR Format
Use the lightweight ADR format proposed by Michael Nygard, adapted for AI-assisted development:
```markdown
# ADR-NNNN: [Decision Title]
**Date**: YYYY-MM-DD
**Status**: proposed | accepted | deprecated | superseded by ADR-NNNN
**Deciders**: [who was involved]
## Context
What is the issue that we're seeing that is motivating this decision or change?
[2-5 sentences describing the situation, constraints, and forces at play]
## Decision
What is the change that we're proposing and/or doing?
[1-3 sentences stating the decision clearly]
## Alternatives Considered
### Alternative 1: [Name]
- **Pros**: [benefits]
- **Cons**: [drawbacks]
- **Why not**: [specific reason this was rejected]
### Alternative 2: [Name]
- **Pros**: [benefits]
- **Cons**: [drawbacks]
- **Why not**: [specific reason this was rejected]
## Consequences
What becomes easier or more difficult to do because of this change?
### Positive
- [benefit 1]
- [benefit 2]
### Negative
- [trade-off 1]
- [trade-off 2]
### Risks
- [risk and mitigation]
```
## Workflow
### Capturing a New ADR
When a decision moment is detected:
1. **Initialize (first time only)** — if `docs/adr/` does not exist, ask the user for confirmation before creating the directory, a `README.md` seeded with the index table header (see ADR Index Format below), and a blank `template.md` for manual use. Do not create files without explicit consent.
2. **Identify the decision** — extract the core architectural choice being made
3. **Gather context** — what problem prompted this? What constraints exist?
4. **Document alternatives** — what other options were considered? Why were they rejected?
5. **State consequences** — what are the trade-offs? What becomes easier/harder?
6. **Assign a number** — scan existing ADRs in `docs/adr/` and increment
7. **Confirm and write** — present the draft ADR to the user for review. Only write to `docs/adr/NNNN-decision-title.md` after explicit approval. If the user declines, discard the draft without writing any files.
8. **Update the index** — append to `docs/adr/README.md`
### Reading Existing ADRs
When a user asks "why did we choose X?":
1. Check if `docs/adr/` exists — if not, respond: "No ADRs found in this project. Would you like to start recording architectural decisions?"
2. If it exists, scan `docs/adr/README.md` index for relevant entries
3. Read matching ADR files and present the Context and Decision sections
4. If no match is found, respond: "No ADR found for that decision. Would you like to record one now?"
### ADR Directory Structure
```
docs/
└── adr/
├── README.md ← index of all ADRs
├── 0001-use-nextjs.md
├── 0002-postgres-over-mongo.md
├── 0003-rest-over-graphql.md
└── template.md ← blank template for manual use
```
### ADR Index Format
```markdown
# Architecture Decision Records
| ADR | Title | Status | Date |
|-----|-------|--------|------|
| [0001](0001-use-nextjs.md) | Use Next.js as frontend framework | accepted | 2026-01-15 |
| [0002](0002-postgres-over-mongo.md) | PostgreSQL over MongoDB for primary datastore | accepted | 2026-01-20 |
| [0003](0003-rest-over-graphql.md) | REST API over GraphQL | accepted | 2026-02-01 |
```
## Decision Detection Signals
Watch for these patterns in conversation that indicate an architectural decision:
**Explicit signals**
- "Let's go with X"
- "We should use X instead of Y"
- "The trade-off is worth it because..."
- "Record this as an ADR"
**Implicit signals** (suggest recording an ADR — do not auto-create without user confirmation)
- Comparing two frameworks or libraries and reaching a conclusion
- Making a database schema design choice with stated rationale
- Choosing between architectural patterns (monolith vs microservices, REST vs GraphQL)
- Deciding on authentication/authorization strategy
- Selecting deployment infrastructure after evaluating alternatives
## What Makes a Good ADR
### Do
- **Be specific** — "Use Prisma ORM" not "use an ORM"
- **Record the why** — the rationale matters more than the what
- **Include rejected alternatives** — future developers need to know what was considered
- **State consequences honestly** — every decision has trade-offs
- **Keep it short** — an ADR should be readable in 2 minutes
- **Use present tense** — "We use X" not "We will use X"
### Don't
- Record trivial decisions — variable naming or formatting choices don't need ADRs
- Write essays — if the context section exceeds 10 lines, it's too long
- Omit alternatives — "we just picked it" is not a valid rationale
- Backfill without marking it — if recording a past decision, note the original date
- Let ADRs go stale — superseded decisions should reference their replacement
## ADR Lifecycle
```
proposed → accepted → [deprecated | superseded by ADR-NNNN]
```
- **proposed**: decision is under discussion, not yet committed
- **accepted**: decision is in effect and being followed
- **deprecated**: decision is no longer relevant (e.g., feature removed)
- **superseded**: a newer ADR replaces this one (always link the replacement)
## Categories of Decisions Worth Recording
| Category | Examples |
|----------|---------|
| **Technology choices** | Framework, language, database, cloud provider |
| **Architecture patterns** | Monolith vs microservices, event-driven, CQRS |
| **API design** | REST vs GraphQL, versioning strategy, auth mechanism |
| **Data modeling** | Schema design, normalization decisions, caching strategy |
| **Infrastructure** | Deployment model, CI/CD pipeline, monitoring stack |
| **Security** | Auth strategy, encryption approach, secret management |
| **Testing** | Test framework, coverage targets, E2E vs integration balance |
| **Process** | Branching strategy, review process, release cadence |
## Integration with Other Skills
- **Planner agent**: when the planner proposes architecture changes, suggest creating an ADR
- **Code reviewer agent**: flag PRs that introduce architectural changes without a corresponding ADR

View File

@@ -0,0 +1,233 @@
---
name: codebase-onboarding
description: Analyze an unfamiliar codebase and generate a structured onboarding guide with architecture map, key entry points, conventions, and a starter CLAUDE.md. Use when joining a new project or setting up Claude Code for the first time in a repo.
origin: ECC
---
# Codebase Onboarding
Systematically analyze an unfamiliar codebase and produce a structured onboarding guide. Designed for developers joining a new project or setting up Claude Code in an existing repo for the first time.
## When to Use
- First time opening a project with Claude Code
- Joining a new team or repository
- User asks "help me understand this codebase"
- User asks to generate a CLAUDE.md for a project
- User says "onboard me" or "walk me through this repo"
## How It Works
### Phase 1: Reconnaissance
Gather raw signals about the project without reading every file. Run these checks in parallel:
```
1. Package manifest detection
→ package.json, go.mod, Cargo.toml, pyproject.toml, pom.xml, build.gradle,
Gemfile, composer.json, mix.exs, pubspec.yaml
2. Framework fingerprinting
→ next.config.*, nuxt.config.*, angular.json, vite.config.*,
django settings, flask app factory, fastapi main, rails config
3. Entry point identification
→ main.*, index.*, app.*, server.*, cmd/, src/main/
4. Directory structure snapshot
→ Top 2 levels of the directory tree, ignoring node_modules, vendor,
.git, dist, build, __pycache__, .next
5. Config and tooling detection
→ .eslintrc*, .prettierrc*, tsconfig.json, Makefile, Dockerfile,
docker-compose*, .github/workflows/, .env.example, CI configs
6. Test structure detection
→ tests/, test/, __tests__/, *_test.go, *.spec.ts, *.test.js,
pytest.ini, jest.config.*, vitest.config.*
```
### Phase 2: Architecture Mapping
From the reconnaissance data, identify:
**Tech Stack**
- Language(s) and version constraints
- Framework(s) and major libraries
- Database(s) and ORMs
- Build tools and bundlers
- CI/CD platform
**Architecture Pattern**
- Monolith, monorepo, microservices, or serverless
- Frontend/backend split or full-stack
- API style: REST, GraphQL, gRPC, tRPC
**Key Directories**
Map the top-level directories to their purpose:
<!-- Example for a React project — replace with detected directories -->
```
src/components/ → React UI components
src/api/ → API route handlers
src/lib/ → Shared utilities
src/db/ → Database models and migrations
tests/ → Test suites
scripts/ → Build and deployment scripts
```
**Data Flow**
Trace one request from entry to response:
- Where does a request enter? (router, handler, controller)
- How is it validated? (middleware, schemas, guards)
- Where is business logic? (services, models, use cases)
- How does it reach the database? (ORM, raw queries, repositories)
### Phase 3: Convention Detection
Identify patterns the codebase already follows:
**Naming Conventions**
- File naming: kebab-case, camelCase, PascalCase, snake_case
- Component/class naming patterns
- Test file naming: `*.test.ts`, `*.spec.ts`, `*_test.go`
**Code Patterns**
- Error handling style: try/catch, Result types, error codes
- Dependency injection or direct imports
- State management approach
- Async patterns: callbacks, promises, async/await, channels
**Git Conventions**
- Branch naming from recent branches
- Commit message style from recent commits
- PR workflow (squash, merge, rebase)
- If the repo has no commits yet or only a shallow history (e.g. `git clone --depth 1`), skip this section and note "Git history unavailable or too shallow to detect conventions"
### Phase 4: Generate Onboarding Artifacts
Produce two outputs:
#### Output 1: Onboarding Guide
```markdown
# Onboarding Guide: [Project Name]
## Overview
[2-3 sentences: what this project does and who it serves]
## Tech Stack
<!-- Example for a Next.js project — replace with detected stack -->
| Layer | Technology | Version |
|-------|-----------|---------|
| Language | TypeScript | 5.x |
| Framework | Next.js | 14.x |
| Database | PostgreSQL | 16 |
| ORM | Prisma | 5.x |
| Testing | Jest + Playwright | - |
## Architecture
[Diagram or description of how components connect]
## Key Entry Points
<!-- Example for a Next.js project — replace with detected paths -->
- **API routes**: `src/app/api/` — Next.js route handlers
- **UI pages**: `src/app/(dashboard)/` — authenticated pages
- **Database**: `prisma/schema.prisma` — data model source of truth
- **Config**: `next.config.ts` — build and runtime config
## Directory Map
[Top-level directory → purpose mapping]
## Request Lifecycle
[Trace one API request from entry to response]
## Conventions
- [File naming pattern]
- [Error handling approach]
- [Testing patterns]
- [Git workflow]
## Common Tasks
<!-- Example for a Node.js project — replace with detected commands -->
- **Run dev server**: `npm run dev`
- **Run tests**: `npm test`
- **Run linter**: `npm run lint`
- **Database migrations**: `npx prisma migrate dev`
- **Build for production**: `npm run build`
## Where to Look
<!-- Example for a Next.js project — replace with detected paths -->
| I want to... | Look at... |
|--------------|-----------|
| Add an API endpoint | `src/app/api/` |
| Add a UI page | `src/app/(dashboard)/` |
| Add a database table | `prisma/schema.prisma` |
| Add a test | `tests/` matching the source path |
| Change build config | `next.config.ts` |
```
#### Output 2: Starter CLAUDE.md
Generate or update a project-specific CLAUDE.md based on detected conventions. If `CLAUDE.md` already exists, read it first and enhance it — preserve existing project-specific instructions and clearly call out what was added or changed.
```markdown
# Project Instructions
## Tech Stack
[Detected stack summary]
## Code Style
- [Detected naming conventions]
- [Detected patterns to follow]
## Testing
- Run tests: `[detected test command]`
- Test pattern: [detected test file convention]
- Coverage: [if configured, the coverage command]
## Build & Run
- Dev: `[detected dev command]`
- Build: `[detected build command]`
- Lint: `[detected lint command]`
## Project Structure
[Key directory → purpose map]
## Conventions
- [Commit style if detectable]
- [PR workflow if detectable]
- [Error handling patterns]
```
## Best Practices
1. **Don't read everything** — reconnaissance should use Glob and Grep, not Read on every file. Read selectively only for ambiguous signals.
2. **Verify, don't guess** — if a framework is detected from config but the actual code uses something different, trust the code.
3. **Respect existing CLAUDE.md** — if one already exists, enhance it rather than replacing it. Call out what's new vs existing.
4. **Stay concise** — the onboarding guide should be scannable in 2 minutes. Details belong in the code, not the guide.
5. **Flag unknowns** — if a convention can't be confidently detected, say so rather than guessing. "Could not determine test runner" is better than a wrong answer.
## Anti-Patterns to Avoid
- Generating a CLAUDE.md that's longer than 100 lines — keep it focused
- Listing every dependency — highlight only the ones that shape how you write code
- Describing obvious directory names — `src/` doesn't need an explanation
- Copying the README — the onboarding guide adds structural insight the README lacks
## Examples
### Example 1: First time in a new repo
**User**: "Onboard me to this codebase"
**Action**: Run full 4-phase workflow → produce Onboarding Guide + Starter CLAUDE.md
**Output**: Onboarding Guide printed directly to the conversation, plus a `CLAUDE.md` written to the project root
### Example 2: Generate CLAUDE.md for existing project
**User**: "Generate a CLAUDE.md for this project"
**Action**: Run Phases 1-3, skip Onboarding Guide, produce only CLAUDE.md
**Output**: Project-specific `CLAUDE.md` with detected conventions
### Example 3: Enhance existing CLAUDE.md
**User**: "Update the CLAUDE.md with current project conventions"
**Action**: Read existing CLAUDE.md, run Phases 1-3, merge new findings
**Output**: Updated `CLAUDE.md` with additions clearly marked

View File

@@ -0,0 +1,135 @@
---
name: context-budget
description: Audits Claude Code context window consumption across agents, skills, MCP servers, and rules. Identifies bloat, redundant components, and produces prioritized token-savings recommendations.
origin: ECC
---
# Context Budget
Analyze token overhead across every loaded component in a Claude Code session and surface actionable optimizations to reclaim context space.
## When to Use
- Session performance feels sluggish or output quality is degrading
- You've recently added many skills, agents, or MCP servers
- You want to know how much context headroom you actually have
- Planning to add more components and need to know if there's room
- Running `/context-budget` command (this skill backs it)
## How It Works
### Phase 1: Inventory
Scan all component directories and estimate token consumption:
**Agents** (`agents/*.md`)
- Count lines and tokens per file (words × 1.3)
- Extract `description` frontmatter length
- Flag: files >200 lines (heavy), description >30 words (bloated frontmatter)
**Skills** (`skills/*/SKILL.md`)
- Count tokens per SKILL.md
- Flag: files >400 lines
- Check for duplicate copies in `.agents/skills/` — skip identical copies to avoid double-counting
**Rules** (`rules/**/*.md`)
- Count tokens per file
- Flag: files >100 lines
- Detect content overlap between rule files in the same language module
**MCP Servers** (`.mcp.json` or active MCP config)
- Count configured servers and total tool count
- Estimate schema overhead at ~500 tokens per tool
- Flag: servers with >20 tools, servers that wrap simple CLI commands (`gh`, `git`, `npm`, `supabase`, `vercel`)
**CLAUDE.md** (project + user-level)
- Count tokens per file in the CLAUDE.md chain
- Flag: combined total >300 lines
### Phase 2: Classify
Sort every component into a bucket:
| Bucket | Criteria | Action |
|--------|----------|--------|
| **Always needed** | Referenced in CLAUDE.md, backs an active command, or matches current project type | Keep |
| **Sometimes needed** | Domain-specific (e.g. language patterns), not referenced in CLAUDE.md | Consider on-demand activation |
| **Rarely needed** | No command reference, overlapping content, or no obvious project match | Remove or lazy-load |
### Phase 3: Detect Issues
Identify the following problem patterns:
- **Bloated agent descriptions** — description >30 words in frontmatter loads into every Task tool invocation
- **Heavy agents** — files >200 lines inflate Task tool context on every spawn
- **Redundant components** — skills that duplicate agent logic, rules that duplicate CLAUDE.md
- **MCP over-subscription** — >10 servers, or servers wrapping CLI tools available for free
- **CLAUDE.md bloat** — verbose explanations, outdated sections, instructions that should be rules
### Phase 4: Report
Produce the context budget report:
```
Context Budget Report
═══════════════════════════════════════
Total estimated overhead: ~XX,XXX tokens
Context model: Claude Sonnet (200K window)
Effective available context: ~XXX,XXX tokens (XX%)
Component Breakdown:
┌─────────────────┬────────┬───────────┐
│ Component │ Count │ Tokens │
├─────────────────┼────────┼───────────┤
│ Agents │ N │ ~X,XXX │
│ Skills │ N │ ~X,XXX │
│ Rules │ N │ ~X,XXX │
│ MCP tools │ N │ ~XX,XXX │
│ CLAUDE.md │ N │ ~X,XXX │
└─────────────────┴────────┴───────────┘
⚠ Issues Found (N):
[ranked by token savings]
Top 3 Optimizations:
1. [action] → save ~X,XXX tokens
2. [action] → save ~X,XXX tokens
3. [action] → save ~X,XXX tokens
Potential savings: ~XX,XXX tokens (XX% of current overhead)
```
In verbose mode, additionally output per-file token counts, line-by-line breakdown of the heaviest files, specific redundant lines between overlapping components, and MCP tool list with per-tool schema size estimates.
## Examples
**Basic audit**
```
User: /context-budget
Skill: Scans setup → 16 agents (12,400 tokens), 28 skills (6,200), 87 MCP tools (43,500), 2 CLAUDE.md (1,200)
Flags: 3 heavy agents, 14 MCP servers (3 CLI-replaceable)
Top saving: remove 3 MCP servers → -27,500 tokens (47% overhead reduction)
```
**Verbose mode**
```
User: /context-budget --verbose
Skill: Full report + per-file breakdown showing planner.md (213 lines, 1,840 tokens),
MCP tool list with per-tool sizes, duplicated rule lines side by side
```
**Pre-expansion check**
```
User: I want to add 5 more MCP servers, do I have room?
Skill: Current overhead 33% → adding 5 servers (~50 tools) would add ~25,000 tokens → pushes to 45% overhead
Recommendation: remove 2 CLI-replaceable servers first to stay under 40%
```
## Best Practices
- **Token estimation**: use `words × 1.3` for prose, `chars / 4` for code-heavy files
- **MCP is the biggest lever**: each tool schema costs ~500 tokens; a 30-tool server costs more than all your skills combined
- **Agent descriptions are loaded always**: even if the agent is never invoked, its description field is present in every Task tool context
- **Verbose mode for debugging**: use when you need to pinpoint the exact files driving overhead, not for regular audits
- **Audit after changes**: run after adding any agent, skill, or MCP server to catch creep early

View File

@@ -114,7 +114,9 @@ PROMPT
fi
# Prevent observe.sh from recording this automated Haiku session as observations
ECC_SKIP_OBSERVE=1 ECC_HOOK_PROFILE=minimal claude --model haiku --max-turns "$max_turns" --print < "$prompt_file" >> "$LOG_FILE" 2>&1 &
ECC_SKIP_OBSERVE=1 ECC_HOOK_PROFILE=minimal claude --model haiku --max-turns "$max_turns" --print \
--allowedTools "Read,Write" \
< "$prompt_file" >> "$LOG_FILE" 2>&1 &
claude_pid=$!
(

View File

@@ -97,8 +97,11 @@ fi
# - automated sessions creating project-scoped homunculus metadata
# Layer 1: entrypoint. Only interactive terminal sessions should continue.
# sdk-ts: Agent SDK sessions can be human-interactive (e.g. via Happy).
# Non-interactive SDK automation is still filtered by Layers 2-5 below
# (ECC_HOOK_PROFILE=minimal, ECC_SKIP_OBSERVE=1, agent_id, path exclusions).
case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in
cli) ;;
cli|sdk-ts) ;;
*) exit 0 ;;
esac

View File

@@ -85,7 +85,7 @@ _clv2_detect_project() {
# fall back to path hash (machine-specific but still useful)
local remote_url=""
if command -v git &>/dev/null; then
if [ "$source_hint" = "git" ] || [ -d "${project_root}/.git" ]; then
if [ "$source_hint" = "git" ] || [ -e "${project_root}/.git" ]; then
remote_url=$(git -C "$project_root" remote get-url origin 2>/dev/null || true)
fi
fi

View File

@@ -0,0 +1,396 @@
---
name: pytorch-patterns
description: PyTorch deep learning patterns and best practices for building robust, efficient, and reproducible training pipelines, model architectures, and data loading.
origin: ECC
---
# PyTorch Development Patterns
Idiomatic PyTorch patterns and best practices for building robust, efficient, and reproducible deep learning applications.
## When to Activate
- Writing new PyTorch models or training scripts
- Reviewing deep learning code
- Debugging training loops or data pipelines
- Optimizing GPU memory usage or training speed
- Setting up reproducible experiments
## Core Principles
### 1. Device-Agnostic Code
Always write code that works on both CPU and GPU without hardcoding devices.
```python
# Good: Device-agnostic
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MyModel().to(device)
data = data.to(device)
# Bad: Hardcoded device
model = MyModel().cuda() # Crashes if no GPU
data = data.cuda()
```
### 2. Reproducibility First
Set all random seeds for reproducible results.
```python
# Good: Full reproducibility setup
def set_seed(seed: int = 42) -> None:
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)
random.seed(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# Bad: No seed control
model = MyModel() # Different weights every run
```
### 3. Explicit Shape Management
Always document and verify tensor shapes.
```python
# Good: Shape-annotated forward pass
def forward(self, x: torch.Tensor) -> torch.Tensor:
# x: (batch_size, channels, height, width)
x = self.conv1(x) # -> (batch_size, 32, H, W)
x = self.pool(x) # -> (batch_size, 32, H//2, W//2)
x = x.view(x.size(0), -1) # -> (batch_size, 32*H//2*W//2)
return self.fc(x) # -> (batch_size, num_classes)
# Bad: No shape tracking
def forward(self, x):
x = self.conv1(x)
x = self.pool(x)
x = x.view(x.size(0), -1) # What size is this?
return self.fc(x) # Will this even work?
```
## Model Architecture Patterns
### Clean nn.Module Structure
```python
# Good: Well-organized module
class ImageClassifier(nn.Module):
def __init__(self, num_classes: int, dropout: float = 0.5) -> None:
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Dropout(dropout),
nn.Linear(64 * 16 * 16, num_classes),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.features(x)
x = x.view(x.size(0), -1)
return self.classifier(x)
# Bad: Everything in forward
class ImageClassifier(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x):
x = F.conv2d(x, weight=self.make_weight()) # Creates weight each call!
return x
```
### Proper Weight Initialization
```python
# Good: Explicit initialization
def _init_weights(self, module: nn.Module) -> None:
if isinstance(module, nn.Linear):
nn.init.kaiming_normal_(module.weight, mode="fan_out", nonlinearity="relu")
if module.bias is not None:
nn.init.zeros_(module.bias)
elif isinstance(module, nn.Conv2d):
nn.init.kaiming_normal_(module.weight, mode="fan_out", nonlinearity="relu")
elif isinstance(module, nn.BatchNorm2d):
nn.init.ones_(module.weight)
nn.init.zeros_(module.bias)
model = MyModel()
model.apply(model._init_weights)
```
## Training Loop Patterns
### Standard Training Loop
```python
# Good: Complete training loop with best practices
def train_one_epoch(
model: nn.Module,
dataloader: DataLoader,
optimizer: torch.optim.Optimizer,
criterion: nn.Module,
device: torch.device,
scaler: torch.amp.GradScaler | None = None,
) -> float:
model.train() # Always set train mode
total_loss = 0.0
for batch_idx, (data, target) in enumerate(dataloader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad(set_to_none=True) # More efficient than zero_grad()
# Mixed precision training
with torch.amp.autocast("cuda", enabled=scaler is not None):
output = model(data)
loss = criterion(output, target)
if scaler is not None:
scaler.scale(loss).backward()
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
scaler.step(optimizer)
scaler.update()
else:
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
total_loss += loss.item()
return total_loss / len(dataloader)
```
### Validation Loop
```python
# Good: Proper evaluation
@torch.no_grad() # More efficient than wrapping in torch.no_grad() block
def evaluate(
model: nn.Module,
dataloader: DataLoader,
criterion: nn.Module,
device: torch.device,
) -> tuple[float, float]:
model.eval() # Always set eval mode — disables dropout, uses running BN stats
total_loss = 0.0
correct = 0
total = 0
for data, target in dataloader:
data, target = data.to(device), target.to(device)
output = model(data)
total_loss += criterion(output, target).item()
correct += (output.argmax(1) == target).sum().item()
total += target.size(0)
return total_loss / len(dataloader), correct / total
```
## Data Pipeline Patterns
### Custom Dataset
```python
# Good: Clean Dataset with type hints
class ImageDataset(Dataset):
def __init__(
self,
image_dir: str,
labels: dict[str, int],
transform: transforms.Compose | None = None,
) -> None:
self.image_paths = list(Path(image_dir).glob("*.jpg"))
self.labels = labels
self.transform = transform
def __len__(self) -> int:
return len(self.image_paths)
def __getitem__(self, idx: int) -> tuple[torch.Tensor, int]:
img = Image.open(self.image_paths[idx]).convert("RGB")
label = self.labels[self.image_paths[idx].stem]
if self.transform:
img = self.transform(img)
return img, label
```
### Efficient DataLoader Configuration
```python
# Good: Optimized DataLoader
dataloader = DataLoader(
dataset,
batch_size=32,
shuffle=True, # Shuffle for training
num_workers=4, # Parallel data loading
pin_memory=True, # Faster CPU->GPU transfer
persistent_workers=True, # Keep workers alive between epochs
drop_last=True, # Consistent batch sizes for BatchNorm
)
# Bad: Slow defaults
dataloader = DataLoader(dataset, batch_size=32) # num_workers=0, no pin_memory
```
### Custom Collate for Variable-Length Data
```python
# Good: Pad sequences in collate_fn
def collate_fn(batch: list[tuple[torch.Tensor, int]]) -> tuple[torch.Tensor, torch.Tensor]:
sequences, labels = zip(*batch)
# Pad to max length in batch
padded = nn.utils.rnn.pad_sequence(sequences, batch_first=True, padding_value=0)
return padded, torch.tensor(labels)
dataloader = DataLoader(dataset, batch_size=32, collate_fn=collate_fn)
```
## Checkpointing Patterns
### Save and Load Checkpoints
```python
# Good: Complete checkpoint with all training state
def save_checkpoint(
model: nn.Module,
optimizer: torch.optim.Optimizer,
epoch: int,
loss: float,
path: str,
) -> None:
torch.save({
"epoch": epoch,
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"loss": loss,
}, path)
def load_checkpoint(
path: str,
model: nn.Module,
optimizer: torch.optim.Optimizer | None = None,
) -> dict:
checkpoint = torch.load(path, map_location="cpu", weights_only=True)
model.load_state_dict(checkpoint["model_state_dict"])
if optimizer:
optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
return checkpoint
# Bad: Only saving model weights (can't resume training)
torch.save(model.state_dict(), "model.pt")
```
## Performance Optimization
### Mixed Precision Training
```python
# Good: AMP with GradScaler
scaler = torch.amp.GradScaler("cuda")
for data, target in dataloader:
with torch.amp.autocast("cuda"):
output = model(data)
loss = criterion(output, target)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad(set_to_none=True)
```
### Gradient Checkpointing for Large Models
```python
# Good: Trade compute for memory
from torch.utils.checkpoint import checkpoint
class LargeModel(nn.Module):
def forward(self, x: torch.Tensor) -> torch.Tensor:
# Recompute activations during backward to save memory
x = checkpoint(self.block1, x, use_reentrant=False)
x = checkpoint(self.block2, x, use_reentrant=False)
return self.head(x)
```
### torch.compile for Speed
```python
# Good: Compile the model for faster execution (PyTorch 2.0+)
model = MyModel().to(device)
model = torch.compile(model, mode="reduce-overhead")
# Modes: "default" (safe), "reduce-overhead" (faster), "max-autotune" (fastest)
```
## Quick Reference: PyTorch Idioms
| Idiom | Description |
|-------|-------------|
| `model.train()` / `model.eval()` | Always set mode before train/eval |
| `torch.no_grad()` | Disable gradients for inference |
| `optimizer.zero_grad(set_to_none=True)` | More efficient gradient clearing |
| `.to(device)` | Device-agnostic tensor/model placement |
| `torch.amp.autocast` | Mixed precision for 2x speed |
| `pin_memory=True` | Faster CPU→GPU data transfer |
| `torch.compile` | JIT compilation for speed (2.0+) |
| `weights_only=True` | Secure model loading |
| `torch.manual_seed` | Reproducible experiments |
| `gradient_checkpointing` | Trade compute for memory |
## Anti-Patterns to Avoid
```python
# Bad: Forgetting model.eval() during validation
model.train()
with torch.no_grad():
output = model(val_data) # Dropout still active! BatchNorm uses batch stats!
# Good: Always set eval mode
model.eval()
with torch.no_grad():
output = model(val_data)
# Bad: In-place operations breaking autograd
x = F.relu(x, inplace=True) # Can break gradient computation
x += residual # In-place add breaks autograd graph
# Good: Out-of-place operations
x = F.relu(x)
x = x + residual
# Bad: Moving data to GPU inside the training loop repeatedly
for data, target in dataloader:
model = model.cuda() # Moves model EVERY iteration!
# Good: Move model once before the loop
model = model.to(device)
for data, target in dataloader:
data, target = data.to(device), target.to(device)
# Bad: Using .item() before backward
loss = criterion(output, target).item() # Detaches from graph!
loss.backward() # Error: can't backprop through .item()
# Good: Call .item() only for logging
loss = criterion(output, target)
loss.backward()
print(f"Loss: {loss.item():.4f}") # .item() after backward is fine
# Bad: Not using torch.save properly
torch.save(model, "model.pt") # Saves entire model (fragile, not portable)
# Good: Save state_dict
torch.save(model.state_dict(), "model.pt")
```
__Remember__: PyTorch code should be device-agnostic, reproducible, and memory-conscious. When in doubt, profile with `torch.profiler` and check GPU memory with `torch.cuda.memory_summary()`.

View File

@@ -0,0 +1,264 @@
---
name: rules-distill
description: "Scan skills to extract cross-cutting principles and distill them into rules — append, revise, or create new rule files"
origin: ECC
---
# Rules Distill
Scan installed skills, extract cross-cutting principles that appear in multiple skills, and distill them into rules — appending to existing rule files, revising outdated content, or creating new rule files.
Applies the "deterministic collection + LLM judgment" principle: scripts collect facts exhaustively, then an LLM cross-reads the full context and produces verdicts.
## When to Use
- Periodic rules maintenance (monthly or after installing new skills)
- After a skill-stocktake reveals patterns that should be rules
- When rules feel incomplete relative to the skills being used
## How It Works
The rules distillation process follows three phases:
### Phase 1: Inventory (Deterministic Collection)
#### 1a. Collect skill inventory
```bash
bash ~/.claude/skills/rules-distill/scripts/scan-skills.sh
```
#### 1b. Collect rules index
```bash
bash ~/.claude/skills/rules-distill/scripts/scan-rules.sh
```
#### 1c. Present to user
```
Rules Distillation — Phase 1: Inventory
────────────────────────────────────────
Skills: {N} files scanned
Rules: {M} files ({K} headings indexed)
Proceeding to cross-read analysis...
```
### Phase 2: Cross-read, Match & Verdict (LLM Judgment)
Extraction and matching are unified in a single pass. Rules files are small enough (~800 lines total) that the full text can be provided to the LLM — no grep pre-filtering needed.
#### Batching
Group skills into **thematic clusters** based on their descriptions. Analyze each cluster in a subagent with the full rules text.
#### Cross-batch Merge
After all batches complete, merge candidates across batches:
- Deduplicate candidates with the same or overlapping principles
- Re-check the "2+ skills" requirement using evidence from **all** batches combined — a principle found in 1 skill per batch but 2+ skills total is valid
#### Subagent Prompt
Launch a general-purpose Agent with the following prompt:
````
You are an analyst who cross-reads skills to extract principles that should be promoted to rules.
## Input
- Skills: {full text of skills in this batch}
- Existing rules: {full text of all rule files}
## Extraction Criteria
Include a candidate ONLY if ALL of these are true:
1. **Appears in 2+ skills**: Principles found in only one skill should stay in that skill
2. **Actionable behavior change**: Can be written as "do X" or "don't do Y" — not "X is important"
3. **Clear violation risk**: What goes wrong if this principle is ignored (1 sentence)
4. **Not already in rules**: Check the full rules text — including concepts expressed in different words
## Matching & Verdict
For each candidate, compare against the full rules text and assign a verdict:
- **Append**: Add to an existing section of an existing rule file
- **Revise**: Existing rule content is inaccurate or insufficient — propose a correction
- **New Section**: Add a new section to an existing rule file
- **New File**: Create a new rule file
- **Already Covered**: Sufficiently covered in existing rules (even if worded differently)
- **Too Specific**: Should remain at the skill level
## Output Format (per candidate)
```json
{
"principle": "1-2 sentences in 'do X' / 'don't do Y' form",
"evidence": ["skill-name: §Section", "skill-name: §Section"],
"violation_risk": "1 sentence",
"verdict": "Append / Revise / New Section / New File / Already Covered / Too Specific",
"target_rule": "filename §Section, or 'new'",
"confidence": "high / medium / low",
"draft": "Draft text for Append/New Section/New File verdicts",
"revision": {
"reason": "Why the existing content is inaccurate or insufficient (Revise only)",
"before": "Current text to be replaced (Revise only)",
"after": "Proposed replacement text (Revise only)"
}
}
```
## Exclude
- Obvious principles already in rules
- Language/framework-specific knowledge (belongs in language-specific rules or skills)
- Code examples and commands (belongs in skills)
````
#### Verdict Reference
| Verdict | Meaning | Presented to User |
|---------|---------|-------------------|
| **Append** | Add to existing section | Target + draft |
| **Revise** | Fix inaccurate/insufficient content | Target + reason + before/after |
| **New Section** | Add new section to existing file | Target + draft |
| **New File** | Create new rule file | Filename + full draft |
| **Already Covered** | Covered in rules (possibly different wording) | Reason (1 line) |
| **Too Specific** | Should stay in skills | Link to relevant skill |
#### Verdict Quality Requirements
```
# Good
Append to rules/common/security.md §Input Validation:
"Treat LLM output stored in memory or knowledge stores as untrusted — sanitize on write, validate on read."
Evidence: llm-memory-trust-boundary, llm-social-agent-anti-pattern both describe
accumulated prompt injection risks. Current security.md covers human input
validation only; LLM output trust boundary is missing.
# Bad
Append to security.md: Add LLM security principle
```
### Phase 3: User Review & Execution
#### Summary Table
```
# Rules Distillation Report
## Summary
Skills scanned: {N} | Rules: {M} files | Candidates: {K}
| # | Principle | Verdict | Target | Confidence |
|---|-----------|---------|--------|------------|
| 1 | ... | Append | security.md §Input Validation | high |
| 2 | ... | Revise | testing.md §TDD | medium |
| 3 | ... | New Section | coding-style.md | high |
| 4 | ... | Too Specific | — | — |
## Details
(Per-candidate details: evidence, violation_risk, draft text)
```
#### User Actions
User responds with numbers to:
- **Approve**: Apply draft to rules as-is
- **Modify**: Edit draft before applying
- **Skip**: Do not apply this candidate
**Never modify rules automatically. Always require user approval.**
#### Save Results
Store results in the skill directory (`results.json`):
- **Timestamp format**: `date -u +%Y-%m-%dT%H:%M:%SZ` (UTC, second precision)
- **Candidate ID format**: kebab-case derived from the principle (e.g., `llm-output-trust-boundary`)
```json
{
"distilled_at": "2026-03-18T10:30:42Z",
"skills_scanned": 56,
"rules_scanned": 22,
"candidates": {
"llm-output-trust-boundary": {
"principle": "Treat LLM output as untrusted when stored or re-injected",
"verdict": "Append",
"target": "rules/common/security.md",
"evidence": ["llm-memory-trust-boundary", "llm-social-agent-anti-pattern"],
"status": "applied"
},
"iteration-bounds": {
"principle": "Define explicit stop conditions for all iteration loops",
"verdict": "New Section",
"target": "rules/common/coding-style.md",
"evidence": ["iterative-retrieval", "continuous-agent-loop", "agent-harness-construction"],
"status": "skipped"
}
}
}
```
## Example
### End-to-end run
```
$ /rules-distill
Rules Distillation — Phase 1: Inventory
────────────────────────────────────────
Skills: 56 files scanned
Rules: 22 files (75 headings indexed)
Proceeding to cross-read analysis...
[Subagent analysis: Batch 1 (agent/meta skills) ...]
[Subagent analysis: Batch 2 (coding/pattern skills) ...]
[Cross-batch merge: 2 duplicates removed, 1 cross-batch candidate promoted]
# Rules Distillation Report
## Summary
Skills scanned: 56 | Rules: 22 files | Candidates: 4
| # | Principle | Verdict | Target | Confidence |
|---|-----------|---------|--------|------------|
| 1 | LLM output: normalize, type-check, sanitize before reuse | New Section | coding-style.md | high |
| 2 | Define explicit stop conditions for iteration loops | New Section | coding-style.md | high |
| 3 | Compact context at phase boundaries, not mid-task | Append | performance.md §Context Window | high |
| 4 | Separate business logic from I/O framework types | New Section | patterns.md | high |
## Details
### 1. LLM Output Validation
Verdict: New Section in coding-style.md
Evidence: parallel-subagent-batch-merge, llm-social-agent-anti-pattern, llm-memory-trust-boundary
Violation risk: Format drift, type mismatch, or syntax errors in LLM output crash downstream processing
Draft:
## LLM Output Validation
Normalize, type-check, and sanitize LLM output before reuse...
See skill: parallel-subagent-batch-merge, llm-memory-trust-boundary
[... details for candidates 2-4 ...]
Approve, modify, or skip each candidate by number:
> User: Approve 1, 3. Skip 2, 4.
✓ Applied: coding-style.md §LLM Output Validation
✓ Applied: performance.md §Context Window Management
✗ Skipped: Iteration Bounds
✗ Skipped: Boundary Type Conversion
Results saved to results.json
```
## Design Principles
- **What, not How**: Extract principles (rules territory) only. Code examples and commands stay in skills.
- **Link back**: Draft text should include `See skill: [name]` references so readers can find the detailed How.
- **Deterministic collection, LLM judgment**: Scripts guarantee exhaustiveness; the LLM guarantees contextual understanding.
- **Anti-abstraction safeguard**: The 3-layer filter (2+ skills evidence, actionable behavior test, violation risk) prevents overly abstract principles from entering rules.

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# scan-rules.sh — enumerate rule files and extract H2 heading index
# Usage: scan-rules.sh [RULES_DIR]
# Output: JSON to stdout
#
# Environment:
# RULES_DISTILL_DIR Override ~/.claude/rules (for testing only)
set -euo pipefail
RULES_DIR="${RULES_DISTILL_DIR:-${1:-$HOME/.claude/rules}}"
if [[ ! -d "$RULES_DIR" ]]; then
jq -n --arg path "$RULES_DIR" '{"error":"rules directory not found","path":$path}' >&2
exit 1
fi
# Collect all .md files (excluding _archived/)
files=()
while IFS= read -r f; do
files+=("$f")
done < <(find "$RULES_DIR" -name '*.md' -not -path '*/_archived/*' -print | sort)
total=${#files[@]}
tmpdir=$(mktemp -d)
_rules_cleanup() { rm -rf "$tmpdir"; }
trap _rules_cleanup EXIT
for i in "${!files[@]}"; do
file="${files[$i]}"
rel_path="${file#"$HOME"/}"
rel_path="~/$rel_path"
# Extract H2 headings (## Title) into a JSON array via jq
headings_json=$({ grep -E '^## ' "$file" 2>/dev/null || true; } | sed 's/^## //' | jq -R . | jq -s '.')
# Get line count
line_count=$(wc -l < "$file" | tr -d ' ')
jq -n \
--arg path "$rel_path" \
--arg file "$(basename "$file")" \
--argjson lines "$line_count" \
--argjson headings "$headings_json" \
'{path:$path,file:$file,lines:$lines,headings:$headings}' \
> "$tmpdir/$i.json"
done
if [[ ${#files[@]} -eq 0 ]]; then
jq -n --arg dir "$RULES_DIR" '{rules_dir:$dir,total:0,rules:[]}'
else
jq -n \
--arg dir "$RULES_DIR" \
--argjson total "$total" \
--argjson rules "$(jq -s '.' "$tmpdir"/*.json)" \
'{rules_dir:$dir,total:$total,rules:$rules}'
fi

View File

@@ -0,0 +1,129 @@
#!/usr/bin/env bash
# scan-skills.sh — enumerate skill files, extract frontmatter and UTC mtime
# Usage: scan-skills.sh [CWD_SKILLS_DIR]
# Output: JSON to stdout
#
# When CWD_SKILLS_DIR is omitted, defaults to $PWD/.claude/skills so the
# script always picks up project-level skills without relying on the caller.
#
# Environment:
# RULES_DISTILL_GLOBAL_DIR Override ~/.claude/skills (for testing only;
# do not set in production — intended for bats tests)
# RULES_DISTILL_PROJECT_DIR Override project dir detection (for testing only)
set -euo pipefail
GLOBAL_DIR="${RULES_DISTILL_GLOBAL_DIR:-$HOME/.claude/skills}"
CWD_SKILLS_DIR="${RULES_DISTILL_PROJECT_DIR:-${1:-$PWD/.claude/skills}}"
# Validate CWD_SKILLS_DIR looks like a .claude/skills path (defense-in-depth).
# Only warn when the path exists — a nonexistent path poses no traversal risk.
if [[ -n "$CWD_SKILLS_DIR" && -d "$CWD_SKILLS_DIR" && "$CWD_SKILLS_DIR" != */.claude/skills* ]]; then
echo "Warning: CWD_SKILLS_DIR does not look like a .claude/skills path: $CWD_SKILLS_DIR" >&2
fi
# Extract a frontmatter field (handles both quoted and unquoted single-line values).
# Does NOT support multi-line YAML blocks (| or >) or nested YAML keys.
extract_field() {
local file="$1" field="$2"
awk -v f="$field" '
BEGIN { fm=0 }
/^---$/ { fm++; next }
fm==1 {
n = length(f) + 2
if (substr($0, 1, n) == f ": ") {
val = substr($0, n+1)
gsub(/^"/, "", val)
gsub(/"$/, "", val)
print val
exit
}
}
fm>=2 { exit }
' "$file"
}
# Get file mtime in UTC ISO8601 (portable: GNU and BSD)
get_mtime() {
local file="$1"
local secs
secs=$(stat -c %Y "$file" 2>/dev/null || stat -f %m "$file" 2>/dev/null) || return 1
date -u -d "@$secs" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null ||
date -u -r "$secs" +%Y-%m-%dT%H:%M:%SZ
}
# Scan a directory and produce a JSON array of skill objects
scan_dir_to_json() {
local dir="$1"
local tmpdir
tmpdir=$(mktemp -d)
local _scan_tmpdir="$tmpdir"
_scan_cleanup() { rm -rf "$_scan_tmpdir"; }
trap _scan_cleanup RETURN
local i=0
while IFS= read -r file; do
local name desc mtime dp
name=$(extract_field "$file" "name")
desc=$(extract_field "$file" "description")
mtime=$(get_mtime "$file")
dp="${file/#$HOME/~}"
jq -n \
--arg path "$dp" \
--arg name "$name" \
--arg description "$desc" \
--arg mtime "$mtime" \
'{path:$path,name:$name,description:$description,mtime:$mtime}' \
> "$tmpdir/$i.json"
i=$((i+1))
done < <(find "$dir" -name "SKILL.md" -type f 2>/dev/null | sort)
if [[ $i -eq 0 ]]; then
echo "[]"
else
jq -s '.' "$tmpdir"/*.json
fi
}
# --- Main ---
global_found="false"
global_count=0
global_skills="[]"
if [[ -d "$GLOBAL_DIR" ]]; then
global_found="true"
global_skills=$(scan_dir_to_json "$GLOBAL_DIR")
global_count=$(echo "$global_skills" | jq 'length')
fi
project_found="false"
project_path=""
project_count=0
project_skills="[]"
if [[ -n "$CWD_SKILLS_DIR" && -d "$CWD_SKILLS_DIR" ]]; then
project_found="true"
project_path="$CWD_SKILLS_DIR"
project_skills=$(scan_dir_to_json "$CWD_SKILLS_DIR")
project_count=$(echo "$project_skills" | jq 'length')
fi
# Merge global + project skills into one array
all_skills=$(jq -s 'add' <(echo "$global_skills") <(echo "$project_skills"))
jq -n \
--arg global_found "$global_found" \
--argjson global_count "$global_count" \
--arg project_found "$project_found" \
--arg project_path "$project_path" \
--argjson project_count "$project_count" \
--argjson skills "$all_skills" \
'{
scan_summary: {
global: { found: ($global_found == "true"), count: $global_count },
project: { found: ($project_found == "true"), path: $project_path, count: $project_count }
},
skills: $skills
}'

View File

@@ -43,7 +43,6 @@ fn process_bad(data: &Vec<u8>) -> usize {
}
```
### Use `Cow` for Flexible Ownership
```rust

View File

@@ -52,6 +52,34 @@ function writeInstallComponentsManifest(testDir, components) {
});
}
/**
* Run modified source via a temp file (avoids Windows node -e shebang issues).
* The temp file is written inside the repo so require() can resolve node_modules.
* @param {string} source - JavaScript source to execute
* @returns {{code: number, stdout: string, stderr: string}}
*/
function runSourceViaTempFile(source) {
const tmpFile = path.join(repoRoot, `.tmp-validator-${Date.now()}-${Math.random().toString(36).slice(2)}.js`);
try {
fs.writeFileSync(tmpFile, source, 'utf8');
const stdout = execFileSync('node', [tmpFile], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
cwd: repoRoot,
});
return { code: 0, stdout, stderr: '' };
} catch (err) {
return {
code: err.status || 1,
stdout: err.stdout || '',
stderr: err.stderr || '',
};
} finally {
try { fs.unlinkSync(tmpFile); } catch (_) { /* ignore cleanup errors */ }
}
}
/**
* Run a validator script via a wrapper that overrides its directory constant.
* This allows testing error cases without modifying real project files.
@@ -67,27 +95,14 @@ function runValidatorWithDir(validatorName, dirConstant, overridePath) {
// Read the validator source, replace the directory constant, and run as a wrapper
let source = fs.readFileSync(validatorPath, 'utf8');
// Remove the shebang line
// Remove the shebang line (Windows node cannot parse shebangs in eval/inline mode)
source = source.replace(/^#!.*\n/, '');
// Replace the directory constant with our override path
const dirRegex = new RegExp(`const ${dirConstant} = .*?;`);
source = source.replace(dirRegex, `const ${dirConstant} = ${JSON.stringify(overridePath)};`);
try {
const stdout = execFileSync('node', ['-e', source], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
return { code: 0, stdout, stderr: '' };
} catch (err) {
return {
code: err.status || 1,
stdout: err.stdout || '',
stderr: err.stderr || '',
};
}
return runSourceViaTempFile(source);
}
/**
@@ -103,20 +118,7 @@ function runValidatorWithDirs(validatorName, overrides) {
const dirRegex = new RegExp(`const ${constant} = .*?;`);
source = source.replace(dirRegex, `const ${constant} = ${JSON.stringify(overridePath)};`);
}
try {
const stdout = execFileSync('node', ['-e', source], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
return { code: 0, stdout, stderr: '' };
} catch (err) {
return {
code: err.status || 1,
stdout: err.stdout || '',
stderr: err.stderr || '',
};
}
return runSourceViaTempFile(source);
}
/**
@@ -158,20 +160,7 @@ function runCatalogValidator(overrides = {}) {
source = source.replace(dirRegex, `const ${constant} = ${JSON.stringify(overridePath)};`);
}
try {
const stdout = execFileSync('node', ['-e', source], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
return { code: 0, stdout, stderr: '' };
} catch (err) {
return {
code: err.status || 1,
stdout: err.stdout || '',
stderr: err.stderr || '',
};
}
return runSourceViaTempFile(source);
}
function writeCatalogFixture(testDir, options = {}) {

View File

@@ -0,0 +1,239 @@
/**
* Tests for worktree project-ID mismatch fix
*
* Validates that detect-project.sh uses -e (not -d) for .git existence
* checks, so that git worktrees (where .git is a file) are detected
* correctly.
*
* Run with: node tests/hooks/detect-project-worktree.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { execSync } = require('child_process');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
passed++;
} catch (err) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${err.message}`);
failed++;
}
}
function createTempDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-worktree-test-'));
}
function cleanupDir(dir) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// ignore cleanup errors
}
}
const repoRoot = path.resolve(__dirname, '..', '..');
const detectProjectPath = path.join(
repoRoot,
'skills',
'continuous-learning-v2',
'scripts',
'detect-project.sh'
);
console.log('\n=== Worktree Project-ID Mismatch Tests ===\n');
// ──────────────────────────────────────────────────────
// Group 1: Content checks on detect-project.sh
// ──────────────────────────────────────────────────────
console.log('--- Content checks on detect-project.sh ---');
test('uses -e (not -d) for .git existence check', () => {
const content = fs.readFileSync(detectProjectPath, 'utf8');
assert.ok(
content.includes('[ -e "${project_root}/.git" ]'),
'detect-project.sh should use -e for .git check'
);
assert.ok(
!content.includes('[ -d "${project_root}/.git" ]'),
'detect-project.sh should NOT use -d for .git check'
);
});
test('has command -v git fallback check', () => {
const content = fs.readFileSync(detectProjectPath, 'utf8');
assert.ok(
content.includes('command -v git'),
'detect-project.sh should check for git availability with command -v'
);
});
test('uses git -C for safe directory operations', () => {
const content = fs.readFileSync(detectProjectPath, 'utf8');
assert.ok(
content.includes('git -C'),
'detect-project.sh should use git -C for directory-scoped operations'
);
});
// ──────────────────────────────────────────────────────
// Group 2: Behavior test — -e vs -d
// ──────────────────────────────────────────────────────
console.log('\n--- Behavior test: -e vs -d ---');
const behaviorDir = createTempDir();
test('[ -d ] returns true for .git directory', () => {
const dir = path.join(behaviorDir, 'test-d-dir');
fs.mkdirSync(dir, { recursive: true });
fs.mkdirSync(path.join(dir, '.git'));
const result = execSync(`bash -c '[ -d "${dir}/.git" ] && echo yes || echo no'`).toString().trim();
assert.strictEqual(result, 'yes');
});
test('[ -d ] returns false for .git file', () => {
const dir = path.join(behaviorDir, 'test-d-file');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, '.git'), 'gitdir: /some/path\n');
const result = execSync(`bash -c '[ -d "${dir}/.git" ] && echo yes || echo no'`).toString().trim();
assert.strictEqual(result, 'no');
});
test('[ -e ] returns true for .git directory', () => {
const dir = path.join(behaviorDir, 'test-e-dir');
fs.mkdirSync(dir, { recursive: true });
fs.mkdirSync(path.join(dir, '.git'));
const result = execSync(`bash -c '[ -e "${dir}/.git" ] && echo yes || echo no'`).toString().trim();
assert.strictEqual(result, 'yes');
});
test('[ -e ] returns true for .git file', () => {
const dir = path.join(behaviorDir, 'test-e-file');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, '.git'), 'gitdir: /some/path\n');
const result = execSync(`bash -c '[ -e "${dir}/.git" ] && echo yes || echo no'`).toString().trim();
assert.strictEqual(result, 'yes');
});
test('[ -e ] returns false when .git does not exist', () => {
const dir = path.join(behaviorDir, 'test-e-none');
fs.mkdirSync(dir, { recursive: true });
const result = execSync(`bash -c '[ -e "${dir}/.git" ] && echo yes || echo no'`).toString().trim();
assert.strictEqual(result, 'no');
});
cleanupDir(behaviorDir);
// ──────────────────────────────────────────────────────
// Group 3: E2E test — detect-project.sh with worktree .git file
// ──────────────────────────────────────────────────────
console.log('\n--- E2E: detect-project.sh with worktree .git file ---');
test('detect-project.sh sets PROJECT_NAME and non-global PROJECT_ID for worktree', () => {
const testDir = createTempDir();
try {
// Create a "main" repo with git init so we have real git structures
const mainRepo = path.join(testDir, 'main-repo');
fs.mkdirSync(mainRepo, { recursive: true });
execSync('git init', { cwd: mainRepo, stdio: 'pipe' });
execSync('git commit --allow-empty -m "init"', {
cwd: mainRepo,
stdio: 'pipe',
env: {
...process.env,
GIT_AUTHOR_NAME: 'Test',
GIT_AUTHOR_EMAIL: 'test@test.com',
GIT_COMMITTER_NAME: 'Test',
GIT_COMMITTER_EMAIL: 'test@test.com'
}
});
// Create a worktree-like directory with .git as a file
const worktreeDir = path.join(testDir, 'my-worktree');
fs.mkdirSync(worktreeDir, { recursive: true });
// Set up the worktree directory structure in the main repo
const worktreesDir = path.join(mainRepo, '.git', 'worktrees', 'my-worktree');
fs.mkdirSync(worktreesDir, { recursive: true });
// Create the gitdir file and commondir in the worktree metadata
const mainGitDir = path.join(mainRepo, '.git');
fs.writeFileSync(
path.join(worktreesDir, 'commondir'),
'../..\n'
);
fs.writeFileSync(
path.join(worktreesDir, 'HEAD'),
fs.readFileSync(path.join(mainGitDir, 'HEAD'), 'utf8')
);
// Write .git file in the worktree directory (this is what git worktree creates)
fs.writeFileSync(
path.join(worktreeDir, '.git'),
`gitdir: ${worktreesDir}\n`
);
// Source detect-project.sh from the worktree directory and capture results
const script = `
export CLAUDE_PROJECT_DIR="${worktreeDir}"
export HOME="${testDir}"
source "${detectProjectPath}"
echo "PROJECT_NAME=\${PROJECT_NAME}"
echo "PROJECT_ID=\${PROJECT_ID}"
`;
const result = execSync(`bash -c '${script.replace(/'/g, "'\\''")}'`, {
cwd: worktreeDir,
timeout: 10000,
env: {
...process.env,
HOME: testDir,
CLAUDE_PROJECT_DIR: worktreeDir
}
}).toString();
const lines = result.trim().split('\n');
const vars = {};
for (const line of lines) {
const match = line.match(/^(PROJECT_NAME|PROJECT_ID)=(.*)$/);
if (match) {
vars[match[1]] = match[2];
}
}
assert.ok(
vars.PROJECT_NAME && vars.PROJECT_NAME.length > 0,
`PROJECT_NAME should be set, got: "${vars.PROJECT_NAME || ''}"`
);
assert.ok(
vars.PROJECT_ID && vars.PROJECT_ID !== 'global',
`PROJECT_ID should not be "global", got: "${vars.PROJECT_ID || ''}"`
);
} finally {
cleanupDir(testDir);
}
});
// ──────────────────────────────────────────────────────
// Summary
// ──────────────────────────────────────────────────────
console.log('\n=== Test Results ===');
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log(`Total: ${passed + failed}\n`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,294 @@
/**
* Tests for governance event capture hook.
*/
const assert = require('assert');
const {
detectSecrets,
detectApprovalRequired,
detectSensitivePath,
analyzeForGovernanceEvents,
run,
} = require('../../scripts/hooks/governance-capture');
async function test(name, fn) {
try {
await fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
async function runTests() {
console.log('\n=== Testing governance-capture ===\n');
let passed = 0;
let failed = 0;
// ── detectSecrets ──────────────────────────────────────────
if (await test('detectSecrets finds AWS access keys', async () => {
const findings = detectSecrets('my key is AKIAIOSFODNN7EXAMPLE');
assert.ok(findings.length > 0);
assert.ok(findings.some(f => f.name === 'aws_key'));
})) passed += 1; else failed += 1;
if (await test('detectSecrets finds generic secrets', async () => {
const findings = detectSecrets('api_key = "sk-proj-abcdefghij1234567890"');
assert.ok(findings.length > 0);
assert.ok(findings.some(f => f.name === 'generic_secret'));
})) passed += 1; else failed += 1;
if (await test('detectSecrets finds private keys', async () => {
const findings = detectSecrets('-----BEGIN RSA PRIVATE KEY-----\nMIIE...');
assert.ok(findings.length > 0);
assert.ok(findings.some(f => f.name === 'private_key'));
})) passed += 1; else failed += 1;
if (await test('detectSecrets finds GitHub tokens', async () => {
const findings = detectSecrets('token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij');
assert.ok(findings.length > 0);
assert.ok(findings.some(f => f.name === 'github_token'));
})) passed += 1; else failed += 1;
if (await test('detectSecrets returns empty array for clean text', async () => {
const findings = detectSecrets('This is a normal log message with no secrets.');
assert.strictEqual(findings.length, 0);
})) passed += 1; else failed += 1;
if (await test('detectSecrets handles null and undefined', async () => {
assert.deepStrictEqual(detectSecrets(null), []);
assert.deepStrictEqual(detectSecrets(undefined), []);
assert.deepStrictEqual(detectSecrets(''), []);
})) passed += 1; else failed += 1;
// ── detectApprovalRequired ─────────────────────────────────
if (await test('detectApprovalRequired flags force push', async () => {
const findings = detectApprovalRequired('git push origin main --force');
assert.ok(findings.length > 0);
})) passed += 1; else failed += 1;
if (await test('detectApprovalRequired flags hard reset', async () => {
const findings = detectApprovalRequired('git reset --hard HEAD~3');
assert.ok(findings.length > 0);
})) passed += 1; else failed += 1;
if (await test('detectApprovalRequired flags rm -rf', async () => {
const findings = detectApprovalRequired('rm -rf /tmp/important');
assert.ok(findings.length > 0);
})) passed += 1; else failed += 1;
if (await test('detectApprovalRequired flags DROP TABLE', async () => {
const findings = detectApprovalRequired('DROP TABLE users');
assert.ok(findings.length > 0);
})) passed += 1; else failed += 1;
if (await test('detectApprovalRequired allows safe commands', async () => {
const findings = detectApprovalRequired('git status');
assert.strictEqual(findings.length, 0);
})) passed += 1; else failed += 1;
if (await test('detectApprovalRequired handles null', async () => {
assert.deepStrictEqual(detectApprovalRequired(null), []);
assert.deepStrictEqual(detectApprovalRequired(''), []);
})) passed += 1; else failed += 1;
// ── detectSensitivePath ────────────────────────────────────
if (await test('detectSensitivePath identifies .env files', async () => {
assert.ok(detectSensitivePath('.env'));
assert.ok(detectSensitivePath('.env.local'));
assert.ok(detectSensitivePath('/project/.env.production'));
})) passed += 1; else failed += 1;
if (await test('detectSensitivePath identifies credential files', async () => {
assert.ok(detectSensitivePath('credentials.json'));
assert.ok(detectSensitivePath('/home/user/.ssh/id_rsa'));
assert.ok(detectSensitivePath('server.key'));
assert.ok(detectSensitivePath('cert.pem'));
})) passed += 1; else failed += 1;
if (await test('detectSensitivePath returns false for normal files', async () => {
assert.ok(!detectSensitivePath('index.js'));
assert.ok(!detectSensitivePath('README.md'));
assert.ok(!detectSensitivePath('package.json'));
})) passed += 1; else failed += 1;
if (await test('detectSensitivePath handles null', async () => {
assert.ok(!detectSensitivePath(null));
assert.ok(!detectSensitivePath(''));
})) passed += 1; else failed += 1;
// ── analyzeForGovernanceEvents ─────────────────────────────
if (await test('analyzeForGovernanceEvents detects secrets in tool input', async () => {
const events = analyzeForGovernanceEvents({
tool_name: 'Write',
tool_input: {
file_path: '/tmp/config.js',
content: 'const key = "AKIAIOSFODNN7EXAMPLE";',
},
});
assert.ok(events.length > 0);
const secretEvent = events.find(e => e.eventType === 'secret_detected');
assert.ok(secretEvent);
assert.strictEqual(secretEvent.payload.severity, 'critical');
})) passed += 1; else failed += 1;
if (await test('analyzeForGovernanceEvents detects approval-required commands', async () => {
const events = analyzeForGovernanceEvents({
tool_name: 'Bash',
tool_input: {
command: 'git push origin main --force',
},
});
assert.ok(events.length > 0);
const approvalEvent = events.find(e => e.eventType === 'approval_requested');
assert.ok(approvalEvent);
assert.strictEqual(approvalEvent.payload.severity, 'high');
})) passed += 1; else failed += 1;
if (await test('analyzeForGovernanceEvents detects sensitive file access', async () => {
const events = analyzeForGovernanceEvents({
tool_name: 'Edit',
tool_input: {
file_path: '/project/.env.production',
old_string: 'DB_URL=old',
new_string: 'DB_URL=new',
},
});
assert.ok(events.length > 0);
const policyEvent = events.find(e => e.eventType === 'policy_violation');
assert.ok(policyEvent);
assert.strictEqual(policyEvent.payload.reason, 'sensitive_file_access');
})) passed += 1; else failed += 1;
if (await test('analyzeForGovernanceEvents detects elevated privilege commands', async () => {
const events = analyzeForGovernanceEvents({
tool_name: 'Bash',
tool_input: { command: 'sudo rm -rf /etc/something' },
}, {
hookPhase: 'post',
});
const securityEvent = events.find(e => e.eventType === 'security_finding');
assert.ok(securityEvent);
assert.strictEqual(securityEvent.payload.reason, 'elevated_privilege_command');
})) passed += 1; else failed += 1;
if (await test('analyzeForGovernanceEvents returns empty for clean inputs', async () => {
const events = analyzeForGovernanceEvents({
tool_name: 'Read',
tool_input: { file_path: '/project/src/index.js' },
});
assert.strictEqual(events.length, 0);
})) passed += 1; else failed += 1;
if (await test('analyzeForGovernanceEvents populates session ID from context', async () => {
const events = analyzeForGovernanceEvents({
tool_name: 'Write',
tool_input: {
file_path: '/project/.env',
content: 'DB_URL=test',
},
}, {
sessionId: 'test-session-123',
});
assert.ok(events.length > 0);
assert.strictEqual(events[0].sessionId, 'test-session-123');
})) passed += 1; else failed += 1;
if (await test('analyzeForGovernanceEvents generates unique event IDs', async () => {
const events1 = analyzeForGovernanceEvents({
tool_name: 'Write',
tool_input: { file_path: '.env', content: '' },
});
const events2 = analyzeForGovernanceEvents({
tool_name: 'Write',
tool_input: { file_path: '.env.local', content: '' },
});
if (events1.length > 0 && events2.length > 0) {
assert.notStrictEqual(events1[0].id, events2[0].id);
}
})) passed += 1; else failed += 1;
// ── run() function ─────────────────────────────────────────
if (await test('run() passes through input when feature flag is off', async () => {
const original = process.env.ECC_GOVERNANCE_CAPTURE;
delete process.env.ECC_GOVERNANCE_CAPTURE;
try {
const input = JSON.stringify({ tool_name: 'Bash', tool_input: { command: 'git push --force' } });
const result = run(input);
assert.strictEqual(result, input);
} finally {
if (original !== undefined) {
process.env.ECC_GOVERNANCE_CAPTURE = original;
}
}
})) passed += 1; else failed += 1;
if (await test('run() passes through input when feature flag is on', async () => {
const original = process.env.ECC_GOVERNANCE_CAPTURE;
process.env.ECC_GOVERNANCE_CAPTURE = '1';
try {
const input = JSON.stringify({ tool_name: 'Read', tool_input: { file_path: 'index.js' } });
const result = run(input);
assert.strictEqual(result, input);
} finally {
if (original !== undefined) {
process.env.ECC_GOVERNANCE_CAPTURE = original;
} else {
delete process.env.ECC_GOVERNANCE_CAPTURE;
}
}
})) passed += 1; else failed += 1;
if (await test('run() handles invalid JSON gracefully', async () => {
const original = process.env.ECC_GOVERNANCE_CAPTURE;
process.env.ECC_GOVERNANCE_CAPTURE = '1';
try {
const result = run('not valid json');
assert.strictEqual(result, 'not valid json');
} finally {
if (original !== undefined) {
process.env.ECC_GOVERNANCE_CAPTURE = original;
} else {
delete process.env.ECC_GOVERNANCE_CAPTURE;
}
}
})) passed += 1; else failed += 1;
if (await test('run() can detect multiple event types in one input', async () => {
// Bash command with force push AND secret in command
const events = analyzeForGovernanceEvents({
tool_name: 'Bash',
tool_input: {
command: 'API_KEY="AKIAIOSFODNN7EXAMPLE" git push --force',
},
});
const eventTypes = events.map(e => e.eventType);
assert.ok(eventTypes.includes('secret_detected'));
assert.ok(eventTypes.includes('approval_requested'));
})) passed += 1; else failed += 1;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -56,42 +56,24 @@ console.log('--- observe.sh signal throttling ---');
test('observe.sh contains SIGNAL_EVERY_N throttle variable', () => {
const content = fs.readFileSync(observeShPath, 'utf8');
assert.ok(
content.includes('SIGNAL_EVERY_N'),
'observe.sh should define SIGNAL_EVERY_N for throttling'
);
assert.ok(content.includes('SIGNAL_EVERY_N'), 'observe.sh should define SIGNAL_EVERY_N for throttling');
});
test('observe.sh uses a counter file instead of signaling every call', () => {
const content = fs.readFileSync(observeShPath, 'utf8');
assert.ok(
content.includes('.observer-signal-counter'),
'observe.sh should use a signal counter file'
);
assert.ok(content.includes('.observer-signal-counter'), 'observe.sh should use a signal counter file');
});
test('observe.sh only signals when counter reaches threshold', () => {
const content = fs.readFileSync(observeShPath, 'utf8');
assert.ok(
content.includes('should_signal=0'),
'observe.sh should default should_signal to 0'
);
assert.ok(
content.includes('should_signal=1'),
'observe.sh should set should_signal=1 when threshold reached'
);
assert.ok(
content.includes('if [ "$should_signal" -eq 1 ]'),
'observe.sh should gate kill -USR1 behind should_signal check'
);
assert.ok(content.includes('should_signal=0'), 'observe.sh should default should_signal to 0');
assert.ok(content.includes('should_signal=1'), 'observe.sh should set should_signal=1 when threshold reached');
assert.ok(content.includes('if [ "$should_signal" -eq 1 ]'), 'observe.sh should gate kill -USR1 behind should_signal check');
});
test('observe.sh default throttle is 20 observations per signal', () => {
const content = fs.readFileSync(observeShPath, 'utf8');
assert.ok(
content.includes('ECC_OBSERVER_SIGNAL_EVERY_N:-20'),
'Default signal frequency should be every 20 observations'
);
assert.ok(content.includes('ECC_OBSERVER_SIGNAL_EVERY_N:-20'), 'Default signal frequency should be every 20 observations');
});
// ──────────────────────────────────────────────────────
@@ -102,22 +84,13 @@ console.log('\n--- observer-loop.sh re-entrancy guard ---');
test('observer-loop.sh defines ANALYZING guard variable', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(
content.includes('ANALYZING=0'),
'observer-loop.sh should initialize ANALYZING=0'
);
assert.ok(content.includes('ANALYZING=0'), 'observer-loop.sh should initialize ANALYZING=0');
});
test('on_usr1 checks ANALYZING before starting analysis', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(
content.includes('if [ "$ANALYZING" -eq 1 ]'),
'on_usr1 should check ANALYZING flag'
);
assert.ok(
content.includes('Analysis already in progress, skipping signal'),
'on_usr1 should log when skipping due to re-entrancy'
);
assert.ok(content.includes('if [ "$ANALYZING" -eq 1 ]'), 'on_usr1 should check ANALYZING flag');
assert.ok(content.includes('Analysis already in progress, skipping signal'), 'on_usr1 should log when skipping due to re-entrancy');
});
test('on_usr1 sets ANALYZING=1 before and ANALYZING=0 after analysis', () => {
@@ -139,30 +112,18 @@ console.log('\n--- observer-loop.sh cooldown throttle ---');
test('observer-loop.sh defines ANALYSIS_COOLDOWN', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(
content.includes('ANALYSIS_COOLDOWN'),
'observer-loop.sh should define ANALYSIS_COOLDOWN'
);
assert.ok(content.includes('ANALYSIS_COOLDOWN'), 'observer-loop.sh should define ANALYSIS_COOLDOWN');
});
test('on_usr1 enforces cooldown between analyses', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(
content.includes('LAST_ANALYSIS_EPOCH'),
'Should track last analysis time'
);
assert.ok(
content.includes('Analysis cooldown active'),
'Should log when cooldown prevents analysis'
);
assert.ok(content.includes('LAST_ANALYSIS_EPOCH'), 'Should track last analysis time');
assert.ok(content.includes('Analysis cooldown active'), 'Should log when cooldown prevents analysis');
});
test('default cooldown is 60 seconds', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(
content.includes('ECC_OBSERVER_ANALYSIS_COOLDOWN:-60'),
'Default cooldown should be 60 seconds'
);
assert.ok(content.includes('ECC_OBSERVER_ANALYSIS_COOLDOWN:-60'), 'Default cooldown should be 60 seconds');
});
// ──────────────────────────────────────────────────────
@@ -173,30 +134,18 @@ console.log('\n--- observer-loop.sh tail-based sampling ---');
test('analyze_observations uses tail to sample recent observations', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(
content.includes('tail -n "$MAX_ANALYSIS_LINES"'),
'Should use tail to limit observations sent to LLM'
);
assert.ok(content.includes('tail -n "$MAX_ANALYSIS_LINES"'), 'Should use tail to limit observations sent to LLM');
});
test('default max analysis lines is 500', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(
content.includes('ECC_OBSERVER_MAX_ANALYSIS_LINES:-500'),
'Default should sample last 500 lines'
);
assert.ok(content.includes('ECC_OBSERVER_MAX_ANALYSIS_LINES:-500'), 'Default should sample last 500 lines');
});
test('analysis temp file is created and cleaned up', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(
content.includes('ecc-observer-analysis'),
'Should create a temp analysis file'
);
assert.ok(
content.includes('rm -f "$prompt_file" "$analysis_file"'),
'Should clean up both prompt and analysis temp files'
);
assert.ok(content.includes('ecc-observer-analysis'), 'Should create a temp analysis file');
assert.ok(content.includes('rm -f "$prompt_file" "$analysis_file"'), 'Should clean up both prompt and analysis temp files');
});
test('prompt references analysis_file not full OBSERVATIONS_FILE', () => {
@@ -208,10 +157,7 @@ test('prompt references analysis_file not full OBSERVATIONS_FILE', () => {
assert.ok(heredocStart > 0, 'Should find prompt heredoc start');
assert.ok(heredocEnd > heredocStart, 'Should find prompt heredoc end');
const promptSection = content.substring(heredocStart, heredocEnd);
assert.ok(
promptSection.includes('${analysis_file}'),
'Prompt should point Claude at the sampled analysis file, not the full observations file'
);
assert.ok(promptSection.includes('${analysis_file}'), 'Prompt should point Claude at the sampled analysis file, not the full observations file');
});
// ──────────────────────────────────────────────────────
@@ -287,22 +233,22 @@ test('observe.sh creates counter file and increments on each call', () => {
fs.mkdirSync(hooksDir, { recursive: true });
// Minimal detect-project.sh stub
fs.writeFileSync(path.join(scriptsDir, 'detect-project.sh'), [
'#!/bin/bash',
`PROJECT_ID="test-project"`,
`PROJECT_NAME="test-project"`,
`PROJECT_ROOT="${projectDir}"`,
`PROJECT_DIR="${projectDir}"`,
`CLV2_PYTHON_CMD="${process.platform === 'win32' ? 'python' : 'python3'}"`,
''
].join('\n'));
fs.writeFileSync(
path.join(scriptsDir, 'detect-project.sh'),
[
'#!/bin/bash',
`PROJECT_ID="test-project"`,
`PROJECT_NAME="test-project"`,
`PROJECT_ROOT="${projectDir}"`,
`PROJECT_DIR="${projectDir}"`,
`CLV2_PYTHON_CMD="${process.platform === 'win32' ? 'python' : 'python3'}"`,
''
].join('\n')
);
// Copy observe.sh but patch SKILL_ROOT to our test dir
let observeContent = fs.readFileSync(observeShPath, 'utf8');
observeContent = observeContent.replace(
'SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"',
`SKILL_ROOT="${skillRoot}"`
);
observeContent = observeContent.replace('SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"', `SKILL_ROOT="${skillRoot}"`);
const testObserve = path.join(hooksDir, 'observe.sh');
fs.writeFileSync(testObserve, observeContent, { mode: 0o755 });
@@ -333,10 +279,7 @@ test('observe.sh creates counter file and increments on each call', () => {
if (fs.existsSync(counterFile)) {
const val = fs.readFileSync(counterFile, 'utf8').trim();
const counterVal = parseInt(val, 10);
assert.ok(
counterVal >= 1 && counterVal <= 2,
`Counter should be 1 or 2 after 2 calls, got ${counterVal}`
);
assert.ok(counterVal >= 1 && counterVal <= 2, `Counter should be 1 or 2 after 2 calls, got ${counterVal}`);
} else {
// If python3 is not available the hook exits early - that is acceptable
const hasPython = spawnSync('python3', ['--version']).status === 0;
@@ -348,6 +291,44 @@ test('observe.sh creates counter file and increments on each call', () => {
cleanupDir(testDir);
});
// ──────────────────────────────────────────────────────
// Test group 7: Observer Haiku invocation flags
// ──────────────────────────────────────────────────────
console.log('\n--- Observer Haiku invocation flags ---');
test('claude invocation includes --allowedTools flag', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
assert.ok(content.includes('--allowedTools'), 'observer-loop.sh should include --allowedTools flag in claude invocation');
});
test('allowedTools includes Read permission', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
const match = content.match(/--allowedTools\s+"([^"]+)"/);
assert.ok(match, 'Should find --allowedTools with quoted value');
assert.ok(match[1].includes('Read'), `allowedTools should include Read, got: ${match[1]}`);
});
test('allowedTools includes Write permission', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
const match = content.match(/--allowedTools\s+"([^"]+)"/);
assert.ok(match, 'Should find --allowedTools with quoted value');
assert.ok(match[1].includes('Write'), `allowedTools should include Write, got: ${match[1]}`);
});
test('claude invocation still includes ECC_SKIP_OBSERVE and ECC_HOOK_PROFILE guards', () => {
const content = fs.readFileSync(observerLoopPath, 'utf8');
// Find the claude execution line(s)
const lines = content.split('\n');
const claudeLine = lines.find(l => l.includes('claude --model haiku'));
assert.ok(claudeLine, 'Should find claude --model haiku invocation line');
// The env vars are on the same line as the claude command
const claudeLineIndex = lines.indexOf(claudeLine);
const fullCommand = lines.slice(Math.max(0, claudeLineIndex - 1), claudeLineIndex + 3).join(' ');
assert.ok(fullCommand.includes('ECC_SKIP_OBSERVE=1'), 'claude invocation should include ECC_SKIP_OBSERVE=1 guard');
assert.ok(fullCommand.includes('ECC_HOOK_PROFILE=minimal'), 'claude invocation should include ECC_HOOK_PROFILE=minimal guard');
});
// ──────────────────────────────────────────────────────
// Summary
// ──────────────────────────────────────────────────────

View File

@@ -0,0 +1,293 @@
/**
* Tests for agent description compression and lazy loading.
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
parseFrontmatter,
extractSummary,
loadAgent,
loadAgents,
compressToCatalog,
compressToSummary,
buildAgentCatalog,
lazyLoadAgent,
} = require('../../scripts/lib/agent-compress');
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanupTempDir(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeAgent(dir, name, content) {
fs.writeFileSync(path.join(dir, `${name}.md`), content, 'utf8');
}
const SAMPLE_AGENT = `---
name: test-agent
description: A test agent for unit testing purposes.
tools: ["Read", "Grep", "Glob"]
model: sonnet
---
You are a test agent that validates compression logic.
## Your Role
- Run unit tests
- Validate compression output
- Ensure correctness
## Process
### 1. Setup
- Prepare test fixtures
- Load agent files
### 2. Validate
Check the output format and content.
`;
const MINIMAL_AGENT = `---
name: minimal
description: Minimal agent.
tools: ["Read"]
model: haiku
---
Short body.
`;
async function test(name, fn) {
try {
await fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
async function runTests() {
console.log('\n=== Testing agent-compress ===\n');
let passed = 0;
let failed = 0;
if (await test('parseFrontmatter extracts YAML frontmatter and body', async () => {
const { frontmatter, body } = parseFrontmatter(SAMPLE_AGENT);
assert.strictEqual(frontmatter.name, 'test-agent');
assert.strictEqual(frontmatter.description, 'A test agent for unit testing purposes.');
assert.deepStrictEqual(frontmatter.tools, ['Read', 'Grep', 'Glob']);
assert.strictEqual(frontmatter.model, 'sonnet');
assert.ok(body.includes('You are a test agent'));
})) passed += 1; else failed += 1;
if (await test('parseFrontmatter handles content without frontmatter', async () => {
const { frontmatter, body } = parseFrontmatter('Just a plain document.');
assert.deepStrictEqual(frontmatter, {});
assert.strictEqual(body, 'Just a plain document.');
})) passed += 1; else failed += 1;
if (await test('extractSummary returns the first paragraph of the body', async () => {
const { body } = parseFrontmatter(SAMPLE_AGENT);
const summary = extractSummary(body);
assert.ok(summary.includes('test agent'));
assert.ok(summary.includes('compression logic'));
})) passed += 1; else failed += 1;
if (await test('extractSummary returns empty string for empty body', async () => {
assert.strictEqual(extractSummary(''), '');
assert.strictEqual(extractSummary('# Just a heading'), '');
})) passed += 1; else failed += 1;
if (await test('loadAgent reads and parses a single agent file', async () => {
const tmpDir = createTempDir('ecc-agent-compress-');
try {
writeAgent(tmpDir, 'test-agent', SAMPLE_AGENT);
const agent = loadAgent(path.join(tmpDir, 'test-agent.md'));
assert.strictEqual(agent.name, 'test-agent');
assert.strictEqual(agent.fileName, 'test-agent');
assert.deepStrictEqual(agent.tools, ['Read', 'Grep', 'Glob']);
assert.strictEqual(agent.model, 'sonnet');
assert.ok(agent.byteSize > 0);
assert.ok(agent.body.includes('You are a test agent'));
} finally {
cleanupTempDir(tmpDir);
}
})) passed += 1; else failed += 1;
if (await test('loadAgents reads all .md files from a directory', async () => {
const tmpDir = createTempDir('ecc-agent-compress-');
try {
writeAgent(tmpDir, 'agent-a', SAMPLE_AGENT);
writeAgent(tmpDir, 'agent-b', MINIMAL_AGENT);
const agents = loadAgents(tmpDir);
assert.strictEqual(agents.length, 2);
assert.strictEqual(agents[0].fileName, 'agent-a');
assert.strictEqual(agents[1].fileName, 'agent-b');
} finally {
cleanupTempDir(tmpDir);
}
})) passed += 1; else failed += 1;
if (await test('loadAgents returns empty array for non-existent directory', async () => {
const agents = loadAgents('/tmp/nonexistent-ecc-dir-12345');
assert.deepStrictEqual(agents, []);
})) passed += 1; else failed += 1;
if (await test('compressToCatalog strips body and keeps only metadata', async () => {
const tmpDir = createTempDir('ecc-agent-compress-');
try {
writeAgent(tmpDir, 'test-agent', SAMPLE_AGENT);
const agent = loadAgent(path.join(tmpDir, 'test-agent.md'));
const catalog = compressToCatalog(agent);
assert.strictEqual(catalog.name, 'test-agent');
assert.strictEqual(catalog.description, 'A test agent for unit testing purposes.');
assert.deepStrictEqual(catalog.tools, ['Read', 'Grep', 'Glob']);
assert.strictEqual(catalog.model, 'sonnet');
assert.strictEqual(catalog.body, undefined);
} finally {
cleanupTempDir(tmpDir);
}
})) passed += 1; else failed += 1;
if (await test('compressToSummary includes first paragraph summary', async () => {
const tmpDir = createTempDir('ecc-agent-compress-');
try {
writeAgent(tmpDir, 'test-agent', SAMPLE_AGENT);
const agent = loadAgent(path.join(tmpDir, 'test-agent.md'));
const summary = compressToSummary(agent);
assert.strictEqual(summary.name, 'test-agent');
assert.ok(summary.summary.length > 0);
assert.strictEqual(summary.body, undefined);
} finally {
cleanupTempDir(tmpDir);
}
})) passed += 1; else failed += 1;
if (await test('buildAgentCatalog in catalog mode produces minimal output with stats', async () => {
const tmpDir = createTempDir('ecc-agent-compress-');
try {
writeAgent(tmpDir, 'agent-a', SAMPLE_AGENT);
writeAgent(tmpDir, 'agent-b', MINIMAL_AGENT);
const result = buildAgentCatalog(tmpDir, { mode: 'catalog' });
assert.strictEqual(result.agents.length, 2);
assert.strictEqual(result.stats.totalAgents, 2);
assert.strictEqual(result.stats.mode, 'catalog');
assert.ok(result.stats.originalBytes > 0);
assert.ok(result.stats.compressedBytes > 0);
assert.ok(result.stats.compressedBytes < result.stats.originalBytes);
assert.ok(result.stats.compressedTokenEstimate > 0);
// Catalog entries should not have body
for (const agent of result.agents) {
assert.strictEqual(agent.body, undefined);
assert.ok(agent.name);
assert.ok(agent.description);
}
} finally {
cleanupTempDir(tmpDir);
}
})) passed += 1; else failed += 1;
if (await test('buildAgentCatalog in summary mode includes summaries', async () => {
const tmpDir = createTempDir('ecc-agent-compress-');
try {
writeAgent(tmpDir, 'agent-a', SAMPLE_AGENT);
const result = buildAgentCatalog(tmpDir, { mode: 'summary' });
assert.strictEqual(result.agents.length, 1);
assert.ok(result.agents[0].summary);
assert.strictEqual(result.agents[0].body, undefined);
} finally {
cleanupTempDir(tmpDir);
}
})) passed += 1; else failed += 1;
if (await test('buildAgentCatalog in full mode preserves body', async () => {
const tmpDir = createTempDir('ecc-agent-compress-');
try {
writeAgent(tmpDir, 'agent-a', SAMPLE_AGENT);
const result = buildAgentCatalog(tmpDir, { mode: 'full' });
assert.strictEqual(result.agents.length, 1);
assert.ok(result.agents[0].body.includes('You are a test agent'));
} finally {
cleanupTempDir(tmpDir);
}
})) passed += 1; else failed += 1;
if (await test('buildAgentCatalog supports filter function', async () => {
const tmpDir = createTempDir('ecc-agent-compress-');
try {
writeAgent(tmpDir, 'agent-a', SAMPLE_AGENT);
writeAgent(tmpDir, 'agent-b', MINIMAL_AGENT);
const result = buildAgentCatalog(tmpDir, {
mode: 'catalog',
filter: agent => agent.model === 'haiku',
});
assert.strictEqual(result.agents.length, 1);
assert.strictEqual(result.agents[0].name, 'minimal');
} finally {
cleanupTempDir(tmpDir);
}
})) passed += 1; else failed += 1;
if (await test('lazyLoadAgent loads a single agent by name', async () => {
const tmpDir = createTempDir('ecc-agent-compress-');
try {
writeAgent(tmpDir, 'test-agent', SAMPLE_AGENT);
writeAgent(tmpDir, 'other', MINIMAL_AGENT);
const agent = lazyLoadAgent(tmpDir, 'test-agent');
assert.ok(agent);
assert.strictEqual(agent.name, 'test-agent');
assert.ok(agent.body.includes('You are a test agent'));
} finally {
cleanupTempDir(tmpDir);
}
})) passed += 1; else failed += 1;
if (await test('lazyLoadAgent returns null for non-existent agent', async () => {
const tmpDir = createTempDir('ecc-agent-compress-');
try {
const agent = lazyLoadAgent(tmpDir, 'nonexistent');
assert.strictEqual(agent, null);
} finally {
cleanupTempDir(tmpDir);
}
})) passed += 1; else failed += 1;
if (await test('buildAgentCatalog works with real agents directory', async () => {
const agentsDir = path.join(__dirname, '..', '..', 'agents');
if (!fs.existsSync(agentsDir)) {
// Skip if agents dir doesn't exist (shouldn't happen in this repo)
return;
}
const result = buildAgentCatalog(agentsDir, { mode: 'catalog' });
assert.ok(result.agents.length > 0, 'Should find at least one agent');
assert.ok(result.stats.originalBytes > 0);
assert.ok(result.stats.compressedBytes < result.stats.originalBytes,
'Catalog mode should be smaller than full agent files');
})) passed += 1; else failed += 1;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,232 @@
/**
* Tests for inspection logic — pattern detection from failures.
*/
const assert = require('assert');
const {
normalizeFailureReason,
groupFailures,
detectPatterns,
generateReport,
suggestAction,
DEFAULT_FAILURE_THRESHOLD,
} = require('../../scripts/lib/inspection');
async function test(name, fn) {
try {
await fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function makeSkillRun(overrides = {}) {
return {
id: overrides.id || `run-${Math.random().toString(36).slice(2, 8)}`,
skillId: overrides.skillId || 'test-skill',
skillVersion: overrides.skillVersion || '1.0.0',
sessionId: overrides.sessionId || 'session-1',
taskDescription: overrides.taskDescription || 'test task',
outcome: overrides.outcome || 'failure',
failureReason: overrides.failureReason || 'generic error',
tokensUsed: overrides.tokensUsed || 500,
durationMs: overrides.durationMs || 1000,
userFeedback: overrides.userFeedback || null,
createdAt: overrides.createdAt || '2026-03-15T08:00:00.000Z',
};
}
async function runTests() {
console.log('\n=== Testing inspection ===\n');
let passed = 0;
let failed = 0;
if (await test('normalizeFailureReason strips timestamps and UUIDs', async () => {
const normalized = normalizeFailureReason(
'Error at 2026-03-15T08:00:00.000Z for id 550e8400-e29b-41d4-a716-446655440000'
);
assert.ok(!normalized.includes('2026'));
assert.ok(!normalized.includes('550e8400'));
assert.ok(normalized.includes('<timestamp>'));
assert.ok(normalized.includes('<uuid>'));
})) passed += 1; else failed += 1;
if (await test('normalizeFailureReason strips file paths', async () => {
const normalized = normalizeFailureReason('File not found: /usr/local/bin/node');
assert.ok(!normalized.includes('/usr/local'));
assert.ok(normalized.includes('<path>'));
})) passed += 1; else failed += 1;
if (await test('normalizeFailureReason handles null and empty values', async () => {
assert.strictEqual(normalizeFailureReason(null), 'unknown');
assert.strictEqual(normalizeFailureReason(''), 'unknown');
assert.strictEqual(normalizeFailureReason(undefined), 'unknown');
})) passed += 1; else failed += 1;
if (await test('groupFailures groups by skillId and normalized reason', async () => {
const runs = [
makeSkillRun({ id: 'r1', skillId: 'skill-a', failureReason: 'timeout' }),
makeSkillRun({ id: 'r2', skillId: 'skill-a', failureReason: 'timeout' }),
makeSkillRun({ id: 'r3', skillId: 'skill-b', failureReason: 'parse error' }),
makeSkillRun({ id: 'r4', skillId: 'skill-a', outcome: 'success' }), // should be excluded
];
const groups = groupFailures(runs);
assert.strictEqual(groups.size, 2);
const skillAGroup = groups.get('skill-a::timeout');
assert.ok(skillAGroup);
assert.strictEqual(skillAGroup.runs.length, 2);
const skillBGroup = groups.get('skill-b::parse error');
assert.ok(skillBGroup);
assert.strictEqual(skillBGroup.runs.length, 1);
})) passed += 1; else failed += 1;
if (await test('groupFailures handles mixed outcome casing', async () => {
const runs = [
makeSkillRun({ id: 'r1', outcome: 'FAILURE', failureReason: 'timeout' }),
makeSkillRun({ id: 'r2', outcome: 'Failed', failureReason: 'timeout' }),
makeSkillRun({ id: 'r3', outcome: 'error', failureReason: 'timeout' }),
];
const groups = groupFailures(runs);
assert.strictEqual(groups.size, 1);
const group = groups.values().next().value;
assert.strictEqual(group.runs.length, 3);
})) passed += 1; else failed += 1;
if (await test('detectPatterns returns empty array when below threshold', async () => {
const runs = [
makeSkillRun({ id: 'r1', failureReason: 'timeout' }),
makeSkillRun({ id: 'r2', failureReason: 'timeout' }),
];
const patterns = detectPatterns(runs, { threshold: 3 });
assert.strictEqual(patterns.length, 0);
})) passed += 1; else failed += 1;
if (await test('detectPatterns detects patterns at or above threshold', async () => {
const runs = [
makeSkillRun({ id: 'r1', failureReason: 'timeout', createdAt: '2026-03-15T08:00:00Z' }),
makeSkillRun({ id: 'r2', failureReason: 'timeout', createdAt: '2026-03-15T08:01:00Z' }),
makeSkillRun({ id: 'r3', failureReason: 'timeout', createdAt: '2026-03-15T08:02:00Z' }),
];
const patterns = detectPatterns(runs, { threshold: 3 });
assert.strictEqual(patterns.length, 1);
assert.strictEqual(patterns[0].count, 3);
assert.strictEqual(patterns[0].skillId, 'test-skill');
assert.strictEqual(patterns[0].normalizedReason, 'timeout');
assert.strictEqual(patterns[0].firstSeen, '2026-03-15T08:00:00Z');
assert.strictEqual(patterns[0].lastSeen, '2026-03-15T08:02:00Z');
assert.strictEqual(patterns[0].runIds.length, 3);
})) passed += 1; else failed += 1;
if (await test('detectPatterns uses default threshold', async () => {
const runs = Array.from({ length: DEFAULT_FAILURE_THRESHOLD }, (_, i) =>
makeSkillRun({ id: `r${i}`, failureReason: 'permission denied' })
);
const patterns = detectPatterns(runs);
assert.strictEqual(patterns.length, 1);
})) passed += 1; else failed += 1;
if (await test('detectPatterns sorts by count descending', async () => {
const runs = [
// 4 timeouts
...Array.from({ length: 4 }, (_, i) =>
makeSkillRun({ id: `t${i}`, skillId: 'skill-a', failureReason: 'timeout' })
),
// 3 parse errors
...Array.from({ length: 3 }, (_, i) =>
makeSkillRun({ id: `p${i}`, skillId: 'skill-b', failureReason: 'parse error' })
),
];
const patterns = detectPatterns(runs, { threshold: 3 });
assert.strictEqual(patterns.length, 2);
assert.strictEqual(patterns[0].count, 4);
assert.strictEqual(patterns[0].skillId, 'skill-a');
assert.strictEqual(patterns[1].count, 3);
assert.strictEqual(patterns[1].skillId, 'skill-b');
})) passed += 1; else failed += 1;
if (await test('detectPatterns groups similar failure reasons with different timestamps', async () => {
const runs = [
makeSkillRun({ id: 'r1', failureReason: 'Error at 2026-03-15T08:00:00Z in /tmp/foo' }),
makeSkillRun({ id: 'r2', failureReason: 'Error at 2026-03-15T09:00:00Z in /tmp/bar' }),
makeSkillRun({ id: 'r3', failureReason: 'Error at 2026-03-15T10:00:00Z in /tmp/baz' }),
];
const patterns = detectPatterns(runs, { threshold: 3 });
assert.strictEqual(patterns.length, 1);
assert.ok(patterns[0].normalizedReason.includes('<timestamp>'));
assert.ok(patterns[0].normalizedReason.includes('<path>'));
})) passed += 1; else failed += 1;
if (await test('detectPatterns tracks unique session IDs and versions', async () => {
const runs = [
makeSkillRun({ id: 'r1', sessionId: 'sess-1', skillVersion: '1.0.0', failureReason: 'err' }),
makeSkillRun({ id: 'r2', sessionId: 'sess-2', skillVersion: '1.0.0', failureReason: 'err' }),
makeSkillRun({ id: 'r3', sessionId: 'sess-1', skillVersion: '1.1.0', failureReason: 'err' }),
];
const patterns = detectPatterns(runs, { threshold: 3 });
assert.strictEqual(patterns.length, 1);
assert.deepStrictEqual(patterns[0].sessionIds.sort(), ['sess-1', 'sess-2']);
assert.deepStrictEqual(patterns[0].versions.sort(), ['1.0.0', '1.1.0']);
})) passed += 1; else failed += 1;
if (await test('generateReport returns clean status with no patterns', async () => {
const report = generateReport([]);
assert.strictEqual(report.status, 'clean');
assert.strictEqual(report.patternCount, 0);
assert.ok(report.summary.includes('No recurring'));
assert.ok(report.generatedAt);
})) passed += 1; else failed += 1;
if (await test('generateReport produces structured report from patterns', async () => {
const runs = [
...Array.from({ length: 3 }, (_, i) =>
makeSkillRun({ id: `r${i}`, skillId: 'my-skill', failureReason: 'timeout' })
),
];
const patterns = detectPatterns(runs, { threshold: 3 });
const report = generateReport(patterns, { generatedAt: '2026-03-15T09:00:00Z' });
assert.strictEqual(report.status, 'attention_needed');
assert.strictEqual(report.patternCount, 1);
assert.strictEqual(report.totalFailures, 3);
assert.deepStrictEqual(report.affectedSkills, ['my-skill']);
assert.strictEqual(report.patterns[0].skillId, 'my-skill');
assert.ok(report.patterns[0].suggestedAction);
assert.strictEqual(report.generatedAt, '2026-03-15T09:00:00Z');
})) passed += 1; else failed += 1;
if (await test('suggestAction returns timeout-specific advice', async () => {
const action = suggestAction({ normalizedReason: 'timeout after 30s', versions: ['1.0.0'] });
assert.ok(action.toLowerCase().includes('timeout'));
})) passed += 1; else failed += 1;
if (await test('suggestAction returns permission-specific advice', async () => {
const action = suggestAction({ normalizedReason: 'permission denied', versions: ['1.0.0'] });
assert.ok(action.toLowerCase().includes('permission'));
})) passed += 1; else failed += 1;
if (await test('suggestAction returns version-span advice when multiple versions affected', async () => {
const action = suggestAction({ normalizedReason: 'something broke', versions: ['1.0.0', '1.1.0'] });
assert.ok(action.toLowerCase().includes('version'));
})) passed += 1; else failed += 1;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,247 @@
/**
* Tests for scripts/lib/resolve-ecc-root.js
*
* Covers the ECC root resolution fallback chain:
* 1. CLAUDE_PLUGIN_ROOT env var
* 2. Standard install (~/.claude/)
* 3. Plugin cache auto-detection
* 4. Fallback to ~/.claude/
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { resolveEccRoot, INLINE_RESOLVE } = require('../../scripts/lib/resolve-ecc-root');
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function createTempDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-root-test-'));
}
function setupStandardInstall(homeDir) {
const claudeDir = path.join(homeDir, '.claude');
const scriptDir = path.join(claudeDir, 'scripts', 'lib');
fs.mkdirSync(scriptDir, { recursive: true });
fs.writeFileSync(path.join(scriptDir, 'utils.js'), '// stub');
return claudeDir;
}
function setupPluginCache(homeDir, orgName, version) {
const cacheDir = path.join(
homeDir, '.claude', 'plugins', 'cache',
'everything-claude-code', orgName, version
);
const scriptDir = path.join(cacheDir, 'scripts', 'lib');
fs.mkdirSync(scriptDir, { recursive: true });
fs.writeFileSync(path.join(scriptDir, 'utils.js'), '// stub');
return cacheDir;
}
function runTests() {
console.log('\n=== Testing resolve-ecc-root.js ===\n');
let passed = 0;
let failed = 0;
// ─── Env Var Priority ───
if (test('returns CLAUDE_PLUGIN_ROOT when set', () => {
const result = resolveEccRoot({ envRoot: '/custom/plugin/root' });
assert.strictEqual(result, '/custom/plugin/root');
})) passed++; else failed++;
if (test('trims whitespace from CLAUDE_PLUGIN_ROOT', () => {
const result = resolveEccRoot({ envRoot: ' /trimmed/root ' });
assert.strictEqual(result, '/trimmed/root');
})) passed++; else failed++;
if (test('skips empty CLAUDE_PLUGIN_ROOT', () => {
const homeDir = createTempDir();
try {
setupStandardInstall(homeDir);
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, path.join(homeDir, '.claude'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('skips whitespace-only CLAUDE_PLUGIN_ROOT', () => {
const homeDir = createTempDir();
try {
setupStandardInstall(homeDir);
const result = resolveEccRoot({ envRoot: ' ', homeDir });
assert.strictEqual(result, path.join(homeDir, '.claude'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── Standard Install ───
if (test('finds standard install at ~/.claude/', () => {
const homeDir = createTempDir();
try {
setupStandardInstall(homeDir);
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, path.join(homeDir, '.claude'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── Plugin Cache Auto-Detection ───
if (test('discovers plugin root from cache directory', () => {
const homeDir = createTempDir();
try {
const expected = setupPluginCache(homeDir, 'everything-claude-code', '1.8.0');
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, expected);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('prefers standard install over plugin cache', () => {
const homeDir = createTempDir();
try {
const claudeDir = setupStandardInstall(homeDir);
setupPluginCache(homeDir, 'everything-claude-code', '1.8.0');
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, claudeDir,
'Standard install should take precedence over plugin cache');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('handles multiple versions in plugin cache', () => {
const homeDir = createTempDir();
try {
setupPluginCache(homeDir, 'everything-claude-code', '1.7.0');
const expected = setupPluginCache(homeDir, 'everything-claude-code', '1.8.0');
const result = resolveEccRoot({ envRoot: '', homeDir });
// Should find one of them (either is valid)
assert.ok(
result === expected ||
result === path.join(homeDir, '.claude', 'plugins', 'cache', 'everything-claude-code', 'everything-claude-code', '1.7.0'),
'Should resolve to a valid plugin cache directory'
);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── Fallback ───
if (test('falls back to ~/.claude/ when nothing is found', () => {
const homeDir = createTempDir();
try {
// Create ~/.claude but don't put scripts there
fs.mkdirSync(path.join(homeDir, '.claude'), { recursive: true });
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, path.join(homeDir, '.claude'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('falls back gracefully when ~/.claude/ does not exist', () => {
const homeDir = createTempDir();
try {
const result = resolveEccRoot({ envRoot: '', homeDir });
assert.strictEqual(result, path.join(homeDir, '.claude'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── Custom Probe ───
if (test('supports custom probe path', () => {
const homeDir = createTempDir();
try {
const claudeDir = path.join(homeDir, '.claude');
fs.mkdirSync(path.join(claudeDir, 'custom'), { recursive: true });
fs.writeFileSync(path.join(claudeDir, 'custom', 'marker.js'), '// probe');
const result = resolveEccRoot({
envRoot: '',
homeDir,
probe: path.join('custom', 'marker.js'),
});
assert.strictEqual(result, claudeDir);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── INLINE_RESOLVE ───
if (test('INLINE_RESOLVE is a non-empty string', () => {
assert.ok(typeof INLINE_RESOLVE === 'string');
assert.ok(INLINE_RESOLVE.length > 50, 'Should be a substantial inline expression');
})) passed++; else failed++;
if (test('INLINE_RESOLVE returns CLAUDE_PLUGIN_ROOT when set', () => {
const { execFileSync } = require('child_process');
const result = execFileSync('node', [
'-e', `console.log(${INLINE_RESOLVE})`,
], {
env: { ...process.env, CLAUDE_PLUGIN_ROOT: '/inline/test/root' },
encoding: 'utf8',
}).trim();
assert.strictEqual(result, '/inline/test/root');
})) passed++; else failed++;
if (test('INLINE_RESOLVE discovers plugin cache when env var is unset', () => {
const homeDir = createTempDir();
try {
const expected = setupPluginCache(homeDir, 'everything-claude-code', '1.9.0');
const { execFileSync } = require('child_process');
const result = execFileSync('node', [
'-e', `console.log(${INLINE_RESOLVE})`,
], {
env: { PATH: process.env.PATH, HOME: homeDir },
encoding: 'utf8',
}).trim();
assert.strictEqual(result, expected);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('INLINE_RESOLVE falls back to ~/.claude/ when nothing found', () => {
const homeDir = createTempDir();
try {
const { execFileSync } = require('child_process');
const result = execFileSync('node', [
'-e', `console.log(${INLINE_RESOLVE})`,
], {
env: { PATH: process.env.PATH, HOME: homeDir },
encoding: 'utf8',
}).trim();
assert.strictEqual(result, path.join(homeDir, '.claude'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,718 @@
/**
* Tests for --with / --without selective install flags (issue #470)
*
* Covers:
* - CLI argument parsing for --with and --without
* - Request normalization with include/exclude component IDs
* - Component-to-module expansion via the manifest catalog
* - End-to-end install plans with --with and --without
* - Validation and error handling for unknown component IDs
* - Combined --profile + --with + --without flows
* - Standalone --with without a profile
* - agent: and skill: component families
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
parseInstallArgs,
normalizeInstallRequest,
} = require('../../scripts/lib/install/request');
const {
loadInstallManifests,
listInstallComponents,
resolveInstallPlan,
} = require('../../scripts/lib/install-manifests');
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing --with / --without selective install flags ===\n');
let passed = 0;
let failed = 0;
// ─── CLI Argument Parsing ───
if (test('parses single --with flag', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'core',
'--with', 'lang:typescript',
]);
assert.deepStrictEqual(parsed.includeComponentIds, ['lang:typescript']);
assert.deepStrictEqual(parsed.excludeComponentIds, []);
})) passed++; else failed++;
if (test('parses single --without flag', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'developer',
'--without', 'capability:orchestration',
]);
assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:orchestration']);
assert.deepStrictEqual(parsed.includeComponentIds, []);
})) passed++; else failed++;
if (test('parses multiple --with flags', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--with', 'lang:typescript',
'--with', 'framework:nextjs',
'--with', 'capability:database',
]);
assert.deepStrictEqual(parsed.includeComponentIds, [
'lang:typescript',
'framework:nextjs',
'capability:database',
]);
})) passed++; else failed++;
if (test('parses multiple --without flags', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'full',
'--without', 'capability:media',
'--without', 'capability:social',
]);
assert.deepStrictEqual(parsed.excludeComponentIds, [
'capability:media',
'capability:social',
]);
})) passed++; else failed++;
if (test('parses combined --with and --without flags', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'developer',
'--with', 'lang:typescript',
'--with', 'framework:nextjs',
'--without', 'capability:orchestration',
]);
assert.strictEqual(parsed.profileId, 'developer');
assert.deepStrictEqual(parsed.includeComponentIds, ['lang:typescript', 'framework:nextjs']);
assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:orchestration']);
})) passed++; else failed++;
if (test('ignores empty --with values', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--with', '',
'--with', 'lang:python',
]);
assert.deepStrictEqual(parsed.includeComponentIds, ['lang:python']);
})) passed++; else failed++;
if (test('ignores empty --without values', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'core',
'--without', '',
'--without', 'capability:media',
]);
assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:media']);
})) passed++; else failed++;
// ─── Request Normalization ───
if (test('normalizes --with-only request as manifest mode', () => {
const request = normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: ['lang:typescript'],
excludeComponentIds: [],
languages: [],
});
assert.strictEqual(request.mode, 'manifest');
assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript']);
assert.deepStrictEqual(request.excludeComponentIds, []);
})) passed++; else failed++;
if (test('normalizes --profile + --with + --without as manifest mode', () => {
const request = normalizeInstallRequest({
target: 'cursor',
profileId: 'developer',
moduleIds: [],
includeComponentIds: ['lang:typescript', 'framework:nextjs'],
excludeComponentIds: ['capability:orchestration'],
languages: [],
});
assert.strictEqual(request.mode, 'manifest');
assert.strictEqual(request.profileId, 'developer');
assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript', 'framework:nextjs']);
assert.deepStrictEqual(request.excludeComponentIds, ['capability:orchestration']);
})) passed++; else failed++;
if (test('rejects --with combined with legacy language arguments', () => {
assert.throws(
() => normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: ['lang:typescript'],
excludeComponentIds: [],
languages: ['python'],
}),
/cannot be combined/
);
})) passed++; else failed++;
if (test('rejects --without combined with legacy language arguments', () => {
assert.throws(
() => normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: [],
excludeComponentIds: ['capability:media'],
languages: ['typescript'],
}),
/cannot be combined/
);
})) passed++; else failed++;
if (test('deduplicates repeated --with component IDs', () => {
const request = normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: ['lang:typescript', 'lang:typescript', 'lang:python'],
excludeComponentIds: [],
languages: [],
});
assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript', 'lang:python']);
})) passed++; else failed++;
if (test('deduplicates repeated --without component IDs', () => {
const request = normalizeInstallRequest({
target: 'claude',
profileId: 'full',
moduleIds: [],
includeComponentIds: [],
excludeComponentIds: ['capability:media', 'capability:media', 'capability:social'],
languages: [],
});
assert.deepStrictEqual(request.excludeComponentIds, ['capability:media', 'capability:social']);
})) passed++; else failed++;
// ─── Component Catalog Validation ───
if (test('component catalog includes lang: family entries', () => {
const components = listInstallComponents({ family: 'language' });
assert.ok(components.some(c => c.id === 'lang:typescript'), 'Should have lang:typescript');
assert.ok(components.some(c => c.id === 'lang:python'), 'Should have lang:python');
assert.ok(components.some(c => c.id === 'lang:go'), 'Should have lang:go');
assert.ok(components.some(c => c.id === 'lang:java'), 'Should have lang:java');
})) passed++; else failed++;
if (test('component catalog includes framework: family entries', () => {
const components = listInstallComponents({ family: 'framework' });
assert.ok(components.some(c => c.id === 'framework:react'), 'Should have framework:react');
assert.ok(components.some(c => c.id === 'framework:nextjs'), 'Should have framework:nextjs');
assert.ok(components.some(c => c.id === 'framework:django'), 'Should have framework:django');
assert.ok(components.some(c => c.id === 'framework:springboot'), 'Should have framework:springboot');
})) passed++; else failed++;
if (test('component catalog includes capability: family entries', () => {
const components = listInstallComponents({ family: 'capability' });
assert.ok(components.some(c => c.id === 'capability:database'), 'Should have capability:database');
assert.ok(components.some(c => c.id === 'capability:security'), 'Should have capability:security');
assert.ok(components.some(c => c.id === 'capability:orchestration'), 'Should have capability:orchestration');
})) passed++; else failed++;
if (test('component catalog includes agent: family entries', () => {
const components = listInstallComponents({ family: 'agent' });
assert.ok(components.length > 0, 'Should have at least one agent component');
assert.ok(components.some(c => c.id === 'agent:security-reviewer'), 'Should have agent:security-reviewer');
})) passed++; else failed++;
if (test('component catalog includes skill: family entries', () => {
const components = listInstallComponents({ family: 'skill' });
assert.ok(components.length > 0, 'Should have at least one skill component');
assert.ok(components.some(c => c.id === 'skill:continuous-learning'), 'Should have skill:continuous-learning');
})) passed++; else failed++;
// ─── Install Plan Resolution with --with ───
if (test('--with alone resolves component modules and their dependencies', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['lang:typescript'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('framework-language'),
'Should include the module behind lang:typescript');
assert.ok(plan.selectedModuleIds.includes('rules-core'),
'Should include framework-language dependency rules-core');
assert.ok(plan.selectedModuleIds.includes('platform-configs'),
'Should include framework-language dependency platform-configs');
})) passed++; else failed++;
if (test('--with adds modules on top of a profile', () => {
const plan = resolveInstallPlan({
profileId: 'core',
includeComponentIds: ['capability:security'],
target: 'claude',
});
// core profile modules
assert.ok(plan.selectedModuleIds.includes('rules-core'));
assert.ok(plan.selectedModuleIds.includes('workflow-quality'));
// added by --with
assert.ok(plan.selectedModuleIds.includes('security'),
'Should include security module from --with');
})) passed++; else failed++;
if (test('multiple --with flags union their modules', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['lang:typescript', 'capability:database'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('framework-language'),
'Should include framework-language from lang:typescript');
assert.ok(plan.selectedModuleIds.includes('database'),
'Should include database from capability:database');
})) passed++; else failed++;
// ─── Install Plan Resolution with --without ───
if (test('--without excludes modules from a profile', () => {
const plan = resolveInstallPlan({
profileId: 'developer',
excludeComponentIds: ['capability:orchestration'],
target: 'claude',
});
assert.ok(!plan.selectedModuleIds.includes('orchestration'),
'Should exclude orchestration module');
assert.ok(plan.excludedModuleIds.includes('orchestration'),
'Should report orchestration as excluded');
// rest of developer profile should remain
assert.ok(plan.selectedModuleIds.includes('rules-core'));
assert.ok(plan.selectedModuleIds.includes('framework-language'));
assert.ok(plan.selectedModuleIds.includes('database'));
})) passed++; else failed++;
if (test('multiple --without flags exclude multiple modules', () => {
const plan = resolveInstallPlan({
profileId: 'full',
excludeComponentIds: ['capability:media', 'capability:social', 'capability:supply-chain'],
target: 'claude',
});
assert.ok(!plan.selectedModuleIds.includes('media-generation'));
assert.ok(!plan.selectedModuleIds.includes('social-distribution'));
assert.ok(!plan.selectedModuleIds.includes('supply-chain-domain'));
assert.ok(plan.excludedModuleIds.includes('media-generation'));
assert.ok(plan.excludedModuleIds.includes('social-distribution'));
assert.ok(plan.excludedModuleIds.includes('supply-chain-domain'));
})) passed++; else failed++;
// ─── Combined --with + --without ───
if (test('--with and --without work together on a profile', () => {
const plan = resolveInstallPlan({
profileId: 'developer',
includeComponentIds: ['capability:security'],
excludeComponentIds: ['capability:orchestration'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('security'),
'Should include security from --with');
assert.ok(!plan.selectedModuleIds.includes('orchestration'),
'Should exclude orchestration from --without');
assert.ok(plan.selectedModuleIds.includes('rules-core'),
'Should keep profile base modules');
})) passed++; else failed++;
if (test('--without on a dependency of --with raises an error', () => {
assert.throws(
() => resolveInstallPlan({
includeComponentIds: ['capability:social'],
excludeComponentIds: ['capability:content'],
}),
/depends on excluded module/
);
})) passed++; else failed++;
// ─── Validation Errors ───
if (test('throws for unknown component ID in --with', () => {
assert.throws(
() => resolveInstallPlan({
includeComponentIds: ['lang:brainfuck-plus-plus'],
}),
/Unknown install component/
);
})) passed++; else failed++;
if (test('throws for unknown component ID in --without', () => {
assert.throws(
() => resolveInstallPlan({
profileId: 'core',
excludeComponentIds: ['capability:teleportation'],
}),
/Unknown install component/
);
})) passed++; else failed++;
if (test('throws when all modules are excluded', () => {
assert.throws(
() => resolveInstallPlan({
profileId: 'core',
excludeComponentIds: [
'baseline:rules',
'baseline:agents',
'baseline:commands',
'baseline:hooks',
'baseline:platform',
'baseline:workflow',
],
target: 'claude',
}),
/excludes every requested install module/
);
})) passed++; else failed++;
// ─── Target-Specific Behavior ───
if (test('--with respects target compatibility filtering', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['capability:orchestration'],
target: 'cursor',
});
// orchestration module only supports claude, codex, opencode
assert.ok(!plan.selectedModuleIds.includes('orchestration'),
'Should skip orchestration for cursor target');
assert.ok(plan.skippedModuleIds.includes('orchestration'),
'Should report orchestration as skipped for cursor');
})) passed++; else failed++;
if (test('--without with agent: component excludes the agent module', () => {
const plan = resolveInstallPlan({
profileId: 'core',
excludeComponentIds: ['agent:security-reviewer'],
target: 'claude',
});
// agent:security-reviewer maps to agents-core module
// Since core profile includes agents-core and it is excluded, it should be gone
assert.ok(!plan.selectedModuleIds.includes('agents-core'),
'Should exclude agents-core when agent:security-reviewer is excluded');
assert.ok(plan.excludedModuleIds.includes('agents-core'),
'Should report agents-core as excluded');
})) passed++; else failed++;
if (test('--with agent: component includes the agents-core module', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['agent:security-reviewer'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('agents-core'),
'Should include agents-core module from agent:security-reviewer');
})) passed++; else failed++;
if (test('--with skill: component includes the parent skill module', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['skill:continuous-learning'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('workflow-quality'),
'Should include workflow-quality module from skill:continuous-learning');
})) passed++; else failed++;
// ─── Help Text ───
if (test('help text documents --with and --without flags', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const result = execFileSync('node', [scriptPath, '--help'], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
assert.ok(result.includes('--with'), 'Help should mention --with');
assert.ok(result.includes('--without'), 'Help should mention --without');
assert.ok(result.includes('component'), 'Help should describe components');
})) passed++; else failed++;
// ─── End-to-End Dry-Run ───
if (test('end-to-end: --profile developer --with capability:security --without capability:orchestration --dry-run', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-project-'));
try {
const result = execFileSync('node', [
scriptPath,
'--profile', 'developer',
'--with', 'capability:security',
'--without', 'capability:orchestration',
'--dry-run',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
assert.ok(result.includes('Mode: manifest'), 'Should be manifest mode');
assert.ok(result.includes('Profile: developer'), 'Should show developer profile');
assert.ok(result.includes('capability:security'), 'Should show included component');
assert.ok(result.includes('capability:orchestration'), 'Should show excluded component');
assert.ok(result.includes('security'), 'Selected modules should include security');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('end-to-end: --with lang:python --with agent:security-reviewer --dry-run', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-project-'));
try {
const result = execFileSync('node', [
scriptPath,
'--with', 'lang:python',
'--with', 'agent:security-reviewer',
'--dry-run',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
assert.ok(result.includes('Mode: manifest'), 'Should be manifest mode');
assert.ok(result.includes('lang:python'), 'Should show lang:python as included');
assert.ok(result.includes('agent:security-reviewer'), 'Should show agent:security-reviewer as included');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('end-to-end: --with with unknown component fails cleanly', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
let exitCode = 0;
let stderr = '';
try {
execFileSync('node', [
scriptPath,
'--with', 'lang:nonexistent-language',
'--dry-run',
], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (error) {
exitCode = error.status || 1;
stderr = error.stderr || '';
}
assert.strictEqual(exitCode, 1, 'Should exit with error code 1');
assert.ok(stderr.includes('Unknown install component'), 'Should report unknown component');
})) passed++; else failed++;
if (test('end-to-end: --without with unknown component fails cleanly', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
let exitCode = 0;
let stderr = '';
try {
execFileSync('node', [
scriptPath,
'--profile', 'core',
'--without', 'capability:nonexistent',
'--dry-run',
], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (error) {
exitCode = error.status || 1;
stderr = error.stderr || '';
}
assert.strictEqual(exitCode, 1, 'Should exit with error code 1');
assert.ok(stderr.includes('Unknown install component'), 'Should report unknown component');
})) passed++; else failed++;
// ─── End-to-End Actual Install ───
if (test('end-to-end: installs --profile core --with capability:security and writes state', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-project-'));
try {
const result = execFileSync('node', [
scriptPath,
'--profile', 'core',
'--with', 'capability:security',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const claudeRoot = path.join(homeDir, '.claude');
// Security skill should be installed (from --with)
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'security-review', 'SKILL.md')),
'Should install security-review skill from --with');
// Core profile modules should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')),
'Should install core rules');
// Install state should record include/exclude
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
assert.strictEqual(state.request.profile, 'core');
assert.deepStrictEqual(state.request.includeComponents, ['capability:security']);
assert.deepStrictEqual(state.request.excludeComponents, []);
assert.ok(state.resolution.selectedModules.includes('security'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('end-to-end: installs --profile developer --without capability:orchestration and state reflects exclusion', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-project-'));
try {
execFileSync('node', [
scriptPath,
'--profile', 'developer',
'--without', 'capability:orchestration',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const claudeRoot = path.join(homeDir, '.claude');
// Orchestration skills should NOT be installed (from --without)
assert.ok(!fs.existsSync(path.join(claudeRoot, 'skills', 'dmux-workflows', 'SKILL.md')),
'Should not install orchestration skills');
// Developer profile base modules should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')),
'Should install core rules');
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'tdd-workflow', 'SKILL.md')),
'Should install workflow skills');
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
assert.strictEqual(state.request.profile, 'developer');
assert.deepStrictEqual(state.request.excludeComponents, ['capability:orchestration']);
assert.ok(!state.resolution.selectedModules.includes('orchestration'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('end-to-end: --with alone (no profile) installs just the component modules', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-project-'));
try {
execFileSync('node', [
scriptPath,
'--with', 'lang:typescript',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const claudeRoot = path.join(homeDir, '.claude');
// framework-language skill (from lang:typescript) should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'coding-standards', 'SKILL.md')),
'Should install framework-language skills');
// Its dependencies should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')),
'Should install dependency rules-core');
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
assert.strictEqual(state.request.profile, null);
assert.deepStrictEqual(state.request.includeComponents, ['lang:typescript']);
assert.ok(state.resolution.selectedModules.includes('framework-language'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── JSON output mode ───
if (test('end-to-end: --dry-run --json includes component selections in output', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-project-'));
try {
const output = execFileSync('node', [
scriptPath,
'--profile', 'core',
'--with', 'capability:database',
'--without', 'baseline:hooks',
'--dry-run',
'--json',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const json = JSON.parse(output);
assert.strictEqual(json.dryRun, true);
assert.ok(json.plan, 'Should include plan object');
assert.ok(
json.plan.includedComponentIds.includes('capability:database'),
'JSON output should include capability:database in included components'
);
assert.ok(
json.plan.excludedComponentIds.includes('baseline:hooks'),
'JSON output should include baseline:hooks in excluded components'
);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -12,7 +12,7 @@ const { spawnSync } = require('child_process');
const dashboard = require('../../scripts/lib/skill-evolution/dashboard');
const versioning = require('../../scripts/lib/skill-evolution/versioning');
const provenance = require('../../scripts/lib/skill-evolution/provenance');
const _provenance = require('../../scripts/lib/skill-evolution/provenance');
const HEALTH_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'skills-health.js');

View File

@@ -2424,6 +2424,65 @@ function runTests() {
}
})) passed++; else failed++;
// ─── stripAnsi ───
console.log('\nstripAnsi:');
if (test('strips SGR color codes (\\x1b[...m)', () => {
assert.strictEqual(utils.stripAnsi('\x1b[31mRed text\x1b[0m'), 'Red text');
assert.strictEqual(utils.stripAnsi('\x1b[1;36mBold cyan\x1b[0m'), 'Bold cyan');
})) passed++; else failed++;
if (test('strips cursor movement sequences (\\x1b[H, \\x1b[2J, \\x1b[3J)', () => {
// These are the exact sequences reported in issue #642
assert.strictEqual(utils.stripAnsi('\x1b[H\x1b[2J\x1b[3JHello'), 'Hello');
assert.strictEqual(utils.stripAnsi('before\x1b[Hafter'), 'beforeafter');
})) passed++; else failed++;
if (test('strips cursor position sequences (\\x1b[row;colH)', () => {
assert.strictEqual(utils.stripAnsi('\x1b[5;10Hplaced'), 'placed');
})) passed++; else failed++;
if (test('strips erase line sequences (\\x1b[K, \\x1b[2K)', () => {
assert.strictEqual(utils.stripAnsi('line\x1b[Kend'), 'lineend');
assert.strictEqual(utils.stripAnsi('line\x1b[2Kend'), 'lineend');
})) passed++; else failed++;
if (test('strips OSC sequences (window title, hyperlinks)', () => {
// OSC terminated by BEL (\x07)
assert.strictEqual(utils.stripAnsi('\x1b]0;My Title\x07content'), 'content');
// OSC terminated by ST (\x1b\\)
assert.strictEqual(utils.stripAnsi('\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\'), 'link');
})) passed++; else failed++;
if (test('strips charset selection (\\x1b(B)', () => {
assert.strictEqual(utils.stripAnsi('\x1b(Bnormal'), 'normal');
})) passed++; else failed++;
if (test('strips bare ESC + letter (\\x1bM reverse index)', () => {
assert.strictEqual(utils.stripAnsi('line\x1bMup'), 'lineup');
})) passed++; else failed++;
if (test('handles mixed ANSI sequences in one string', () => {
const input = '\x1b[H\x1b[2J\x1b[1;36mSession\x1b[0m summary\x1b[K';
assert.strictEqual(utils.stripAnsi(input), 'Session summary');
})) passed++; else failed++;
if (test('returns empty string for non-string input', () => {
assert.strictEqual(utils.stripAnsi(null), '');
assert.strictEqual(utils.stripAnsi(undefined), '');
assert.strictEqual(utils.stripAnsi(42), '');
})) passed++; else failed++;
if (test('preserves string with no ANSI codes', () => {
assert.strictEqual(utils.stripAnsi('plain text'), 'plain text');
assert.strictEqual(utils.stripAnsi(''), '');
})) passed++; else failed++;
if (test('handles CSI with question mark parameter (DEC private modes)', () => {
// e.g. \x1b[?25h (show cursor), \x1b[?25l (hide cursor)
assert.strictEqual(utils.stripAnsi('\x1b[?25hvisible\x1b[?25l'), 'visible');
})) passed++; else failed++;
// Summary
console.log('\n=== Test Results ===');
console.log(`Passed: ${passed}`);