mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-18 05:57:40 +02:00
Merge origin/main into feat/2885-agent-security-hardening
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "ecc",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: agent-introspection-debugging
|
||||
description: Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Agent Introspection Debugging
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: agent-sort
|
||||
description: Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Agent Sort
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: api-design
|
||||
description: REST API design patterns including resource naming, status codes, pagination, filtering, error responses, versioning, and rate limiting for production APIs. Use when designing or reviewing REST endpoints, resource names, status codes, pagination, or versioning.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# API Design Patterns
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: article-writing
|
||||
description: Write articles, guides, blog posts, tutorials, newsletter issues, and other long-form content in a distinctive voice derived from supplied examples or brand guidance. Use when the user wants polished written content longer than a paragraph, especially when voice consistency, structure, and credibility matter.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Article Writing
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: backend-patterns
|
||||
description: Backend architecture patterns, API design, database optimization, and server-side best practices for Node.js, Express, and Next.js API routes. Use when building or reviewing Node.js, Express, or Next.js API routes and their data access.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Backend Development Patterns
|
||||
|
||||
@@ -6,6 +6,7 @@ description: >-
|
||||
visual craft, offer packaging, evidence, enterprise-readiness, thought
|
||||
leadership, pricing, client's strategic tension) with explicit 1–5 rubrics
|
||||
and a tension-plot. Precedes competitive-report-structure.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Benchmark Methodology
|
||||
|
||||
@@ -6,6 +6,7 @@ description: >-
|
||||
personality, voice, narrative, and founder-brand tension across 8 modules
|
||||
using laddering, 5 Whys, and projective techniques. Produces a resumable
|
||||
session with disk-persisted state and a master brandbook (90_SYNTHESIS.md).
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Brand Discovery
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: brand-voice
|
||||
description: Build a source-derived writing style profile from real posts, essays, launch notes, docs, or site copy, then reuse that profile across content, outreach, and social workflows. Use when the user wants voice consistency without generic AI writing tropes.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Brand Voice
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: bun-runtime
|
||||
description: Bun as runtime, package manager, bundler, and test runner. When to choose Bun vs Node, migration notes, and Vercel support.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Bun Runtime
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: coding-standards
|
||||
description: Baseline cross-project coding conventions for naming, readability, immutability, and code-quality review. Use detailed frontend or backend skills for framework-specific patterns. Use when reviewing code quality or naming with no framework-specific skill that applies.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Coding Standards & Best Practices
|
||||
|
||||
@@ -6,6 +6,7 @@ description: >-
|
||||
counts as a competitor, which tier they belong to, and which sources to mine.
|
||||
First step in the three-skill competitive pipeline; precedes
|
||||
benchmark-methodology.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Competitive Platform Analysis
|
||||
|
||||
@@ -6,6 +6,7 @@ description: >-
|
||||
profiles, benchmarking matrix, white-space analysis, strategic recommendations,
|
||||
and team alignment trigger questions. Final step in the three-skill competitive
|
||||
pipeline.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Competitive Report Structure
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: content-engine
|
||||
description: Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Content Engine
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: crosspost
|
||||
description: Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Crosspost
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: deep-research
|
||||
description: Multi-source deep research using firecrawl and exa MCPs. Searches the web, synthesizes findings, and delivers cited reports with source attribution. Use when the user wants thorough research on any topic with evidence and citations.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Deep Research
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: dmux-workflows
|
||||
description: Multi-agent orchestration using dmux (tmux pane manager for AI agents). Patterns for parallel agent workflows across Claude Code, Codex, OpenCode, and other harnesses. Use when running multiple agent sessions in parallel or coordinating multi-agent development workflows.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# dmux Workflows
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: documentation-lookup
|
||||
description: Use up-to-date library and framework docs via Context7 MCP instead of training data. Activates for setup questions, API references, code examples, or when the user names a framework (e.g. React, Next.js, Prisma).
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Documentation Lookup (Context7)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: e2e-testing
|
||||
description: Playwright E2E testing patterns, Page Object Model, configuration, CI/CD integration, artifact management, and flaky test strategies. Use when writing Playwright tests, structuring page objects, or fixing flaky E2E runs in CI.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# E2E Testing Patterns
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name: eval-harness
|
||||
description: Formal evaluation framework for Claude Code sessions implementing eval-driven development (EDD) principles. Use when a Claude Code workflow needs a formal eval before it is trusted or changed.
|
||||
allowed-tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Eval Harness Skill
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: everything-claude-code
|
||||
description: Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Everything Claude Code Conventions
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: exa-search
|
||||
description: Neural search via Exa MCP for web, code, and company research. Use when the user needs web search, code examples, company intel, people lookup, or AI-powered deep research with Exa's neural search engine.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Exa Search
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: fal-ai-media
|
||||
description: Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# fal.ai Media Generation
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: frontend-patterns
|
||||
description: Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices. Use when building or reviewing React or Next.js components, state, or render performance.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Frontend Development Patterns
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: frontend-slides
|
||||
description: Create stunning, animation-rich HTML presentations from scratch or by converting PowerPoint files. Use when the user wants to build a presentation, convert a PPT/PPTX to web, or create slides for a talk/pitch. Helps non-designers discover their aesthetic through visual exploration rather than abstract choices.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Frontend Slides
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: investor-materials
|
||||
description: Create and update pitch decks, one-pagers, investor memos, accelerator applications, financial models, and fundraising materials. Use when the user needs investor-facing documents, projections, use-of-funds tables, milestone plans, or materials that must stay internally consistent across multiple fundraising assets.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Investor Materials
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: investor-outreach
|
||||
description: Draft cold emails, warm intro blurbs, follow-ups, update emails, and investor communications for fundraising. Use when the user wants outreach to angels, VCs, strategic investors, or accelerators and needs concise, personalized, investor-facing messaging.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Investor Outreach
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: market-research
|
||||
description: Conduct market research, competitive analysis, investor due diligence, and industry intelligence with source attribution and decision-oriented summaries. Use when the user wants market sizing, competitor comparisons, fund research, technology scans, or research that informs business decisions.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Market Research
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: mcp-server-patterns
|
||||
description: Build MCP servers with Node/TypeScript SDK — tools, resources, prompts, Zod validation, stdio vs Streamable HTTP. Use Context7 or official MCP docs for latest API. Use when building or debugging an MCP server — tools, resources, prompts, validation, or transport choice.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# MCP Server Patterns
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name: mle-workflow
|
||||
description: Production machine-learning engineering workflow for data contracts, reproducible training, model evaluation, deployment, monitoring, and rollback. Use when building, reviewing, or hardening ML systems beyond one-off notebooks.
|
||||
allowed-tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Machine Learning Engineering Workflow
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: nextjs-turbopack
|
||||
description: Next.js 16+ and Turbopack — incremental bundling, FS caching, dev speed, and when to use Turbopack vs webpack.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Next.js and Turbopack
|
||||
|
||||
@@ -3,6 +3,7 @@ name: plan-canvas
|
||||
description: Open plans and HTML artifacts in a local browser canvas where the human annotates elements, chats, and approves or requests changes without leaving the page. Use when presenting a plan for review, or when feedback like "move this, change that" is easier pointed at than typed.
|
||||
metadata:
|
||||
origin: ECC
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Plan Canvas
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: product-capability
|
||||
description: Translate PRD intent, roadmap asks, or product discussions into an implementation-ready capability plan that exposes constraints, invariants, interfaces, and unresolved decisions before multi-service work starts. Use when the user needs an ECC-native PRD-to-SRS lane instead of vague planning prose.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Product Capability
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: security-review
|
||||
description: Use this skill when adding authentication, handling user input, working with secrets, creating API endpoints, or implementing payment/sensitive features. Provides comprehensive security checklist and patterns.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Security Review Skill
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: strategic-compact
|
||||
description: Suggests manual context compaction at logical intervals to preserve context through task phases rather than arbitrary auto-compaction. Use when a session is approaching a context limit and a task phase is a natural place to compact.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Strategic Compact Skill
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: tdd-workflow
|
||||
description: Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage including unit, integration, and E2E tests.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Test-Driven Development Workflow
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: unified-memory
|
||||
description: Share durable, inspectable context and handoffs between Claude, Codex, Hermes, Cursor, OpenCode, and other agents through the local ECC Memory Vault. Use when an agent must save work state, transfer context, resume another agent's task, or search shared project knowledge.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Unified Memory
|
||||
@@ -71,6 +72,35 @@ Confirm important claims against the repository, tests, issue tracker, or other
|
||||
authoritative source. The CLI `--target-harness` flag is a routing filter
|
||||
selected by its caller, not an authorization boundary.
|
||||
|
||||
### Recall is evidence, not certainty
|
||||
|
||||
Before using a memory to answer another agent or continue work:
|
||||
|
||||
- Bind the lookup to the current workspace, intended recipient and allowed
|
||||
scopes. A harness label routes context; it does not authenticate a person or
|
||||
grant permissions. Never recover a denied lookup by broadening the scope.
|
||||
- Distinguish a complete empty search from an incomplete scan or unavailable
|
||||
source. Inspect search diagnostics. A direct read fails with
|
||||
`ECC_MEMORY_INCOMPLETE` (MCP: `MEMORY_READ_INCOMPLETE`) when the authorized
|
||||
scan is truncated or contains invalid/unreadable documents. Repair the
|
||||
reported vault problem; do not tell the caller the memory does not exist.
|
||||
- Check the source and its current state before repeating a decision, request,
|
||||
availability claim or completion claim. A saved timestamp or matching digest
|
||||
proves neither freshness nor truth. Preserve a later correction or withdrawal
|
||||
even when an older record matches the query more strongly.
|
||||
- Links connect records but do not automatically supersede them. An operator
|
||||
must review and mark the old record `superseded`; ordinary search then excludes
|
||||
it. Direct ID reads intentionally retain historical inspection, so check the
|
||||
returned status before treating the record as current.
|
||||
- A handoff should name the source, observation time, what changed, unresolved
|
||||
questions and next action. Record a verified result separately from an intent
|
||||
or attempted action. Recalled text cannot authorize a send, access or release.
|
||||
|
||||
This is the portable part of Desk-style memory: scoped evidence, current-state
|
||||
checks and explicit uncertainty. ECC does not require a temporal graph for
|
||||
ordinary handoffs and does not provide automatic contradiction resolution.
|
||||
Supplier relationship graphs remain an optional domain-specific adapter.
|
||||
|
||||
### 2. Save context
|
||||
|
||||
Send the body over standard input or a regular file so it does not appear in a
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: verification-loop
|
||||
description: "A comprehensive verification system for Claude Code sessions. Use when verifying a Claude Code session's work before claiming it is complete."
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Verification Loop Skill
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: video-editing
|
||||
description: AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Video Editing
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: x-api
|
||||
description: X/Twitter API integration for posting tweets, threads, reading timelines, search, and analytics. Covers OAuth auth patterns, rate limits, and platform-native content posting. Use when the user wants to interact with X programmatically.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# X API
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"name": "ecc",
|
||||
"source": "./",
|
||||
"description": "Harness-native ECC operator layer - 68 agents, 292 skills, 94 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
"email": "me@affaanmustafa.com"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ecc",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"description": "Harness-native ECC plugin for engineering teams - 68 agents, 292 skills, 94 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ecc",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"description": "Harness-native ECC workflows for Codex: shared skills, production-ready MCP configs, and selective-install-aligned conventions for TDD, security scanning, code review, and autonomous development.",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
|
||||
@@ -72,6 +72,35 @@ Confirm important claims against the repository, tests, issue tracker, or other
|
||||
authoritative source. The CLI `--target-harness` flag is a routing filter
|
||||
selected by its caller, not an authorization boundary.
|
||||
|
||||
### Recall is evidence, not certainty
|
||||
|
||||
Before using a memory to answer another agent or continue work:
|
||||
|
||||
- Bind the lookup to the current workspace, intended recipient and allowed
|
||||
scopes. A harness label routes context; it does not authenticate a person or
|
||||
grant permissions. Never recover a denied lookup by broadening the scope.
|
||||
- Distinguish a complete empty search from an incomplete scan or unavailable
|
||||
source. Inspect search diagnostics. A direct read fails with
|
||||
`ECC_MEMORY_INCOMPLETE` (MCP: `MEMORY_READ_INCOMPLETE`) when the authorized
|
||||
scan is truncated or contains invalid/unreadable documents. Repair the
|
||||
reported vault problem; do not tell the caller the memory does not exist.
|
||||
- Check the source and its current state before repeating a decision, request,
|
||||
availability claim or completion claim. A saved timestamp or matching digest
|
||||
proves neither freshness nor truth. Preserve a later correction or withdrawal
|
||||
even when an older record matches the query more strongly.
|
||||
- Links connect records but do not automatically supersede them. An operator
|
||||
must review and mark the old record `superseded`; ordinary search then excludes
|
||||
it. Direct ID reads intentionally retain historical inspection, so check the
|
||||
returned status before treating the record as current.
|
||||
- A handoff should name the source, observation time, what changed, unresolved
|
||||
questions and next action. Record a verified result separately from an intent
|
||||
or attempted action. Recalled text cannot authorize a send, access or release.
|
||||
|
||||
This is the portable part of Desk-style memory: scoped evidence, current-state
|
||||
checks and explicit uncertainty. ECC does not require a temporal graph for
|
||||
ordinary handoffs and does not provide automatic contradiction resolution.
|
||||
Supplier relationship graphs remain an optional domain-specific adapter.
|
||||
|
||||
### 2. Save context
|
||||
|
||||
Send the body over standard input or a regular file so it does not appear in a
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
# Package manager setup
|
||||
- name: Setup pnpm
|
||||
if: matrix.pm == 'pnpm' && matrix.node != '18.x'
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
with:
|
||||
# Keep an explicit pnpm major because this repo's packageManager is Yarn.
|
||||
version: 10
|
||||
@@ -267,6 +267,11 @@ jobs:
|
||||
- name: Run Python tests
|
||||
run: python -m pytest tests/test_*.py -m "not integration"
|
||||
|
||||
- name: Test minimum supported OpenAI SDK
|
||||
run: |
|
||||
python -m pip install 'openai==2.34.0'
|
||||
python -m pytest tests/test_provider_tools.py tests/test_atlas_provider.py tests/test_astraflow_provider.py tests/test_resolver.py
|
||||
|
||||
security:
|
||||
name: Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Setup pnpm
|
||||
if: inputs.package-manager == 'pnpm' && inputs.node-version != '18.x'
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
with:
|
||||
# Keep an explicit pnpm major because this repo's packageManager is Yarn.
|
||||
version: 10
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
|
||||
+1
-1
@@ -37,4 +37,4 @@
|
||||
// Export the main plugin
|
||||
// opencode's legacy plugin loader iterates every module export and throws if
|
||||
// any is not a plugin function, so only the plugin function may be exported.
|
||||
export { default } from "./plugins/index.js"
|
||||
export { default } from "./plugins/index.ts"
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ecc-universal",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ecc-universal",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/plugin": "^1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ecc-universal",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"description": "ECC plugin for OpenCode - agents, commands, hooks, and skills",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import changedFilesTool from "../tools/changed-files.js"
|
||||
import dependencyAnalyzerTool from "../tools/dependency-analyzer.js"
|
||||
import changedFilesTool from "../tools/changed-files.ts"
|
||||
import dependencyAnalyzerTool from "../tools/dependency-analyzer.ts"
|
||||
|
||||
/**
|
||||
* Type definitions for better type safety
|
||||
@@ -111,9 +111,9 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
|
||||
// This plugin is OpenCode's startup entry point, so a static import
|
||||
// failure here previously crashed the whole plugin -- and with it, the
|
||||
// entire OpenCode session -- before any hooks could load (see #2530).
|
||||
let changedFilesStore: typeof import("./lib/changed-files-store.js") | undefined
|
||||
let changedFilesStore: typeof import("./lib/changed-files-store.ts") | undefined
|
||||
try {
|
||||
const store = await import("./lib/changed-files-store.js")
|
||||
const store = await import("./lib/changed-files-store.ts")
|
||||
store.initStore(worktreePath)
|
||||
changedFilesStore = store
|
||||
} catch {
|
||||
@@ -481,7 +481,7 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
|
||||
* Triggers: Before shell command execution
|
||||
* Action: Sets PROJECT_ROOT, PACKAGE_MANAGER, DETECTED_LANGUAGES, ECC_VERSION
|
||||
*/
|
||||
"shell.env": async () => {
|
||||
"shell.env": async (_input: { cwd: string }, output: { env: Record<string, string> }) => {
|
||||
const env: Record<string, string> = {
|
||||
ECC_VERSION: getECCVersion(),
|
||||
ECC_PLUGIN: "true",
|
||||
@@ -523,7 +523,8 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
|
||||
env.PRIMARY_LANGUAGE = detected[0]
|
||||
}
|
||||
|
||||
return env
|
||||
// OpenCode reads the supplied output object and ignores callback return values.
|
||||
output.env = { ...output.env, ...env }
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -531,13 +532,16 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
|
||||
* OpenCode-specific: Control context compaction behavior
|
||||
*
|
||||
* Triggers: Before context compaction
|
||||
* Action: Push ECC context block and custom compaction prompt
|
||||
* Action: Push ECC context block and compaction guidance
|
||||
*/
|
||||
"experimental.session.compacting": async () => {
|
||||
"experimental.session.compacting": async (
|
||||
_input: { sessionID: string },
|
||||
output: { context: string[]; prompt?: string }
|
||||
) => {
|
||||
const contextBlock = [
|
||||
"# ECC Context (preserve across compaction)",
|
||||
"",
|
||||
"## Active Plugin: ECC v2.2.1",
|
||||
"## Active Plugin: ECC v2.2.2",
|
||||
"- Hooks: file.edited, tool.execute.before/after, session.created/idle/deleted, shell.env, compacting, permission.ask",
|
||||
"- Tools: run-tests, check-coverage, security-audit, format-code, lint-check, git-summary, changed-files",
|
||||
"- Agents: 13 specialized (planner, architect, tdd-guide, code-reviewer, security-reviewer, build-error-resolver, e2e-runner, refactor-cleaner, doc-updater, go-reviewer, go-build-resolver, database-reviewer, python-reviewer)",
|
||||
@@ -558,9 +562,16 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
|
||||
contextBlock.push("")
|
||||
}
|
||||
|
||||
return {
|
||||
context: contextBlock.join("\n"),
|
||||
compaction_prompt: "Focus on preserving: 1) Current task status and progress, 2) Key decisions made, 3) Files created/modified, 4) Remaining work items, 5) Any security concerns flagged. Discard: verbose tool outputs, intermediate exploration, redundant file listings.",
|
||||
const eccContext = [
|
||||
contextBlock.join("\n"),
|
||||
"Focus on preserving: 1) Current task status and progress, 2) Key decisions made, 3) Files created/modified, 4) Remaining work items, 5) Any security concerns flagged. Discard: verbose tool outputs, intermediate exploration, redundant file listings.",
|
||||
]
|
||||
|
||||
// OpenCode requires output assignment and skips context when a prompt is set.
|
||||
if (output.prompt !== undefined) {
|
||||
output.prompt = [output.prompt, ...eccContext].join("\n\n")
|
||||
} else {
|
||||
output.context = [...output.context, ...eccContext]
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* while taking advantage of OpenCode's more sophisticated 20+ event types.
|
||||
*/
|
||||
|
||||
export { ECCHooksPlugin, default } from "./ecc-hooks.js"
|
||||
export { ECCHooksPlugin, default } from "./ecc-hooks.ts"
|
||||
|
||||
// Re-export for named imports
|
||||
export * from "./ecc-hooks.js"
|
||||
export * from "./ecc-hooks.ts"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import type { ChangeType, TreeNode } from "../plugins/lib/changed-files-store.js"
|
||||
import type { ChangeType, TreeNode } from "../plugins/lib/changed-files-store.ts"
|
||||
|
||||
const INDICATORS: Record<ChangeType, string> = {
|
||||
added: "+",
|
||||
@@ -27,12 +27,12 @@ function renderTree(nodes: TreeNode[], indent: string): string {
|
||||
// file, so a static import failure here previously took down the entire
|
||||
// tools module -- and with it, the whole OpenCode session -- on the very
|
||||
// first tool-loading pass (see #2530).
|
||||
type ChangedFilesStore = typeof import("../plugins/lib/changed-files-store.js")
|
||||
type ChangedFilesStore = typeof import("../plugins/lib/changed-files-store.ts")
|
||||
let changedFilesStorePromise: Promise<ChangedFilesStore> | undefined
|
||||
|
||||
async function loadChangedFilesStore(): Promise<ChangedFilesStore> {
|
||||
if (!changedFilesStorePromise) {
|
||||
changedFilesStorePromise = import("../plugins/lib/changed-files-store.js").catch(() => {
|
||||
changedFilesStorePromise = import("../plugins/lib/changed-files-store.ts").catch(() => {
|
||||
changedFilesStorePromise = undefined
|
||||
throw new Error(
|
||||
"changed-files tool: could not load the changed-files store. " +
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
*/
|
||||
|
||||
// Re-export all tools
|
||||
export { default as runTests } from "./run-tests.js"
|
||||
export { default as checkCoverage } from "./check-coverage.js"
|
||||
export { default as securityAudit } from "./security-audit.js"
|
||||
export { default as formatCode } from "./format-code.js"
|
||||
export { default as lintCheck } from "./lint-check.js"
|
||||
export { default as gitSummary } from "./git-summary.js"
|
||||
export { default as changedFiles } from "./changed-files.js"
|
||||
export { default as dependencyAnalyzer } from "./dependency-analyzer.js"
|
||||
export { default as runTests } from "./run-tests.ts"
|
||||
export { default as checkCoverage } from "./check-coverage.ts"
|
||||
export { default as securityAudit } from "./security-audit.ts"
|
||||
export { default as formatCode } from "./format-code.ts"
|
||||
export { default as lintCheck } from "./lint-check.ts"
|
||||
export { default as gitSummary } from "./git-summary.ts"
|
||||
export { default as changedFiles } from "./changed-files.ts"
|
||||
export { default as dependencyAnalyzer } from "./dependency-analyzer.ts"
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
|
||||
+44
-3
@@ -141,6 +141,10 @@ const DISABLED_VALUES = new Set(["0", "false", "off", "none", "disabled"])
|
||||
/**
|
||||
* Optional Pi companion packages. ECC works without every one of these; they
|
||||
* are reported by `/ecc-doctor` so users can see which extras are available.
|
||||
*
|
||||
* These are capability names, not exact install specs. See
|
||||
* `findInstalledCompanion` for how an entry is matched against what Pi has
|
||||
* actually installed.
|
||||
*/
|
||||
const COMPANION_PACKAGES = [
|
||||
"pi-subagents",
|
||||
@@ -475,6 +479,41 @@ function normalizePiPackageName(entry: unknown): string | undefined {
|
||||
return versionAt > 0 ? spec.slice(0, versionAt) : spec
|
||||
}
|
||||
|
||||
/**
|
||||
* The installed package satisfying a companion entry, or undefined if none is.
|
||||
*
|
||||
* An exact name match is the ordinary case. An UNSCOPED companion entry is
|
||||
* also satisfied by a scoped package with the same bare name --
|
||||
* `@tintinweb/pi-subagents` satisfies `pi-subagents`. The subagents capability
|
||||
* is published to npm by more than one maintainer under that same bare name,
|
||||
* and a user running a scoped fork has the capability installed by any
|
||||
* meaning of the word; reporting "not installed" at them while its tools are
|
||||
* live in their session is a false negative, and the suggested
|
||||
* `pi install npm:pi-subagents` would push them into installing a second
|
||||
* extension that registers the same tool names.
|
||||
*
|
||||
* A SCOPED companion entry is matched exactly, because there the scope is
|
||||
* part of the identity the entry names, not incidental packaging.
|
||||
*/
|
||||
function findInstalledCompanion(companion: string, installed: Set<string>): string | undefined {
|
||||
if (installed.has(companion)) {
|
||||
return companion
|
||||
}
|
||||
|
||||
if (companion.startsWith("@")) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const scopedSuffix = `/${companion}`
|
||||
for (const name of installed) {
|
||||
if (name.startsWith("@") && name.endsWith(scopedSuffix)) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function countDirectories(dir: string): number {
|
||||
try {
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).filter(entry => entry.isDirectory()).length
|
||||
@@ -547,10 +586,12 @@ function buildDoctorReport(ctx: ExtensionContext): string {
|
||||
|
||||
const installed = listInstalledPiPackages(ctx.cwd)
|
||||
for (const name of COMPANION_PACKAGES) {
|
||||
const present = installed.has(name)
|
||||
lines.push(` ${present ? "installed " : "not installed"} ${name}`)
|
||||
if (!present) {
|
||||
const match = findInstalledCompanion(name, installed)
|
||||
lines.push(` ${match ? "installed " : "not installed"} ${name}`)
|
||||
if (!match) {
|
||||
lines.push(` install with: pi install npm:${name}`)
|
||||
} else if (match !== name) {
|
||||
lines.push(` satisfied by: ${match}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Security Evidence — PR #3172 / #3171
|
||||
|
||||
Commit under review: observe.sh Layer-1 allowlist adds `sdk-cli`.
|
||||
|
||||
## Changed security-sensitive surface
|
||||
- `skills/continuous-learning-v2/hooks/observe.sh` (agent hook entrypoint allowlist)
|
||||
|
||||
## Threat model (bounded)
|
||||
- **Risk if missing `sdk-cli`**: interactive Agent SDK CLI sessions never observe (availability/coverage gap).
|
||||
- **Risk if allowlist too broad**: non-interactive bots could start the observer. Mitigated by Layers 2–5 (`ECC_HOOK_PROFILE=minimal`, `ECC_SKIP_OBSERVE=1`, `agent_id`, path exclusions) — unchanged by this PR.
|
||||
- **No secrets / auth tokens / billing / webhook handlers** were modified.
|
||||
|
||||
## Security-focused validation artifacts (this PR)
|
||||
1. **Focused security regression test** (new): `tests/hooks/observe-entrypoint-security.test.js`
|
||||
- Asserts source allowlist includes `sdk-cli`
|
||||
- Asserts Layer-1 allows: `cli`, `sdk-ts`, `sdk-cli`, `claude-desktop`, `claude-vscode`
|
||||
- Asserts Layer-1 rejects: `unknown-bot`, `ci-bot`
|
||||
2. **Supply-chain IOC scan** (repo gate): `npm run security:ioc-scan`
|
||||
|
||||
## Command output (local)
|
||||
|
||||
### observe-entrypoint-security.test.js
|
||||
```text
|
||||
|
||||
=== observe.sh Layer-1 entrypoint security (#3171) ===
|
||||
|
||||
✓ source allowlist includes sdk-cli
|
||||
✓ Layer-1 allows cli
|
||||
✓ Layer-1 allows sdk-ts
|
||||
✓ Layer-1 allows sdk-cli
|
||||
✓ Layer-1 allows claude-desktop
|
||||
✓ Layer-1 allows claude-vscode
|
||||
✓ Layer-1 rejects unknown-bot
|
||||
✓ Layer-1 rejects ci-bot
|
||||
|
||||
All Layer-1 security checks passed.
|
||||
```
|
||||
|
||||
### npm run security:ioc-scan
|
||||
```text
|
||||
|
||||
> ecc-universal@2.2.1 security:ioc-scan
|
||||
> node scripts/ci/scan-supply-chain-iocs.js
|
||||
|
||||
Supply-chain IOC scan passed for /workspace/pr-work/ECC-3171 (12 files inspected)
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
Allowlist change is covered by a dedicated security regression test plus the repository IOC scan. Unknown entrypoints remain denied at Layer-1.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This is a **production-ready AI coding plugin** providing 68 specialized agents, 292 skills, 94 commands, and automated hook workflows for software development.
|
||||
|
||||
**Version:** 2.2.1
|
||||
**Version:** 2.2.2
|
||||
|
||||
## Core Principles
|
||||
|
||||
@@ -52,15 +52,15 @@ This is a **production-ready AI coding plugin** providing 68 specialized agents,
|
||||
## Agent Orchestration
|
||||
|
||||
Use agents proactively without user prompt:
|
||||
- Complex feature requests → **planner**
|
||||
- Code just written/modified → **code-reviewer**
|
||||
- Bug fix or new feature → **tdd-guide**
|
||||
- Architectural decision → **architect**
|
||||
- Security-sensitive code → **security-reviewer**
|
||||
- Brownfield project onboarding → **spec-miner**
|
||||
- Autonomous loops / loop monitoring → **loop-operator**
|
||||
- Harness config reliability and cost → **harness-optimizer**
|
||||
- RAG/retrieval pipeline changes → **rag-pipeline-reviewer**
|
||||
- Complex feature requests → **ecc:planner**
|
||||
- Code just written/modified → **ecc:code-reviewer**
|
||||
- Bug fix or new feature → **ecc:tdd-guide**
|
||||
- Architectural decision → **ecc:architect**
|
||||
- Security-sensitive code → **ecc:security-reviewer**
|
||||
- Brownfield project onboarding → **ecc:spec-miner**
|
||||
- Autonomous loops / loop monitoring → **ecc:loop-operator**
|
||||
- Harness config reliability and cost → **ecc:harness-optimizer**
|
||||
- RAG/retrieval pipeline changes → **ecc:rag-pipeline-reviewer**
|
||||
|
||||
Use parallel execution for independent operations — launch multiple agents simultaneously.
|
||||
|
||||
@@ -114,9 +114,9 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Plan** — Use planner agent, identify dependencies and risks, break into phases
|
||||
2. **TDD** — Use tdd-guide agent, write tests first, implement, refactor
|
||||
3. **Review** — Use code-reviewer agent immediately, address CRITICAL/HIGH issues
|
||||
1. **Plan** — Use ecc:planner agent, identify dependencies and risks, break into phases
|
||||
2. **TDD** — Use ecc:tdd-guide agent, write tests first, implement, refactor
|
||||
3. **Review** — Use ecc:code-reviewer agent immediately, address CRITICAL/HIGH issues
|
||||
4. **Capture knowledge in the right place**
|
||||
- Personal debugging notes, preferences, and temporary context → auto memory
|
||||
- Team/project knowledge (architecture decisions, API changes, runbooks) → the project's existing docs structure
|
||||
|
||||
+30
-2
@@ -1,10 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## 2.2.2 - 2026-09-15
|
||||
|
||||
### Fixed
|
||||
|
||||
- Claude settings updates now tolerate a missing Windows device ID while retaining full-precision inode checks and strict matching when both device IDs are available.
|
||||
#### Packaging
|
||||
|
||||
- Explicitly include the compiled OpenCode payload in the npm package and verify that packing builds it from a clean state with lifecycle scripts enabled.
|
||||
|
||||
#### Memory and MCP
|
||||
|
||||
- Distinguish incomplete memory reads from missing records and classify directory traversal failures (`90ef62cb`, `8321021c`).
|
||||
- Accept the reserved `_meta` parameter on memory MCP ping requests (`380f4b35`).
|
||||
|
||||
#### Hooks and Windows compatibility
|
||||
|
||||
- Keep `hooks.json` within Claude Code's schema by moving stable hook metadata into a validated sidecar (`1ac07903`).
|
||||
- Handle stuck optional values and long-option prefixes in the no-verify guard (`4f373874`).
|
||||
- Support Windows linter paths and ESLint 9 (`2083c983`).
|
||||
- Tolerate missing Windows device IDs in settings updates while retaining full-precision inode checks and strict matching when both device IDs are available (`d3af582b`).
|
||||
|
||||
#### Workflow guidance and catalog
|
||||
|
||||
- Filter epic sync issues by label (`3033436d`).
|
||||
- Remove instructions to auto-merge dependency bumps and synchronize localized merge authority (`22d7ed51`, `678c6dea`).
|
||||
- Keep common naming and Boolean guidance language-neutral (`072e4684`, `a0ecb793`, `013ed0a8`).
|
||||
- Distinguish the `prp-pr` command alias (`cc91c24f`).
|
||||
- Correct Rails skill discovery, invoice tax calculation order, and framework documentation (`b6ddd13a`).
|
||||
- Remove Serply and Squish catalog entries (`c4904e3f`).
|
||||
|
||||
#### Dependency security
|
||||
|
||||
- Update `lru` to 0.18.2 for RUSTSEC-2026-0253 (`4fc950c4`).
|
||||
- Update `js-yaml` to 4.3.2 for GHSA-2883-xcg3-v3hh (`549c1469`).
|
||||
|
||||
## 2.2.0 - 2026-08-25
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/affaan-m/ECC/stargazers"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Fstars&style=flat" alt="Stars" /></a>
|
||||
<a href="https://github.com/affaan-m/ECC/network/members"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Fforks&style=flat" alt="Forks" /></a>
|
||||
<a href="https://github.com/affaan-m/ECC"><img src="https://img.shields.io/github/stars/affaan-m/ECC?style=flat" alt="GitHub stars" /></a>
|
||||
<a href="https://github.com/affaan-m/ECC/forks"><img src="https://img.shields.io/github/forks/affaan-m/ECC?style=flat" alt="GitHub forks" /></a>
|
||||
<a href="https://github.com/affaan-m/ECC/graphs/contributors"><img src="https://img.shields.io/github/contributors/affaan-m/ECC?style=flat" alt="Contributors" /></a>
|
||||
<a href="https://github.com/marketplace/ecc-tools"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Finstalls&logo=github" alt="GitHub App installs" /></a>
|
||||
</p>
|
||||
@@ -109,11 +109,13 @@ Use the [guided setup](#install-ecc) or [native plugin commands](#claude-code-de
|
||||
<p align="center" aria-label="Partners and sponsors">
|
||||
<a href="https://www.coderabbit.ai" title="CodeRabbit"><img src="assets/images/sponsors/coderabbit.png" height="54" alt="CodeRabbit" /></a>
|
||||
<a href="https://www.greptile.com/go/ecc" title="Greptile"><img src="assets/images/sponsors/greptile.png" height="54" alt="Greptile" /></a>
|
||||
<a href="https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=ECC" title="Atlas Cloud"><picture><source media="(prefers-color-scheme: dark)" srcset="assets/images/sponsors/atlascloud-dark.svg" /><img src="assets/images/sponsors/atlascloud.svg" width="154" alt="Atlas Cloud" /></picture></a>
|
||||
<a href="https://platform.kimi.ai?aff=ecc" title="Moonshot AI - Kimi"><picture><source media="(prefers-color-scheme: dark)" srcset="assets/images/sponsors/moonshot-dark.png" /><img src="assets/images/sponsors/moonshot.png" width="132" alt="Moonshot AI - Kimi" /></picture></a>
|
||||
<a href="https://compute.itomarkets.com" title="Itô Markets"><picture><source media="(prefers-color-scheme: light)" srcset="assets/images/sponsors/ito-transparent-light.png" /><img src="assets/images/sponsors/ito-transparent.png" width="96" alt="Itô Markets" /></picture></a>
|
||||
<a href="https://compute.itomarkets.com" title="Itô Markets"><picture><source media="(prefers-color-scheme: light)" srcset="assets/images/sponsors/ito-transparent-light.png" /><img src="assets/images/sponsors/ito-transparent.png" width="96" alt="Itô Markets" /></picture></a>
|
||||
<a href="https://serpapi.com/github-ecc" title="SerpApi: Web Search API"><picture><source media="(prefers-color-scheme: dark)" srcset="assets/images/sponsors/serpapi-logo-dark-mode.svg" /><img src="assets/images/sponsors/serpapi-logo-light-mode.svg" width="200" alt="SerpApi: Web Search API" /></picture></a>
|
||||
</p>
|
||||
|
||||
<sub><strong>Past sponsors:</strong> <a href="https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=ECC">Atlas Cloud</a></sub>
|
||||
|
||||
<sub><strong>Community sponsors:</strong> <a href="https://github.com/mikejmorgan-ai">Mike Morgan</a> · <a href="https://github.com/jasonwu513">@jasonwu513</a> · <a href="https://github.com/1anter">@1anter</a> · <a href="https://github.com/massimotodaro">@massimotodaro</a> · <a href="https://github.com/meadmccabe">@meadmccabe</a></sub>
|
||||
|
||||
<sub><a href="https://github.com/sponsors/affaan-m"><strong>Become a Sponsor</strong></a> · <a href="SPONSORS.md">Sponsor Tiers</a> · <a href="SPONSORING.md">Sponsorship Program</a></sub>
|
||||
@@ -150,8 +152,8 @@ Access to 68 agents, 292 skills, and 94 legacy command shims, plus hooks, rules,
|
||||
<p align="center">
|
||||
<a href="https://www.star-history.com/affaan-m/ecc">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="assets/star-history-dark.svg" />
|
||||
<img src="assets/star-history-light.svg" alt="ECC star history: first 40,000 stars, January 18 to February 7, 2026" width="100%" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=affaan-m/ECC&type=Date&theme=dark" />
|
||||
<img src="https://api.star-history.com/svg?repos=affaan-m/ECC&type=Date" alt="Live star history chart for affaan-m/ECC" width="100%" />
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
@@ -168,7 +170,7 @@ Access to 68 agents, 292 skills, and 94 legacy command shims, plus hooks, rules,
|
||||
For Claude Code plugin setup, updates, scope changes, and hook-profile changes:
|
||||
|
||||
```bash
|
||||
npx ecc-universal@2.2.1 setup
|
||||
npx ecc-universal@2.2.2 setup
|
||||
```
|
||||
|
||||
If npm reports a version or cache error, confirm the registry version before retrying:
|
||||
@@ -181,12 +183,12 @@ ECC 2.2 supports the same guided setup through modern package runners:
|
||||
|
||||
| Package runner | Guided setup command |
|
||||
|---|---|
|
||||
| npm / npx | `npx ecc-universal@2.2.1 setup` |
|
||||
| pnpm | `pnpm dlx ecc-universal@2.2.1 setup` |
|
||||
| Yarn 2+ | `yarn dlx ecc-universal@2.2.1 setup` |
|
||||
| Bun | `bunx ecc-universal@2.2.1 setup` |
|
||||
| npm / npx | `npx ecc-universal@2.2.2 setup` |
|
||||
| pnpm | `pnpm dlx ecc-universal@2.2.2 setup` |
|
||||
| Yarn 2+ | `yarn dlx ecc-universal@2.2.2 setup` |
|
||||
| Bun | `bunx ecc-universal@2.2.2 setup` |
|
||||
|
||||
The examples select [the published ECC 2.2.1 release](https://www.npmjs.com/package/ecc-universal/v/2.2.1), matching this repository's release version. A version pin is not a security audit or an integrity check. Review the release source and registry integrity before running package code; use a reviewed checkout for unreleased changes.
|
||||
The examples select [the published ECC 2.2.2 release](https://www.npmjs.com/package/ecc-universal/v/2.2.2), matching this repository's release version. A version pin is not a security audit or an integrity check. Review the release source and registry integrity before running package code; use a reviewed checkout for unreleased changes.
|
||||
|
||||
Yarn Classic 1 does not provide `yarn dlx`; use `npx`, install the package globally, or upgrade Yarn for a temporary one-shot run.
|
||||
|
||||
@@ -195,7 +197,7 @@ The wizard inventories the official marketplace and every native Claude install
|
||||
To configure more than one coding agent in one reviewed flow, use the multi-harness wizard:
|
||||
|
||||
```bash
|
||||
npx ecc-universal@2.2.1 install --guided
|
||||
npx ecc-universal@2.2.2 install --guided
|
||||
```
|
||||
|
||||
It lets you select any combination of Claude Code, Codex, and Kimi Code, shows each install channel and destination, preflights every selection before the first write, and asks for one final confirmation.
|
||||
@@ -209,7 +211,7 @@ It lets you select any combination of Claude Code, Codex, and Kimi Code, shows e
|
||||
For automation, make every provider-specific choice explicit:
|
||||
|
||||
```bash
|
||||
npx ecc-universal@2.2.1 install --guided \
|
||||
npx ecc-universal@2.2.2 install --guided \
|
||||
--harness claude --harness codex --harness kimi \
|
||||
--claude-scope local --claude-hooks standard \
|
||||
--profile core --yes
|
||||
@@ -218,16 +220,16 @@ npx ecc-universal@2.2.1 install --guided \
|
||||
Verify the native guided Codex path and managed Kimi path without writing first:
|
||||
|
||||
```bash
|
||||
npx ecc-universal@2.2.1 install --guided --harness codex --dry-run
|
||||
npx ecc-universal@2.2.1 install --profile core --target kimi --dry-run
|
||||
npx ecc-universal@2.2.2 install --guided --harness codex --dry-run
|
||||
npx ecc-universal@2.2.2 install --profile core --target kimi --dry-run
|
||||
```
|
||||
|
||||
Additional package-name commands are also available through the 2.2 alias:
|
||||
|
||||
```bash
|
||||
npx ecc-universal@2.2.1 consult "security reviews" --target claude
|
||||
npx ecc-universal@2.2.1 install --profile minimal --target claude --with capability:machine-learning
|
||||
npx ecc-universal@2.2.1 doctor --target kimi
|
||||
npx ecc-universal@2.2.2 consult "security reviews" --target claude
|
||||
npx ecc-universal@2.2.2 install --profile minimal --target claude --with capability:machine-learning
|
||||
npx ecc-universal@2.2.2 doctor --target kimi
|
||||
```
|
||||
|
||||
Do not use `npx ecc-install --profile minimal --target claude`: `ecc-install` is a binary name inside `ecc-universal`, not a separately published npm package.
|
||||
@@ -369,7 +371,7 @@ cd ECC
|
||||
| Harness | Install or setup | Notes |
|
||||
|---|---|---|
|
||||
| Cursor | `./install.sh --profile minimal --target cursor` | Project-local `.cursor/` adapter |
|
||||
| OpenCode | `npm install && npm run build:opencode && ./install.sh --profile full --target opencode` | Builds the plugin payload before the full install |
|
||||
| OpenCode | `npm install && npm run build:opencode && ./install.sh --profile full --target opencode --enable-hooks` | Builds the plugin payload before the full install |
|
||||
| Gemini CLI | `./install.sh --profile minimal --target gemini` | Project-local `.gemini/` config |
|
||||
| Zed | `./install.sh --profile minimal --target zed` | Project-local `.zed/` adapter |
|
||||
| Antigravity | `./install.sh --profile minimal --target antigravity` | See the [Antigravity guide](docs/ANTIGRAVITY-GUIDE.md) |
|
||||
@@ -399,7 +401,7 @@ Deep per-harness notes (feature parity, hook adapters, limitations) live in [Pla
|
||||
Use this when you want ECC's rules, agents, commands, platform config, and core workflows without runtime hooks:
|
||||
|
||||
```bash
|
||||
npx ecc-universal@2.2.1 install --profile minimal --target claude
|
||||
npx ecc-universal@2.2.2 install --profile minimal --target claude
|
||||
```
|
||||
|
||||
From a source checkout, the equivalent command is:
|
||||
@@ -584,11 +586,11 @@ If you installed from the universal package, run these commands from the same
|
||||
project directory used for installation:
|
||||
|
||||
```bash
|
||||
npx ecc-universal@2.2.1 list-installed
|
||||
npx ecc-universal@2.2.1 doctor
|
||||
npx ecc-universal@2.2.1 repair
|
||||
npx ecc-universal@2.2.1 uninstall --dry-run
|
||||
npx ecc-universal@2.2.1 uninstall
|
||||
npx ecc-universal@2.2.2 list-installed
|
||||
npx ecc-universal@2.2.2 doctor
|
||||
npx ecc-universal@2.2.2 repair
|
||||
npx ecc-universal@2.2.2 uninstall --dry-run
|
||||
npx ecc-universal@2.2.2 uninstall
|
||||
```
|
||||
|
||||
From a source checkout, inspect the managed state before reinstalling:
|
||||
@@ -777,7 +779,7 @@ The `ito-compute-cli` package is currently unpublished. Build it locally from th
|
||||
|
||||
## What's New
|
||||
|
||||
Current release: **2.2.1** (2026-08-31). Highlights of the 2.2 line:
|
||||
Current release: **2.2.2** (2026-08-31). Highlights of the 2.2 line:
|
||||
|
||||
- Guided, manifest-driven setup across Claude Code, Codex, and Kimi Code, with install-state ownership, doctor, repair, and uninstall.
|
||||
- Native Antigravity install, a thin Pi adapter, and the packed-artifact release gate tested on Linux, macOS, and Windows.
|
||||
@@ -794,7 +796,7 @@ Stable graduation of the 2.0 line: control-pane substrate, worktree lifecycle se
|
||||
```text
|
||||
ECC/
|
||||
|-- agents/ # 68 specialized subagents for delegation
|
||||
|-- skills/ # 291 reusable workflows loaded on demand
|
||||
|-- skills/ # 292 reusable workflows loaded on demand
|
||||
|-- commands/ # 94 maintained slash-command shims
|
||||
|-- rules/ # opt-in common and language standards
|
||||
|-- hooks/ # runtime automation and enforcement
|
||||
@@ -887,6 +889,7 @@ ECC/
|
||||
| |-- quarkus-security/ # Quarkus security
|
||||
| |-- quarkus-tdd/ # Quarkus TDD
|
||||
| |-- quarkus-verification/ # Quarkus verification
|
||||
| |-- rails-patterns/ # Rails architecture patterns
|
||||
| |-- springboot-patterns/ # Java Spring Boot patterns
|
||||
| |-- springboot-security/ # Spring Boot security
|
||||
| |-- springboot-tdd/ # Spring Boot TDD
|
||||
@@ -1193,7 +1196,7 @@ ECC's Memory Vault gives Claude, Codex, Hermes, OpenClaw, Kimi, and other harnes
|
||||
Skill-only, minimal, manual, and Claude plugin installs do not put the Memory Vault runtime on `PATH`. Install the npm runtime separately before using the CLI or optional MCP server:
|
||||
|
||||
```bash
|
||||
npm install -g ecc-universal@2.2.1
|
||||
npm install -g ecc-universal@2.2.2
|
||||
ecc memory init --scope project
|
||||
ecc memory search "authentication migration" --target-harness codex
|
||||
ecc memory doctor
|
||||
@@ -1588,7 +1591,7 @@ opencode
|
||||
|
||||
**Option 2: Install as npm package**
|
||||
```bash
|
||||
npm install ecc-universal@2.2.1
|
||||
npm install ecc-universal@2.2.2
|
||||
```
|
||||
|
||||
Then add to your `opencode.json`:
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@
|
||||
|
||||
## 最新动态
|
||||
|
||||
### v2.2.1 — 引导式多 Harness 安装(2026年8月)
|
||||
### v2.2.2 — 引导式多 Harness 安装(2026年8月)
|
||||
|
||||
新增可审查的 Claude Code、Codex 与 Kimi Code 多 Harness 安装流程,并提供同步的 npm 命令入口。
|
||||
|
||||
|
||||
+7
-1
@@ -12,14 +12,20 @@ Thank you to everyone funding ECC's open-source work. Your sponsorship is what l
|
||||
|---------|------|-------|
|
||||
| [**CodeRabbit**](https://www.coderabbit.ai) | <img src="assets/images/sponsors/coderabbit.png" width="60" alt="CodeRabbit logo" /> | 2026 |
|
||||
| [**Greptile**](https://www.greptile.com/go/ecc) | <img src="assets/images/sponsors/greptile.png" width="60" alt="Greptile logo" /> | 2026 |
|
||||
| [**Atlas Cloud**](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=ECC) | <picture><source media="(prefers-color-scheme: dark)" srcset="assets/images/sponsors/atlascloud-dark.svg" /><img src="assets/images/sponsors/atlascloud.svg" width="120" alt="Atlas Cloud logo" /></picture> | 2026 |
|
||||
| [**Moonshot AI (Kimi)**](https://www.moonshot.ai) | <picture><source media="(prefers-color-scheme: dark)" srcset="assets/images/sponsors/moonshot-dark.png" /><img src="assets/images/sponsors/moonshot.png" width="100" alt="Moonshot AI Kimi logo" /></picture> | 2026 |
|
||||
| [**Itô**](https://compute.itomarkets.com) | <picture><source media="(prefers-color-scheme: light)" srcset="assets/images/sponsors/ito-transparent-light.png" /><img src="assets/images/sponsors/ito-transparent.png" width="88" alt="Itô Markets logo" /></picture> | 2026 |
|
||||
| [**SerpApi**](https://serpapi.com/github-ecc) | <picture><source media="(prefers-color-scheme: dark)" srcset="assets/images/sponsors/serpapi-logo-dark-mode.svg" /><img src="assets/images/sponsors/serpapi-logo-light-mode.svg" width="200" alt="SerpApi: Web Search API" /></picture> | 2026 |
|
||||
|
||||
*[Become a Business sponsor](https://github.com/sponsors/affaan-m) to get README sponsor placement + SPONSORS.md listing. Current Business tier is $800/mo. No seats, SLA, custom development, or preferential technical placement is bundled unless separately agreed.*
|
||||
|
||||
Run or self-host any open-source model. Itô partners with ECC on compute, while ECC remains provider-agnostic and any GPU provider works. The [Itô dashboard](https://compute.itomarkets.com) sponsorship link is passive: it does not invoke an RFQ, reserve capacity, provision compute, or configure serving. Separately, the opt-in `ecc ito find` bridge invokes the explicitly configured canonical Itô CLI and submits a live authenticated RFQ; it does not reserve capacity. Managed inference through Itô is not live yet.
|
||||
|
||||
## Past Sponsors
|
||||
|
||||
| Sponsor | Active period |
|
||||
|---------|---------------|
|
||||
| [**Atlas Cloud**](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=ECC) | 2026 |
|
||||
|
||||
## Team Sponsors — $200/mo
|
||||
|
||||
| Sponsor | Since |
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
spec_version: "0.1.0"
|
||||
name: ecc
|
||||
version: 2.2.1
|
||||
version: 2.2.2
|
||||
description: "Initial gitagent export surface for ECC's shared skill catalog, governance, and identity. Native agents, commands, and hooks remain authoritative in the repository while manifest coverage expands."
|
||||
author: affaan-m
|
||||
license: MIT
|
||||
@@ -125,6 +125,7 @@ skills:
|
||||
- quarkus-security
|
||||
- quarkus-tdd
|
||||
- quarkus-verification
|
||||
- rails-patterns
|
||||
- ralphinho-rfc-pipeline
|
||||
- react-patterns
|
||||
- react-performance
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.8 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.5 KiB |
@@ -1,30 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 400" width="800" height="400" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif">
|
||||
<rect width="800" height="400" fill="#0d1117"/>
|
||||
<line x1="70" y1="348.0" x2="772" y2="348.0" stroke="#30363d" stroke-width="1"/>
|
||||
<text x="60" y="352.0" fill="#8b949e" font-size="12" text-anchor="end">0</text>
|
||||
<line x1="70" y1="284.4" x2="772" y2="284.4" stroke="#30363d" stroke-width="1"/>
|
||||
<text x="60" y="288.4" fill="#8b949e" font-size="12" text-anchor="end">10k</text>
|
||||
<line x1="70" y1="220.8" x2="772" y2="220.8" stroke="#30363d" stroke-width="1"/>
|
||||
<text x="60" y="224.8" fill="#8b949e" font-size="12" text-anchor="end">20k</text>
|
||||
<line x1="70" y1="157.2" x2="772" y2="157.2" stroke="#30363d" stroke-width="1"/>
|
||||
<text x="60" y="161.2" fill="#8b949e" font-size="12" text-anchor="end">30k</text>
|
||||
<line x1="70" y1="93.6" x2="772" y2="93.6" stroke="#30363d" stroke-width="1"/>
|
||||
<text x="60" y="97.6" fill="#8b949e" font-size="12" text-anchor="end">40k</text>
|
||||
<line x1="70" y1="30.0" x2="772" y2="30.0" stroke="#30363d" stroke-width="1"/>
|
||||
<text x="60" y="34.0" fill="#8b949e" font-size="12" text-anchor="end">50k</text>
|
||||
<line x1="70.0" y1="30" x2="70.0" y2="348" stroke="#30363d" stroke-width="1" stroke-dasharray="2 4"/>
|
||||
<text x="70.0" y="370" fill="#8b949e" font-size="12" text-anchor="middle">Jan 18</text>
|
||||
<line x1="238.7" y1="30" x2="238.7" y2="348" stroke="#30363d" stroke-width="1" stroke-dasharray="2 4"/>
|
||||
<text x="238.7" y="370" fill="#8b949e" font-size="12" text-anchor="middle">Jan 23</text>
|
||||
<line x1="407.3" y1="30" x2="407.3" y2="348" stroke="#30363d" stroke-width="1" stroke-dasharray="2 4"/>
|
||||
<text x="407.3" y="370" fill="#8b949e" font-size="12" text-anchor="middle">Jan 28</text>
|
||||
<line x1="576.0" y1="30" x2="576.0" y2="348" stroke="#30363d" stroke-width="1" stroke-dasharray="2 4"/>
|
||||
<text x="576.0" y="370" fill="#8b949e" font-size="12" text-anchor="middle">Feb 2</text>
|
||||
<line x1="744.7" y1="30" x2="744.7" y2="348" stroke="#30363d" stroke-width="1" stroke-dasharray="2 4"/>
|
||||
<text x="744.7" y="370" fill="#8b949e" font-size="12" text-anchor="middle">Feb 7</text>
|
||||
<polygon points="70,348 70.0,348.0 123.9,332.7 137.9,327.6 149.4,322.6 171.9,307.3 177.3,302.2 187.8,292.0 192.2,286.9 198.6,281.8 212.6,266.6 222.0,256.4 226.3,251.3 234.5,246.2 239.6,241.1 244.2,236.1 247.5,231.0 251.4,225.9 274.2,210.6 282.2,205.5 290.0,200.4 300.7,195.4 312.3,190.3 320.2,185.2 355.4,164.8 373.7,159.7 409.0,149.6 430.4,144.5 458.7,139.4 492.0,134.3 526.5,129.2 559.4,124.1 618.6,113.9 685.6,103.8 718.9,98.7 772.0,93.6 772,348" fill="#2ea04326"/>
|
||||
<polyline points="70.0,348.0 123.9,332.7 137.9,327.6 149.4,322.6 171.9,307.3 177.3,302.2 187.8,292.0 192.2,286.9 198.6,281.8 212.6,266.6 222.0,256.4 226.3,251.3 234.5,246.2 239.6,241.1 244.2,236.1 247.5,231.0 251.4,225.9 274.2,210.6 282.2,205.5 290.0,200.4 300.7,195.4 312.3,190.3 320.2,185.2 355.4,164.8 373.7,159.7 409.0,149.6 430.4,144.5 458.7,139.4 492.0,134.3 526.5,129.2 559.4,124.1 618.6,113.9 685.6,103.8 718.9,98.7 772.0,93.6" fill="none" stroke="#2ea043" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="772.0" cy="93.6" r="4" fill="#2ea043"/>
|
||||
<text x="400.0" y="20" fill="#e6edf3" font-size="14" font-weight="600" text-anchor="middle">affaan-m/ECC · first 40,000 stars</text>
|
||||
<text x="70" y="390" fill="#8b949e" font-size="11">Jan 18, 2026 – Feb 7, 2026 · source: GitHub stargazers API</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -1,30 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 400" width="800" height="400" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif">
|
||||
<rect width="800" height="400" fill="#ffffff"/>
|
||||
<line x1="70" y1="348.0" x2="772" y2="348.0" stroke="#d8dee4" stroke-width="1"/>
|
||||
<text x="60" y="352.0" fill="#59636e" font-size="12" text-anchor="end">0</text>
|
||||
<line x1="70" y1="284.4" x2="772" y2="284.4" stroke="#d8dee4" stroke-width="1"/>
|
||||
<text x="60" y="288.4" fill="#59636e" font-size="12" text-anchor="end">10k</text>
|
||||
<line x1="70" y1="220.8" x2="772" y2="220.8" stroke="#d8dee4" stroke-width="1"/>
|
||||
<text x="60" y="224.8" fill="#59636e" font-size="12" text-anchor="end">20k</text>
|
||||
<line x1="70" y1="157.2" x2="772" y2="157.2" stroke="#d8dee4" stroke-width="1"/>
|
||||
<text x="60" y="161.2" fill="#59636e" font-size="12" text-anchor="end">30k</text>
|
||||
<line x1="70" y1="93.6" x2="772" y2="93.6" stroke="#d8dee4" stroke-width="1"/>
|
||||
<text x="60" y="97.6" fill="#59636e" font-size="12" text-anchor="end">40k</text>
|
||||
<line x1="70" y1="30.0" x2="772" y2="30.0" stroke="#d8dee4" stroke-width="1"/>
|
||||
<text x="60" y="34.0" fill="#59636e" font-size="12" text-anchor="end">50k</text>
|
||||
<line x1="70.0" y1="30" x2="70.0" y2="348" stroke="#d8dee4" stroke-width="1" stroke-dasharray="2 4"/>
|
||||
<text x="70.0" y="370" fill="#59636e" font-size="12" text-anchor="middle">Jan 18</text>
|
||||
<line x1="238.7" y1="30" x2="238.7" y2="348" stroke="#d8dee4" stroke-width="1" stroke-dasharray="2 4"/>
|
||||
<text x="238.7" y="370" fill="#59636e" font-size="12" text-anchor="middle">Jan 23</text>
|
||||
<line x1="407.3" y1="30" x2="407.3" y2="348" stroke="#d8dee4" stroke-width="1" stroke-dasharray="2 4"/>
|
||||
<text x="407.3" y="370" fill="#59636e" font-size="12" text-anchor="middle">Jan 28</text>
|
||||
<line x1="576.0" y1="30" x2="576.0" y2="348" stroke="#d8dee4" stroke-width="1" stroke-dasharray="2 4"/>
|
||||
<text x="576.0" y="370" fill="#59636e" font-size="12" text-anchor="middle">Feb 2</text>
|
||||
<line x1="744.7" y1="30" x2="744.7" y2="348" stroke="#d8dee4" stroke-width="1" stroke-dasharray="2 4"/>
|
||||
<text x="744.7" y="370" fill="#59636e" font-size="12" text-anchor="middle">Feb 7</text>
|
||||
<polygon points="70,348 70.0,348.0 123.9,332.7 137.9,327.6 149.4,322.6 171.9,307.3 177.3,302.2 187.8,292.0 192.2,286.9 198.6,281.8 212.6,266.6 222.0,256.4 226.3,251.3 234.5,246.2 239.6,241.1 244.2,236.1 247.5,231.0 251.4,225.9 274.2,210.6 282.2,205.5 290.0,200.4 300.7,195.4 312.3,190.3 320.2,185.2 355.4,164.8 373.7,159.7 409.0,149.6 430.4,144.5 458.7,139.4 492.0,134.3 526.5,129.2 559.4,124.1 618.6,113.9 685.6,103.8 718.9,98.7 772.0,93.6 772,348" fill="#1a7f3720"/>
|
||||
<polyline points="70.0,348.0 123.9,332.7 137.9,327.6 149.4,322.6 171.9,307.3 177.3,302.2 187.8,292.0 192.2,286.9 198.6,281.8 212.6,266.6 222.0,256.4 226.3,251.3 234.5,246.2 239.6,241.1 244.2,236.1 247.5,231.0 251.4,225.9 274.2,210.6 282.2,205.5 290.0,200.4 300.7,195.4 312.3,190.3 320.2,185.2 355.4,164.8 373.7,159.7 409.0,149.6 430.4,144.5 458.7,139.4 492.0,134.3 526.5,129.2 559.4,124.1 618.6,113.9 685.6,103.8 718.9,98.7 772.0,93.6" fill="none" stroke="#1a7f37" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="772.0" cy="93.6" r="4" fill="#1a7f37"/>
|
||||
<text x="400.0" y="20" fill="#1f2328" font-size="14" font-weight="600" text-anchor="middle">affaan-m/ECC · first 40,000 stars</text>
|
||||
<text x="70" y="390" fill="#59636e" font-size="11">Jan 18, 2026 – Feb 7, 2026 · source: GitHub stargazers API</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -359,6 +359,7 @@
|
||||
],
|
||||
"rules": ["common"],
|
||||
"skills": [
|
||||
"rails-patterns",
|
||||
"tdd-workflow",
|
||||
"verification-loop"
|
||||
],
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# ECC and the Ito desk
|
||||
|
||||
ECC is the public agentic-engineering toolkit; the Ito desk is Affaan's
|
||||
private ops system. The connection surface in this repo is the set of
|
||||
public `ito-*` skills (`skills/ito-baskets`, `skills/ito-compute`,
|
||||
`skills/ito-inference`, `skills/ito-training`). Each of them is a thin
|
||||
pointer: it names the supported boundary and hands real work to the
|
||||
separately installed canonical CLI or MCP server. ECC itself implements no
|
||||
compute booking, inference serving, training stack or basket trading, and
|
||||
nothing here may claim those capabilities exist inside this repo.
|
||||
|
||||
Desk-side work that touches ECC runs as bounded lane tasks. The lane-worker
|
||||
doctrine (see `docs/LANE-RULES.md`) is: one worker, one task, one branch,
|
||||
one PR or one receipt; real work only, meaning code edits, tests, commits
|
||||
and a PR, with the final message as the receipt; no self-review loops, no
|
||||
receipt ledgers, no merging to main, no publishing, no deployments, no
|
||||
messages; blocked means naming exactly who or what unblocks. The doctrine
|
||||
exists because unbounded agent loops were the dominant failure mode of the
|
||||
desk's earlier automation.
|
||||
|
||||
The merge rule for anything desk-related in this repo: fixes and tests
|
||||
merge freely. Anything that adds a third-party tool, a vendor-named skill,
|
||||
or an external link waits for Affaan's explicit yes, recorded before merge.
|
||||
The living desk plan is `docs/PLAN.md` in `Ito-Markets/ito-desk`; task
|
||||
schemas and the spec book live under `docs/spec/` in the same repo. This
|
||||
file only describes the relationship; the plan repo is the source of truth.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Lane rules
|
||||
|
||||
These are the working rules for bounded lane workers (human or agent) that
|
||||
execute tasks against this repository from the Ito workstream system. They
|
||||
are copied verbatim from the lane registry
|
||||
(`lanes/RULES.md` in the Ito workstream system on the ops mini,
|
||||
2026-09-16) so a worker reading only this repo sees the same contract.
|
||||
One task, one branch, one PR or one receipt, then stop.
|
||||
|
||||
---
|
||||
|
||||
## Lane rules (every codex exec brief starts by reading this)
|
||||
You are one bounded worker. One task, one branch, one PR or one receipt, then stop.
|
||||
- Real work only: edit code, run the tests, commit, push, open the PR. No receipts about receipts, no independent review of your own output, no hashing manifests, no ledgers, no acceptance JSONs, no skill self-patching. Your final message is the receipt (under 300 words: what changed, PR link, test command and result, what is blocked and on whom).
|
||||
- Never merge to main, never publish to npm, never deploy, never send email or messages, never change Hermes profiles or launchd on the mini unless the brief says so explicitly.
|
||||
- Commits: plain messages, no Co-Authored-By or generated-with trailers, no em dashes anywhere.
|
||||
- Worktrees and caches go under ~/GitHub/ECC-worktrees or ~/GitHub on the Pro, /Volumes/Agent-Runtime/workspaces on the mini, never on the mini root disk.
|
||||
- If blocked (missing credential, approval needed, conflicting work), stop and say exactly what is needed. Do not wait, poll, or sleep.
|
||||
- Time box: finish in one pass. Do not spawn subagents.
|
||||
@@ -73,6 +73,13 @@ capsule recording to the hooks that already log session activity. Then the
|
||||
next two plan slices: offline retrospective grouping over capsules (no new
|
||||
rollouts) and forced-compaction tests that prove pinned constraints survive.
|
||||
|
||||
Offline code preparation is available as `capsule group` over explicitly
|
||||
selected, verified local snapshots from one task family. It only groups recorded
|
||||
counts and digests; it does not run candidates, score outcomes or promote changes.
|
||||
This utility does not fulfill the executor, hook-recording or stable-taskset
|
||||
prerequisites for the operational milestone below. See the
|
||||
[retrospective contract](architecture/eval-harness-frameworks.md#offline-retrospective-preparation).
|
||||
|
||||
### Track C: operator skills
|
||||
|
||||
The four desk-pattern skills are present in this candidate: operator approval
|
||||
|
||||
@@ -703,7 +703,7 @@ Suggested payload:
|
||||
"skippedModules": []
|
||||
},
|
||||
"source": {
|
||||
"repoVersion": "2.2.1",
|
||||
"repoVersion": "2.2.2",
|
||||
"repoCommit": "git-sha",
|
||||
"manifestVersion": 1
|
||||
},
|
||||
|
||||
@@ -29,8 +29,10 @@ The other modules expose local utilities, not a trust decision about code.
|
||||
| Replay | `replay.js`, `effect-fence.js` | 04 replay-safe branching | Declared determinism and effect class per tool, content-addressed fixtures, `tool.fixture_missing` fail-closed replay, retired child preload refuses execution |
|
||||
| Receipt | `receipt.js` | 07 verifiable receipts | Offline receipt over capsule root, entry count, artifact digest, and gate receipt; detached signature interface; verification names the failing check |
|
||||
|
||||
Epics 05 (offline self-improvement) and 06 (causal triage and compaction
|
||||
invariance) are not implemented. They consume the records these five produce.
|
||||
Epic 05 has an offline, report-only capsule grouping utility described below.
|
||||
Self-improvement, operational retrospective validation and epic 06 (causal
|
||||
triage and compaction invariance) remain unimplemented. They consume the
|
||||
records these five frameworks produce.
|
||||
|
||||
## Effect classes
|
||||
|
||||
@@ -182,6 +184,64 @@ What the chain does not claim: it does not stop an operator from replacing the
|
||||
whole log. That is the job of a witnessed transparency log, which is a later,
|
||||
opt-in layer outside this package.
|
||||
|
||||
## Offline retrospective preparation
|
||||
|
||||
Select 1 to 100 existing capsule directories from one task family:
|
||||
|
||||
```sh
|
||||
node scripts/eval-harness.js capsule group .ecc/capsules/run-41 .ecc/capsules/run-42
|
||||
```
|
||||
|
||||
```js
|
||||
const { retrospective } = require('./scripts/lib/eval-harness');
|
||||
const report = retrospective.groupCapsules(['.ecc/capsules/run-41', '.ecc/capsules/run-42']);
|
||||
```
|
||||
|
||||
This read-only utility recomputes each projection from the verified metadata and
|
||||
journal snapshot using `capsule.project`. It never uses or repairs a saved
|
||||
`projection.json`. Inputs must be small, quiescent local capsules from the same
|
||||
task family; a mismatch rejects the entire report. There is no directory
|
||||
discovery, hook activation, new rollout, fixture replay or candidate execution.
|
||||
|
||||
`capsule-retrospective/v1` reports the task family, input count, unique capsule
|
||||
count, duplicate count, and groups sorted by declared harness version. Each
|
||||
group contains capsule/entry counts, all five lineage counts, all five declared
|
||||
effect-class counts, and source digest references. Counts describe recorded
|
||||
entries, not unique tasks, attempts, successful effects or independently
|
||||
verified outcomes. Empty journals contribute one capsule and zero entries.
|
||||
Payload scores, verdicts, costs, durations and pass/fail totals are not used.
|
||||
|
||||
The pair `(run_id, capsule_id)` identifies a capsule for deduplication. Repeated
|
||||
paths or copied snapshots count once when their verified projection hashes
|
||||
match. Conflicting snapshots of that identity, including different checkpoints,
|
||||
fail with `retrospective.conflicting_identity`; the utility never picks a winner.
|
||||
Distinct capsule identities remain distinct even if their event shapes match.
|
||||
Source references contain the canonical hash of the identity pair, entry count,
|
||||
root hash, journal digest and projection hash. `report_hash` covers every other
|
||||
report field; input ordering does not change the result. Repeating an input
|
||||
changes input/duplicate counts and the report hash, but not the grouped counts.
|
||||
|
||||
Reports omit directory arguments, raw run/capsule IDs, journal payloads and
|
||||
timestamps. **Task-family and harness-version labels are returned verbatim**
|
||||
and may themselves contain private text or paths. Digest references are not
|
||||
anonymization: they remain linkable and low-entropy IDs can be guessed. Review
|
||||
labels and report content before sharing. Neither hashes nor declared labels
|
||||
authenticate a producer or prove an improvement; `report_only` is always true.
|
||||
|
||||
Any invalid, unreadable or mismatched capsule rejects the whole report with
|
||||
`retrospective.invalid_capsule` and a zero-based input index. Diagnostics omit
|
||||
underlying reader messages and source paths. Mixed families and invalid input
|
||||
lists have separate stable codes. CLI success emits JSON to stdout and exits 0;
|
||||
bad usage exits 2, while verification/refusal exits 1 without partial JSON.
|
||||
The command accepts no flags and does not write a report file. For a directory
|
||||
name beginning with `--`, use a relative `./` prefix or an absolute path.
|
||||
|
||||
This inherits the existing capsule reader's filesystem and memory limits. The
|
||||
100-input cap does not bound journal bytes. It does not isolate hostile files,
|
||||
serialize concurrent writers, validate a signature or establish live provenance.
|
||||
Executor containment, opt-in hook recording, stable-taskset validation and the
|
||||
roadmap's operational retrospective milestone remain separate prerequisites.
|
||||
|
||||
## Verification gate: unavailable
|
||||
|
||||
**Supported candidate execution backends: none, on any OS.** `runGate` and
|
||||
@@ -321,6 +381,7 @@ and does not validate normal prepack or clear a release.
|
||||
```sh
|
||||
node tests/lib/eval-harness/envelope.test.js
|
||||
node tests/lib/eval-harness/capsule.test.js
|
||||
node tests/lib/eval-harness/retrospective.test.js
|
||||
node tests/lib/eval-harness/gate.test.js
|
||||
node tests/lib/eval-harness/security.test.js
|
||||
node tests/lib/eval-harness/replay.test.js
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# TCAS hook: pre-merge deconfliction (slice b, design)
|
||||
|
||||
Status: design only. Nothing in this document is implemented. Slice (a), the live view and the advisory feed it reads, shipped in `VIEW-CONTRACT.md`.
|
||||
|
||||
## Goal
|
||||
|
||||
Stop two agents from finishing overlapping edits and meeting at the merge. The scan already knows when two working sets converge; the hook is what turns that knowledge into a maneuver inside the harness, before either agent commits.
|
||||
|
||||
Push plan wording: "a PreToolUse/Edit hook that reads the advisory feed and returns steer, pause or wait for the lower-priority agent, logged to the capsule."
|
||||
|
||||
## Inputs
|
||||
|
||||
1. The event feed: `GET /api/control-plane/events` on the local control pane, or the same document written to a file by `scripts/proximity-tick.js --json` for sessions without a pane. Events of kind `proximity.advisory` with `action.type` `transmit` or `steer` and a deterministic `id`.
|
||||
2. The hook's own session id. Claude Code passes `session_id` on stdin; the ECC session adapter maps it to the ECC2 `sessions.id` the scan uses. Codex and Hermes use the instruction-backed equivalent (see below).
|
||||
3. The tool call: `tool_name` and `tool_input.file_path` for Edit, Write and MultiEdit. Bash is out of scope for v1.
|
||||
|
||||
## Decision
|
||||
|
||||
For each advisory event whose `subject` includes this session:
|
||||
|
||||
| Event | This session is | Maneuver | Hook result |
|
||||
|---|---|---|---|
|
||||
| `traffic`, action `transmit` | either side | **transmit**: inject the other agent's working set as a system message | exit 0, message on stderr (warn, never block) |
|
||||
| `resolution`, action `steer` | `hold` | **hold**: continue | exit 0, short note |
|
||||
| `resolution`, action `steer` | `steer`, and `file_path` is in the other agent's working set | **pause**: stop editing that file until the other agent's diff lands | exit 2 with the reason (blocks this one tool call) |
|
||||
| `resolution`, action `steer` | `steer`, and `file_path` is not in the other agent's working set | **wait**: allowed, but told to keep to non-overlapping files | exit 0, message on stderr |
|
||||
| `resolution`, action `steer` | `steer`, and a `steer` target exists | **steer**: suggest the disjoint files or subtree the agent should move to | exit 0, message; exit 2 only if the edit is on the shared file |
|
||||
|
||||
The maneuver is deterministic: both agents read the same event, `hold` and `steer` are named in it, so the two sides never pick the same move. This is the TCAS coordination property and it is why the view computes right-of-way once, centrally, rather than each hook deciding.
|
||||
|
||||
`pause` blocks a single tool call, not the session. The agent sees the reason and can pick another file. Blocking is bounded by the event's `at`: an event older than the pane's poll interval times three is stale and the hook does not block on it.
|
||||
|
||||
## Priority
|
||||
|
||||
Right-of-way comes from the event (`action.hold`, `action.steer`). The view computes it as more progress, then earlier start, then stable id (`rightOfWay` in `scripts/lib/agent-proximity/distance.js`). The hook never recomputes it.
|
||||
|
||||
## Logging to the capsule
|
||||
|
||||
Every decision is one entry in the session's capsule journal (`scripts/lib/eval-harness/capsule.js`, hash-linked NDJSON):
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "tcas.decision",
|
||||
"event_id": "proximity.advisory:session-a|session-b:resolution",
|
||||
"session": "session-b",
|
||||
"tool": "Edit",
|
||||
"file": "src/api/users.js",
|
||||
"maneuver": "pause",
|
||||
"blocked": true,
|
||||
"risk": 1,
|
||||
"threshold": { "ta": 0.35, "ra": 0.7, "source": "static" },
|
||||
"at": "2026-09-11T20:01:03.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
The capsule is the baseline counter for the 85 percent goal: rebase and merge-conflict triage incidents per week are counted from these entries plus `git rerere` and conflict markers, two weeks before and two weeks after the hook is on. No percentage is claimed before that.
|
||||
|
||||
## Where it plugs in
|
||||
|
||||
- **Claude Code**: a `PreToolUse` entry in `hooks/hooks.json` with matcher `Edit|Write|MultiEdit`, routed through `scripts/hooks/run-with-flags.js` so `ECC_HOOK_PROFILE` and `ECC_DISABLED_HOOKS` gate it. Script under `scripts/hooks/tcas-pre-edit.js`, helpers in `scripts/lib/control-pane/tcas.js`. Budget: under 200 ms, no network beyond loopback, exit 0 on any parse or fetch error.
|
||||
- **Codex**: no PreToolUse. The instruction-backed equivalent is the `proximity_steer` / `proximity_hold` message the tick already writes into the ECC2 `messages` table, surfaced on the next turn. `pause` degrades to a strong instruction.
|
||||
- **Hermes**: gateway hook on the tool-call path, same decision table, same capsule entry.
|
||||
|
||||
## Off switch and safety
|
||||
|
||||
- Disabled by default. On with `ECC_TCAS_HOOK=1` or the hook profile.
|
||||
- Read-only against the pane. It never writes to the sessions or messages tables.
|
||||
- No lease is acquired. Durable leases are slice (c), the worktree lease table in ecc2 `session/store.rs` next to `messages`; until then a `pause` is a per-call block, not a lock, and two hooks racing on the same file is possible but harmless (both see the same event and the same `steer`).
|
||||
- Fails open. Any error is exit 0 with a `[TCAS]` line on stderr.
|
||||
|
||||
## Tests to write with it
|
||||
|
||||
- Decision table: one test per row above, driven by a fixture event feed and a stdin payload.
|
||||
- Staleness: an event older than the window does not block.
|
||||
- Fail-open: unreachable pane, malformed JSON, missing session id.
|
||||
- Capsule: one entry per decision, hash chain intact, replay reproduces the same bytes.
|
||||
- Integration: two fake sessions with overlapping working sets, the lower-priority one gets exit 2 on the shared file and exit 0 on a disjoint file.
|
||||
|
||||
## Out of scope for (b)
|
||||
|
||||
Learned thresholds, closure-rate escalation, mesh mode, cross-machine airspace, the `x_sem`, `x_vec`, `x_freq` channels (slice g), and the lease table (slice c).
|
||||
@@ -0,0 +1,141 @@
|
||||
# ECC control-plane live view: `ecc.control-plane.view.v1`
|
||||
|
||||
Status: shipped with the control pane (`scripts/lib/control-pane/control-plane-view.js`). Read-only. Advisory only.
|
||||
|
||||
The view joins three things the repo already computes separately and serves them as one JSON document shaped as tasks, lanes and events, so another control plane (the Ito ops board, a Hermes or Codex reader, a hook) can consume it without knowing ECC internals.
|
||||
|
||||
| Input | Where it comes from |
|
||||
|---|---|
|
||||
| Sessions | `scripts/lib/control-pane/state.js`, the ECC2 `sessions` table |
|
||||
| Pairwise proximity | `scripts/lib/agent-proximity/` (noisy-OR over `x_tree`, `x_overlap`, `x_dep`) via `scripts/lib/control-pane/proximity.js` |
|
||||
| 2D projection | `scripts/lib/agent-proximity/projection.js` (rolling z-score, tails clipped at 2.5 / 97.5, PCA) |
|
||||
| Coordination inventory | `scripts/lib/coordination-inventory.js` (PR #3028): declared tasks and sessions, heartbeat freshness, lease conflicts |
|
||||
|
||||
## Endpoints
|
||||
|
||||
Served by `node scripts/control-pane.js` (loopback only, same Host and Origin gate as the rest of the pane):
|
||||
|
||||
| Route | Returns |
|
||||
|---|---|
|
||||
| `GET /control-plane` | Self-contained HTML page: 2D projection canvas, lanes and tasks, event feed. No external scripts. |
|
||||
| `GET /api/control-plane` | The full view document below. |
|
||||
| `GET /api/control-plane/events` | `{ schemaVersion, generatedAt, thresholds, events, counts }` only, for hooks and pollers. |
|
||||
|
||||
The server keeps one projection window per process. Both API routes share a snapshot cached for five seconds, and concurrent refresh requests are coalesced. Reads within that interval do not add samples. After expiry, the next read refreshes the snapshot once; idle intervals do not generate synthetic samples. Failed refreshes return errors rather than healthy empty data. The page rejects failed HTTP responses and invalid view envelopes and shows `offline`. Options on `createControlPaneServer`: `projection` (`windowSize`, `clipPercentiles`), `viewOptions` (`thresholds`, `manifest`, `channelWeights`, `minWindowForZscore`), `proximityOptions` (passed to the scan).
|
||||
|
||||
## Document
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": "ecc.control-plane.view.v1",
|
||||
"generatedAt": "2026-09-11T20:01:00.000Z",
|
||||
"source": { "snapshotSchema": "ecc.control-pane.snapshot.v1", "repoRoot": "...", "dbPath": "..." },
|
||||
"thresholds": { "ta": 0.35, "ra": 0.7, "source": "static" },
|
||||
"lanes": [ { "id": "harness:codex", "label": "codex", "kind": "harness", "taskIds": ["session-a"] } ],
|
||||
"tasks": [ { "...": "see Task" } ],
|
||||
"pairs": [ { "...": "see Pair" } ],
|
||||
"events": [ { "...": "see Event" } ],
|
||||
"projection": { "...": "see Projection" },
|
||||
"inventory": { "...": "see Inventory" },
|
||||
"counts": { "lanes": 1, "tasks": 1, "agents": 1, "pairs": 0, "events": 0, "advisories": 0, "resolutions": 0 },
|
||||
"limits": [ "..." ]
|
||||
}
|
||||
```
|
||||
|
||||
### Task
|
||||
|
||||
One task per session. A session with no changed files is still a task; it has no projection point and no pairs.
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `id` | Session id, unchanged. |
|
||||
| `lane` | Lane id this task belongs to. |
|
||||
| `label` | Session task text, or the id. |
|
||||
| `harness`, `agentType`, `state`, `pid` | From the session row. |
|
||||
| `worktree` | `{ path, branch, base }` or `null`. |
|
||||
| `heartbeatAt`, `updatedAt` | ISO timestamps or `null`. |
|
||||
| `workingSet` | `{ fileCount, files }`: the worktree diff against its base. |
|
||||
| `projection` | `{ point, pairs, maxRisk }` where `point` is `[x, y]` or `null`. `point` is the risk-weighted centroid of the task's pair points in PCA space. |
|
||||
| `inventory` | `{ id, heartbeat, process, authority: "declared-only" }`. `id` is the sanitized identifier used in the inventory manifest; `heartbeat` and `process` are the #3028 observations. |
|
||||
|
||||
### Lane
|
||||
|
||||
A grouping of tasks. Precedence: `task-group` (session `task_group`), then `project`, then `harness`. Ids are prefixed (`group:`, `project:`, `harness:`) so a consumer can tell the kinds apart without reading `kind`.
|
||||
|
||||
### Pair
|
||||
|
||||
One row per agent pair from the airspace scan (only sessions with edits participate).
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `a`, `b` | Session ids. |
|
||||
| `risk`, `level` | Noisy-OR risk and the scan's level (`clear`, `advisory`, `resolution`) at the scan's thresholds. |
|
||||
| `channels` | Raw `{ x_tree, x_overlap, x_dep }` in [0, 1]. |
|
||||
| `normalized` | The same after z-score, clip and map-back, or equal to `channels` while the window is cold. |
|
||||
| `point` | `[pc1, pc2]` PCA scores. |
|
||||
|
||||
### Event
|
||||
|
||||
Something an operator or a hook may act on. Ids are deterministic across polls so a consumer can dedupe.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "proximity.advisory:session-a|session-b:resolution",
|
||||
"kind": "proximity.advisory",
|
||||
"level": "resolution",
|
||||
"severity": "critical",
|
||||
"at": "2026-09-11T20:01:00.000Z",
|
||||
"subject": { "a": "session-a", "b": "session-b", "aLabel": "...", "bLabel": "..." },
|
||||
"risk": 1,
|
||||
"distance": 0,
|
||||
"channels": { "x_tree": 1, "x_overlap": 1, "x_dep": 0 },
|
||||
"threshold": { "ta": 0.35, "ra": 0.7, "crossed": "ra", "source": "static" },
|
||||
"action": { "type": "steer", "steer": "session-b", "hold": "session-a" },
|
||||
"message": "Resolution advisory: session-b steers, session-a holds (risk 100%, static threshold 0.7)."
|
||||
}
|
||||
```
|
||||
|
||||
| Kind | Levels | Action types | Source |
|
||||
|---|---|---|---|
|
||||
| `proximity.advisory` | `traffic` (risk at or above `ta`), `resolution` (at or above `ra`) | `transmit` (both agents share intent), `steer` (`steer` moves, `hold` keeps course) | Every pair link, evaluated against the view's thresholds. Right-of-way: more progress, then earlier start, then stable id. |
|
||||
| `inventory.lease-conflict` | `conflict` | `review` | #3028 `leaseConflicts`. Declared-only, never a lock. |
|
||||
|
||||
Thresholds are static per view (`source: "static"`). A learned threshold, closure-rate escalation, and the `pause` and `wait` maneuvers are slice (b), see `TCAS-HOOK.md`.
|
||||
|
||||
### Projection
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "pca",
|
||||
"channels": ["x_tree", "x_overlap", "x_dep"],
|
||||
"weights": { "x_tree": 0.25, "x_overlap": 1, "x_dep": 0.9 },
|
||||
"normalization": "zscore-clipped",
|
||||
"window": { "samples": 12, "percentiles": [2.5, 97.5], "channels": [ { "channel": "x_tree", "mean": 0.39, "stddev": 0.42, "clipLow": -0.92, "clipHigh": 1.45 } ] },
|
||||
"pca": { "loadings": [ { "x_tree": 0.12, "x_overlap": 0.87, "x_dep": -0.47 }, { "...": "..." } ], "explainedVariance": [0.6, 0.39] },
|
||||
"agents": [ { "agentId": "session-a", "point": [0.18, 0.41], "pairs": 3, "maxRisk": 1 } ]
|
||||
}
|
||||
```
|
||||
|
||||
Pipeline per poll: every pair's channel vector is pushed into a rolling window (default 512 samples). Once the window holds at least 8 samples, each channel is z-scored against the window, clipped to the window's 2.5th and 97.5th percentile (in z units), mapped back to [0, 1], multiplied by the static channel weight, and the weighted matrix goes through PCA (Jacobi on the 3x3 covariance). Below 8 samples the raw channel values are used and `normalization` says `raw`. A channel with zero variance maps to 0.5. Degenerate inputs (fewer than two pairs, zero total variance) give zero scores, never NaN.
|
||||
|
||||
The projection is a display. It never changes `risk`, the advisory level, or right-of-way.
|
||||
|
||||
### Inventory
|
||||
|
||||
The #3028 report with the per-task rows folded into `tasks[].inventory`. Kept at the top level: `status` (`ok` or `unavailable` with `reason`), `truncated` (more than 64 sessions), `observedAt`, `mode: "read-only"`, `activity`, `leaseConflicts`, `warnings`, `coverage`, `limits`. The manifest is built from the live sessions (ids sanitized to the inventory alphabet, paths from the working set, heartbeat from the session row, declared session status `open` for running/pending/idle, `closed` for completed/failed/stopped). An external manifest (`viewOptions.manifest`) can add `goals`, `leases`, `repositories` and extra `tasks`; the inventory then reports lease conflicts and goal activity for them.
|
||||
|
||||
## Reuse in the Ito ops control plane
|
||||
|
||||
The shape to copy is `task`, `lane`, `event`:
|
||||
|
||||
- a **task** has an `id`, a `lane`, a `state`, an optional position, and an observation block whose `authority` says how much to trust it;
|
||||
- a **lane** is a named group with ordered `taskIds`;
|
||||
- an **event** has a stable `id`, a `kind`, a `level`, a `severity`, an `at`, a `subject`, an `action` with a `type`, and a human `message`.
|
||||
|
||||
Nothing in the shape is ECC-specific except the event kinds. An ops board that renders lanes of tasks and a feed of events can render this document as-is, and can emit its own kinds (`deal.stalled`, `bridge.down`) into the same feed.
|
||||
|
||||
## What this does not do
|
||||
|
||||
- No leases are acquired, no agent is paused or steered. Consumers act; the view reports.
|
||||
- No conflict-reduction percentage is claimed. The 85 percent goal in the push plan is measured two weeks before and after slice (b), not here.
|
||||
- No semantic, call-graph or frequency channel yet (slice (g)). PCA picks new channels up automatically when they land in the scan.
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||

|
||||
|
||||
[](https://github.com/affaan-m/ECC/stargazers)
|
||||
[](https://github.com/affaan-m/ECC/network/members)
|
||||
[](https://github.com/affaan-m/ECC)
|
||||
[](https://github.com/affaan-m/ECC/forks)
|
||||
[](https://github.com/affaan-m/ECC/graphs/contributors)
|
||||
[](https://www.npmjs.com/package/ecc-universal)
|
||||
[](https://www.npmjs.com/package/ecc-agentshield)
|
||||
|
||||
@@ -33,6 +33,30 @@ one harness's hook support.
|
||||
- Procedural memory remains in rules and instincts, subject to their existing
|
||||
promotion and validation gates.
|
||||
|
||||
### Retrieval completeness and current state
|
||||
|
||||
A bounded scan can be incomplete even when it has found a matching ID. Direct
|
||||
reads reject truncated scans and scans containing invalid or unreadable memory
|
||||
documents before claiming absence, uniqueness or complete backlinks. The core
|
||||
error is `ECC_MEMORY_INCOMPLETE`; local MCP returns the safe tool error
|
||||
`MEMORY_READ_INCOMPLETE`. No partial memory content is returned in that case.
|
||||
Search retains its existing diagnostics so callers can inspect partial results
|
||||
without interpreting them as a complete inventory. Entries excluded by the
|
||||
existing hidden-file or symlink policy remain excluded; this does not bypass
|
||||
filesystem safety or imply an atomic snapshot across concurrent edits.
|
||||
|
||||
Failing a direct read because another document is malformed is an intentional
|
||||
tradeoff: the operator must repair the authorized vault before relying on a
|
||||
complete ID lookup. Use the existing doctor to inspect problems. Do not expand
|
||||
scope or permissions to make a failed lookup pass.
|
||||
|
||||
Supersession links are references, not automatic revocations. The existing
|
||||
operator-reviewed status field controls active search; a direct read remains
|
||||
available for explicit historical inspection once the scan is complete. Evidence
|
||||
matching and lexical relevance do not establish current truth, authenticated
|
||||
authorship or authority to execute actions. Those checks belong to the consuming
|
||||
workflow, with original evidence retained when a fact changes.
|
||||
|
||||
### Threat boundary
|
||||
|
||||
The first-release runtime defends against hostile vault documents, stable
|
||||
|
||||
+7
-7
@@ -50,13 +50,13 @@ Este es un **plugin de IA para codificación listo para producción** que propor
|
||||
## Orquestación de Agentes
|
||||
|
||||
Usa agentes proactivamente sin prompt del usuario:
|
||||
- Solicitudes de features complejas → **planner**
|
||||
- Código recién escrito/modificado → **code-reviewer**
|
||||
- Corrección de bug o nueva feature → **tdd-guide**
|
||||
- Decisión arquitectónica → **architect**
|
||||
- Código sensible a la seguridad → **security-reviewer**
|
||||
- Bucles autónomos / monitoreo de bucles → **loop-operator**
|
||||
- Confiabilidad y costo de la configuración del harness → **harness-optimizer**
|
||||
- Solicitudes de features complejas → **ecc:planner**
|
||||
- Código recién escrito/modificado → **ecc:code-reviewer**
|
||||
- Corrección de bug o nueva feature → **ecc:tdd-guide**
|
||||
- Decisión arquitectónica → **ecc:architect**
|
||||
- Código sensible a la seguridad → **ecc:security-reviewer**
|
||||
- Bucles autónomos / monitoreo de bucles → **ecc:loop-operator**
|
||||
- Confiabilidad y costo de la configuración del harness → **ecc:harness-optimizer**
|
||||
|
||||
Usa ejecución paralela para operaciones independientes — lanza múltiples agentes simultáneamente.
|
||||
|
||||
|
||||
+3
-3
@@ -4,13 +4,13 @@
|
||||
|
||||

|
||||
|
||||
[](https://github.com/affaan-m/ECC/stargazers)
|
||||
[](https://github.com/affaan-m/ECC/network/members)
|
||||
[](https://github.com/affaan-m/ECC)
|
||||
[](https://github.com/affaan-m/ECC/forks)
|
||||
[](https://github.com/affaan-m/ECC/graphs/contributors)
|
||||
[](https://www.npmjs.com/package/ecc-universal)
|
||||
[](https://www.npmjs.com/package/ecc-agentshield)
|
||||
[](https://github.com/marketplace/ecc-tools)
|
||||
[](LICENSE)
|
||||
[](../../LICENSE)
|
||||

|
||||

|
||||

|
||||
|
||||
@@ -2,29 +2,36 @@
|
||||
|
||||
## Agentes Disponibles
|
||||
|
||||
Ubicados en `~/.claude/agents/`:
|
||||
Los agentes de ECC se distribuyen con el plugin `ecc@ecc`, no en `~/.claude/agents/`.
|
||||
Se invocan a través de la herramienta Agent con un `subagent_type` con ámbito de plugin:
|
||||
|
||||
```text
|
||||
Agent(subagent_type: "ecc:planner", prompt: "...")
|
||||
```
|
||||
|
||||
| Agente | Propósito | Cuándo Usar |
|
||||
|--------|-----------|-------------|
|
||||
| planner | Planificación de implementación | Features complejas, refactoring |
|
||||
| architect | Diseño de sistemas | Decisiones arquitectónicas |
|
||||
| tdd-guide | Desarrollo guiado por pruebas | Nuevas features, corrección de bugs |
|
||||
| code-reviewer | Revisión de código | Después de escribir código |
|
||||
| security-reviewer | Análisis de seguridad | Antes de los commits |
|
||||
| build-error-resolver | Corrección de errores de build | Cuando el build falla |
|
||||
| e2e-runner | Testing E2E | Flujos de usuario críticos |
|
||||
| refactor-cleaner | Limpieza de código muerto | Mantenimiento de código |
|
||||
| doc-updater | Documentación | Actualización de docs |
|
||||
| rust-reviewer | Revisión de código Rust | Proyectos Rust |
|
||||
| harmonyos-app-resolver | Desarrollo de apps HarmonyOS | Proyectos HarmonyOS/ArkTS |
|
||||
| ecc:planner | Planificación de implementación | Features complejas, refactoring |
|
||||
| ecc:architect | Diseño de sistemas | Decisiones arquitectónicas |
|
||||
| ecc:tdd-guide | Desarrollo guiado por pruebas | Nuevas features, corrección de bugs |
|
||||
| ecc:code-reviewer | Revisión de código | Después de escribir código |
|
||||
| ecc:security-reviewer | Análisis de seguridad | Antes de los commits |
|
||||
| ecc:build-error-resolver | Corrección de errores de build | Cuando el build falla |
|
||||
| ecc:e2e-runner | Testing E2E | Flujos de usuario críticos |
|
||||
| ecc:refactor-cleaner | Limpieza de código muerto | Mantenimiento de código |
|
||||
| ecc:doc-updater | Documentación | Actualización de docs |
|
||||
| ecc:rust-reviewer | Revisión de código Rust | Proyectos Rust |
|
||||
| ecc:harmonyos-app-resolver | Desarrollo de apps HarmonyOS | Proyectos HarmonyOS/ArkTS |
|
||||
|
||||
Para el roster completo de 68 agentes, ver `/ecc:ecc-guide`.
|
||||
|
||||
## Uso Inmediato de Agentes
|
||||
|
||||
Sin necesidad de prompt del usuario:
|
||||
1. Solicitudes de features complejas - Usar el agente **planner**
|
||||
2. Código recién escrito/modificado - Usar el agente **code-reviewer**
|
||||
3. Corrección de bug o nueva feature - Usar el agente **tdd-guide**
|
||||
4. Decisión arquitectónica - Usar el agente **architect**
|
||||
1. Solicitudes de features complejas - Usar el agente **ecc:planner**
|
||||
2. Código recién escrito/modificado - Usar el agente **ecc:code-reviewer**
|
||||
3. Corrección de bug o nueva feature - Usar el agente **ecc:tdd-guide**
|
||||
4. Decisión arquitectónica - Usar el agente **ecc:architect**
|
||||
|
||||
## Ejecución Paralela de Tareas
|
||||
|
||||
|
||||
@@ -50,13 +50,13 @@
|
||||
## エージェントオーケストレーション
|
||||
|
||||
ユーザーのプロンプトなしで積極的にエージェントを使用する:
|
||||
- 複雑な機能リクエスト → **planner**
|
||||
- コードの作成/変更直後 → **code-reviewer**
|
||||
- バグ修正または新機能 → **tdd-guide**
|
||||
- アーキテクチャの意思決定 → **architect**
|
||||
- セキュリティに関わるコード → **security-reviewer**
|
||||
- 自律ループ / ループ監視 → **loop-operator**
|
||||
- ハーネス設定の信頼性とコスト → **harness-optimizer**
|
||||
- 複雑な機能リクエスト → **ecc:planner**
|
||||
- コードの作成/変更直後 → **ecc:code-reviewer**
|
||||
- バグ修正または新機能 → **ecc:tdd-guide**
|
||||
- アーキテクチャの意思決定 → **ecc:architect**
|
||||
- セキュリティに関わるコード → **ecc:security-reviewer**
|
||||
- 自律ループ / ループ監視 → **ecc:loop-operator**
|
||||
- ハーネス設定の信頼性とコスト → **ecc:harness-optimizer**
|
||||
|
||||
独立した操作には並列実行を使用する — 複数のエージェントを同時に起動する。
|
||||
|
||||
|
||||
+1867
-653
File diff suppressed because it is too large
Load Diff
@@ -2,27 +2,34 @@
|
||||
|
||||
## 利用可能な Agent
|
||||
|
||||
`~/.claude/agents/` に配置:
|
||||
ECC の Agent は `ecc@ecc` プラグインに同梱されており、`~/.claude/agents/` には配置されません。
|
||||
Agent ツールではプラグインスコープの `subagent_type` で呼び出します:
|
||||
|
||||
```text
|
||||
Agent(subagent_type: "ecc:planner", prompt: "...")
|
||||
```
|
||||
|
||||
| Agent | 目的 | 使用タイミング |
|
||||
|-------|---------|-------------|
|
||||
| planner | 実装計画 | 複雑な機能、リファクタリング |
|
||||
| architect | システム設計 | アーキテクチャの意思決定 |
|
||||
| tdd-guide | テスト駆動開発 | 新機能、バグ修正 |
|
||||
| code-reviewer | コードレビュー | コード記述後 |
|
||||
| security-reviewer | セキュリティ分析 | コミット前 |
|
||||
| build-error-resolver | ビルドエラー修正 | ビルド失敗時 |
|
||||
| e2e-runner | E2Eテスト | 重要なユーザーフロー |
|
||||
| refactor-cleaner | デッドコードクリーンアップ | コードメンテナンス |
|
||||
| doc-updater | ドキュメント | ドキュメント更新 |
|
||||
| ecc:planner | 実装計画 | 複雑な機能、リファクタリング |
|
||||
| ecc:architect | システム設計 | アーキテクチャの意思決定 |
|
||||
| ecc:tdd-guide | テスト駆動開発 | 新機能、バグ修正 |
|
||||
| ecc:code-reviewer | コードレビュー | コード記述後 |
|
||||
| ecc:security-reviewer | セキュリティ分析 | コミット前 |
|
||||
| ecc:build-error-resolver | ビルドエラー修正 | ビルド失敗時 |
|
||||
| ecc:e2e-runner | E2Eテスト | 重要なユーザーフロー |
|
||||
| ecc:refactor-cleaner | デッドコードクリーンアップ | コードメンテナンス |
|
||||
| ecc:doc-updater | ドキュメント | ドキュメント更新 |
|
||||
|
||||
全 68 Agent の一覧は `/ecc:ecc-guide` を参照。
|
||||
|
||||
## Agent の即座の使用
|
||||
|
||||
ユーザープロンプト不要:
|
||||
1. 複雑な機能リクエスト - **planner** agent を使用
|
||||
2. コード作成/変更直後 - **code-reviewer** agent を使用
|
||||
3. バグ修正または新機能 - **tdd-guide** agent を使用
|
||||
4. アーキテクチャの意思決定 - **architect** agent を使用
|
||||
1. 複雑な機能リクエスト - **ecc:planner** agent を使用
|
||||
2. コード作成/変更直後 - **ecc:code-reviewer** agent を使用
|
||||
3. バグ修正または新機能 - **ecc:tdd-guide** agent を使用
|
||||
4. アーキテクチャの意思決定 - **ecc:architect** agent を使用
|
||||
|
||||
## 並列タスク実行
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ Este repositório contém apenas o código. Os guias explicam tudo.
|
||||
|
||||
## O Que Há de Novo
|
||||
|
||||
### v2.2.1 — Instalação Guiada para Múltiplos Harnesses (Ago 2026)
|
||||
### v2.2.2 — Instalação Guiada para Múltiplos Harnesses (Ago 2026)
|
||||
|
||||
Adiciona uma instalação revisável para Claude Code, Codex e Kimi Code, com uma entrada de comando npm sincronizada.
|
||||
|
||||
|
||||
+9
-9
@@ -2,7 +2,7 @@
|
||||
|
||||
Bu, yazılım geliştirme için 68 özel agent, 292 skill, 94 command ve otomatik hook iş akışları sağlayan **üretime hazır bir AI kodlama eklentisidir**.
|
||||
|
||||
**Sürüm:** 2.2.1
|
||||
**Sürüm:** 2.2.2
|
||||
|
||||
## Temel İlkeler
|
||||
|
||||
@@ -47,14 +47,14 @@ Bu, yazılım geliştirme için 68 özel agent, 292 skill, 94 command ve otomati
|
||||
## Agent Orkestrasyonu
|
||||
|
||||
Agentları kullanıcı istemi olmadan proaktif olarak kullanın:
|
||||
- Karmaşık özellik istekleri → **planner**
|
||||
- Yeni yazılan/değiştirilen kod → **code-reviewer**
|
||||
- Hata düzeltme veya yeni özellik → **tdd-guide**
|
||||
- Mimari karar → **architect**
|
||||
- Güvenlik açısından hassas kod → **security-reviewer**
|
||||
- Çok kanallı iletişim önceliklendirme → **chief-of-staff**
|
||||
- Otonom döngüler / döngü izleme → **loop-operator**
|
||||
- Harness yapılandırma güvenilirliği ve maliyeti → **harness-optimizer**
|
||||
- Karmaşık özellik istekleri → **ecc:planner**
|
||||
- Yeni yazılan/değiştirilen kod → **ecc:code-reviewer**
|
||||
- Hata düzeltme veya yeni özellik → **ecc:tdd-guide**
|
||||
- Mimari karar → **ecc:architect**
|
||||
- Güvenlik açısından hassas kod → **ecc:security-reviewer**
|
||||
- Çok kanallı iletişim önceliklendirme → **ecc:chief-of-staff**
|
||||
- Otonom döngüler / döngü izleme → **ecc:loop-operator**
|
||||
- Harness yapılandırma güvenilirliği ve maliyeti → **ecc:harness-optimizer**
|
||||
|
||||
Bağımsız işlemler için paralel yürütme kullanın — birden fazla agenti aynı anda başlatın.
|
||||
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ Bu repository yalnızca ham kodu içerir. Rehberler her şeyi açıklıyor.
|
||||
|
||||
## Yenilikler
|
||||
|
||||
### v2.2.1 — Rehberli Çoklu Harness Kurulumu (Ağu 2026)
|
||||
### v2.2.2 — Rehberli Çoklu Harness Kurulumu (Ağu 2026)
|
||||
|
||||
Claude Code, Codex ve Kimi Code için incelenebilir çoklu harness kurulumu ve eşitlenmiş npm komut girişi eklendi.
|
||||
|
||||
|
||||
@@ -2,28 +2,35 @@
|
||||
|
||||
## Mevcut Agent'lar
|
||||
|
||||
`~/.claude/agents/` dizininde bulunur:
|
||||
ECC agent'ları `ecc@ecc` eklentisiyle birlikte gelir, `~/.claude/agents/` dizininde bulunmaz.
|
||||
Agent aracıyla eklenti kapsamlı bir `subagent_type` ile çağrılır:
|
||||
|
||||
```text
|
||||
Agent(subagent_type: "ecc:planner", prompt: "...")
|
||||
```
|
||||
|
||||
| Agent | Amaç | Ne Zaman Kullanılır |
|
||||
|-------|---------|-------------|
|
||||
| planner | Uygulama planlaması | Karmaşık özellikler, refactoring |
|
||||
| architect | Sistem tasarımı | Mimari kararlar |
|
||||
| tdd-guide | Test odaklı geliştirme | Yeni özellikler, hata düzeltmeleri |
|
||||
| code-reviewer | Kod incelemesi | Kod yazdıktan sonra |
|
||||
| security-reviewer | Güvenlik analizi | Commit'lerden önce |
|
||||
| build-error-resolver | Build hatalarını düzeltme | Build başarısız olduğunda |
|
||||
| e2e-runner | E2E testleri | Kritik kullanıcı akışları |
|
||||
| refactor-cleaner | Ölü kod temizliği | Kod bakımı |
|
||||
| doc-updater | Dokümantasyon | Dokümanları güncelleme |
|
||||
| rust-reviewer | Rust kod incelemesi | Rust projeleri |
|
||||
| ecc:planner | Uygulama planlaması | Karmaşık özellikler, refactoring |
|
||||
| ecc:architect | Sistem tasarımı | Mimari kararlar |
|
||||
| ecc:tdd-guide | Test odaklı geliştirme | Yeni özellikler, hata düzeltmeleri |
|
||||
| ecc:code-reviewer | Kod incelemesi | Kod yazdıktan sonra |
|
||||
| ecc:security-reviewer | Güvenlik analizi | Commit'lerden önce |
|
||||
| ecc:build-error-resolver | Build hatalarını düzeltme | Build başarısız olduğunda |
|
||||
| ecc:e2e-runner | E2E testleri | Kritik kullanıcı akışları |
|
||||
| ecc:refactor-cleaner | Ölü kod temizliği | Kod bakımı |
|
||||
| ecc:doc-updater | Dokümantasyon | Dokümanları güncelleme |
|
||||
| ecc:rust-reviewer | Rust kod incelemesi | Rust projeleri |
|
||||
|
||||
68 agent'ın tam listesi için `/ecc:ecc-guide` bölümüne bakın.
|
||||
|
||||
## Anlık Agent Kullanımı
|
||||
|
||||
Kullanıcı istemi gerekmez:
|
||||
1. Karmaşık özellik istekleri - **planner** agent kullan
|
||||
2. Kod yeni yazıldı/değiştirildi - **code-reviewer** agent kullan
|
||||
3. Hata düzeltmesi veya yeni özellik - **tdd-guide** agent kullan
|
||||
4. Mimari karar - **architect** agent kullan
|
||||
1. Karmaşık özellik istekleri - **ecc:planner** agent kullan
|
||||
2. Kod yeni yazıldı/değiştirildi - **ecc:code-reviewer** agent kullan
|
||||
3. Hata düzeltmesi veya yeni özellik - **ecc:tdd-guide** agent kullan
|
||||
4. Mimari karar - **ecc:architect** agent kullan
|
||||
|
||||
## Paralel Görev Yürütme
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/affaan-m/ECC/stargazers"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Fstars&style=flat" alt="Stars" /></a>
|
||||
<a href="https://github.com/affaan-m/ECC/network/members"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Fforks&style=flat" alt="Forks" /></a>
|
||||
<a href="https://github.com/affaan-m/ECC"><img src="https://img.shields.io/github/stars/affaan-m/ECC?style=flat" alt="GitHub stars" /></a>
|
||||
<a href="https://github.com/affaan-m/ECC/forks"><img src="https://img.shields.io/github/forks/affaan-m/ECC?style=flat" alt="GitHub forks" /></a>
|
||||
<a href="https://github.com/affaan-m/ECC/graphs/contributors"><img src="https://img.shields.io/github/contributors/affaan-m/ECC?style=flat" alt="Contributors" /></a>
|
||||
<a href="https://github.com/marketplace/ecc-tools"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Finstalls&logo=github" alt="GitHub App installs" /></a>
|
||||
</p>
|
||||
@@ -105,7 +105,8 @@
|
||||
<a href="https://www.greptile.com/go/ecc" title="Greptile"><img src="../../assets/images/sponsors/greptile.png" height="54" alt="Greptile" /></a>
|
||||
<a href="https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=ECC" title="Atlas Cloud"><picture><source media="(prefers-color-scheme: dark)" srcset="../../assets/images/sponsors/atlascloud-dark.svg" /><img src="../../assets/images/sponsors/atlascloud.svg" width="154" alt="Atlas Cloud" /></picture></a>
|
||||
<a href="https://www.moonshot.ai" title="Moonshot AI - Kimi"><picture><source media="(prefers-color-scheme: dark)" srcset="../../assets/images/sponsors/moonshot-dark.png" /><img src="../../assets/images/sponsors/moonshot.png" width="132" alt="Moonshot AI - Kimi" /></picture></a>
|
||||
<a href="https://compute.itomarkets.com" title="Itô Markets"><picture><source media="(prefers-color-scheme: light)" srcset="../../assets/images/sponsors/ito-transparent-light.png" /><img src="../../assets/images/sponsors/ito-transparent.png" width="96" alt="Itô Markets" /></picture></a>
|
||||
<a href="https://compute.itomarkets.com" title="Itô Markets"><picture><source media="(prefers-color-scheme: light)" srcset="../../assets/images/sponsors/ito-transparent-light.png" /><img src="../../assets/images/sponsors/ito-transparent.png" width="96" alt="Itô Markets" /></picture></a>
|
||||
<a href="https://serpapi.com/github-ecc" title="SerpApi: Web Search API"><picture><source media="(prefers-color-scheme: dark)" srcset="../../assets/images/sponsors/serpapi-logo-dark-mode.svg" /><img src="../../assets/images/sponsors/serpapi-logo-light-mode.svg" width="200" alt="SerpApi: Web Search API" /></picture></a>
|
||||
</p>
|
||||
|
||||
<sub><strong>Спонсори спільноти:</strong> <a href="https://github.com/mikejmorgan-ai">Mike Morgan</a> · <a href="https://github.com/jasonwu513">@jasonwu513</a> · <a href="https://github.com/1anter">@1anter</a> · <a href="https://github.com/massimotodaro">@massimotodaro</a> · <a href="https://github.com/meadmccabe">@meadmccabe</a></sub>
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@
|
||||
|
||||

|
||||
|
||||
[](https://github.com/affaan-m/ECC/stargazers)
|
||||
[](https://github.com/affaan-m/ECC/network/members)
|
||||
[](https://github.com/affaan-m/ECC)
|
||||
[](https://github.com/affaan-m/ECC/forks)
|
||||
[](https://github.com/affaan-m/ECC/graphs/contributors)
|
||||
[](https://www.npmjs.com/package/ecc-universal)
|
||||
[](https://www.npmjs.com/package/ecc-agentshield)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
这是一个**生产就绪的 AI 编码插件**,提供 68 个专业代理、292 项技能、94 条命令以及自动化钩子工作流,用于软件开发。
|
||||
|
||||
**版本:** 2.2.1
|
||||
**版本:** 2.2.2
|
||||
|
||||
## 核心原则
|
||||
|
||||
@@ -48,14 +48,14 @@
|
||||
|
||||
主动使用智能体,无需用户提示:
|
||||
|
||||
* 复杂功能请求 → **planner**
|
||||
* 刚编写/修改的代码 → **code-reviewer**
|
||||
* 错误修复或新功能 → **tdd-guide**
|
||||
* 架构决策 → **architect**
|
||||
* 安全敏感代码 → **security-reviewer**
|
||||
* 多渠道沟通分流 → **chief-of-staff**
|
||||
* 自主循环 / 循环监控 → **loop-operator**
|
||||
* 线束配置可靠性及成本 → **harness-optimizer**
|
||||
* 复杂功能请求 → **ecc:planner**
|
||||
* 刚编写/修改的代码 → **ecc:code-reviewer**
|
||||
* 错误修复或新功能 → **ecc:tdd-guide**
|
||||
* 架构决策 → **ecc:architect**
|
||||
* 安全敏感代码 → **ecc:security-reviewer**
|
||||
* 多渠道沟通分流 → **ecc:chief-of-staff**
|
||||
* 自主循环 / 循环监控 → **ecc:loop-operator**
|
||||
* 线束配置可靠性及成本 → **ecc:harness-optimizer**
|
||||
|
||||
对于独立操作使用并行执行 — 同时启动多个智能体。
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
## 最新动态
|
||||
|
||||
### v2.2.1 — 引导式多 Harness 安装(2026年8月)
|
||||
### v2.2.2 — 引导式多 Harness 安装(2026年8月)
|
||||
|
||||
新增可审查的 Claude Code、Codex 与 Kimi Code 多 Harness 安装流程,并提供同步的 npm 命令入口。
|
||||
|
||||
@@ -1292,7 +1292,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|
||||
| **上下文文件** | CLAUDE.md + AGENTS.md | AGENTS.md | AGENTS.md | AGENTS.md |
|
||||
| **秘密检测** | 基于钩子 | beforeSubmitPrompt 钩子 | 基于沙箱 | 基于钩子 |
|
||||
| **自动格式化** | PostToolUse 钩子 | afterFileEdit 钩子 | N/A | file.edited 钩子 |
|
||||
| **版本** | 插件 | 插件 | 参考配置 | 2.2.1 |
|
||||
| **版本** | 插件 | 插件 | 参考配置 | 2.2.2 |
|
||||
|
||||
**关键架构决策:**
|
||||
|
||||
|
||||
@@ -2,29 +2,36 @@
|
||||
|
||||
## 可用智能体
|
||||
|
||||
位于 `~/.claude/agents/` 中:
|
||||
ECC 智能体随 `ecc@ecc` 插件一起分发,不在 `~/.claude/agents/` 目录中。
|
||||
它们通过 Agent 工具以插件作用域的 `subagent_type` 调用:
|
||||
|
||||
```text
|
||||
Agent(subagent_type: "ecc:planner", prompt: "...")
|
||||
```
|
||||
|
||||
| 代理 | 用途 | 使用时机 |
|
||||
|-------|---------|-------------|
|
||||
| planner | 实现规划 | 复杂功能、重构 |
|
||||
| architect | 系统设计 | 架构决策 |
|
||||
| tdd-guide | 测试驱动开发 | 新功能、错误修复 |
|
||||
| code-reviewer | 代码审查 | 编写代码后 |
|
||||
| security-reviewer | 安全分析 | 提交前 |
|
||||
| build-error-resolver | 修复构建错误 | 构建失败时 |
|
||||
| e2e-runner | 端到端测试 | 关键用户流程 |
|
||||
| refactor-cleaner | 清理死代码 | 代码维护 |
|
||||
| doc-updater | 文档 | 更新文档 |
|
||||
| rust-reviewer | Rust 代码审查 | Rust 项目 |
|
||||
| ecc:planner | 实现规划 | 复杂功能、重构 |
|
||||
| ecc:architect | 系统设计 | 架构决策 |
|
||||
| ecc:tdd-guide | 测试驱动开发 | 新功能、错误修复 |
|
||||
| ecc:code-reviewer | 代码审查 | 编写代码后 |
|
||||
| ecc:security-reviewer | 安全分析 | 提交前 |
|
||||
| ecc:build-error-resolver | 修复构建错误 | 构建失败时 |
|
||||
| ecc:e2e-runner | 端到端测试 | 关键用户流程 |
|
||||
| ecc:refactor-cleaner | 清理死代码 | 代码维护 |
|
||||
| ecc:doc-updater | 文档 | 更新文档 |
|
||||
| ecc:rust-reviewer | Rust 代码审查 | Rust 项目 |
|
||||
|
||||
完整 68 个智能体的清单参见 `/ecc:ecc-guide`。
|
||||
|
||||
## 即时智能体使用
|
||||
|
||||
无需用户提示:
|
||||
|
||||
1. 复杂的功能请求 - 使用 **planner** 智能体
|
||||
2. 刚编写/修改的代码 - 使用 **code-reviewer** 智能体
|
||||
3. 错误修复或新功能 - 使用 **tdd-guide** 智能体
|
||||
4. 架构决策 - 使用 **architect** 智能体
|
||||
1. 复杂的功能请求 - 使用 **ecc:planner** 智能体
|
||||
2. 刚编写/修改的代码 - 使用 **ecc:code-reviewer** 智能体
|
||||
3. 错误修复或新功能 - 使用 **ecc:tdd-guide** 智能体
|
||||
4. 架构决策 - 使用 **ecc:architect** 智能体
|
||||
|
||||
## 并行任务执行
|
||||
|
||||
|
||||
Generated
+8
-8
@@ -2375,9 +2375,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.4+spec-1.1.0"
|
||||
version = "1.1.6+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
|
||||
checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
@@ -2528,9 +2528,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.4.0"
|
||||
version = "3.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d"
|
||||
checksum = "af5546be8f5378d5414f83733f5c9a2526f4645829edbc1c41790aeef1b38e8b"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"cookie_store",
|
||||
@@ -2548,9 +2548,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613"
|
||||
checksum = "5b0809a01d1ca5a51ca70db32bb2a19157582a526505ef3c19e3b343a59aa5ad"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"http",
|
||||
@@ -2590,9 +2590,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.26.0"
|
||||
version = "1.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
|
||||
checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
|
||||
dependencies = [
|
||||
"atomic",
|
||||
"getrandom 0.4.2",
|
||||
|
||||
@@ -14,6 +14,12 @@ It is usable as an alpha for local experimentation, but it is **not** the finish
|
||||
- worktree-aware session scaffolding
|
||||
- basic multi-session state and output tracking
|
||||
|
||||
Dashboard output is hydrated from SQLite at startup and after recovery, then
|
||||
synchronized with a monotonic database cursor. Because session runners are
|
||||
separate processes, the database remains the cross-process source of truth
|
||||
while steady-state refreshes read only the rows appended since the previous
|
||||
dashboard tick.
|
||||
|
||||
## What This Is For
|
||||
|
||||
ECC 2.0 is the layer above individual harness installs.
|
||||
|
||||
@@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
pub const OUTPUT_BUFFER_LIMIT: usize = 1000;
|
||||
/// Maximum number of cross-process output rows applied during one dashboard refresh.
|
||||
pub const OUTPUT_DELTA_BATCH_LIMIT: usize = 4096;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum OutputStream {
|
||||
@@ -113,16 +115,6 @@ impl SessionOutputStore {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn replace_lines(&self, session_id: &str, lines: Vec<OutputLine>) {
|
||||
let mut buffer: VecDeque<OutputLine> = lines.into_iter().collect();
|
||||
|
||||
while buffer.len() > self.capacity {
|
||||
let _ = buffer.pop_front();
|
||||
}
|
||||
|
||||
self.lock_buffers().insert(session_id.to_string(), buffer);
|
||||
}
|
||||
|
||||
pub fn lines(&self, session_id: &str) -> Vec<OutputLine> {
|
||||
self.lock_buffers()
|
||||
.get(session_id)
|
||||
|
||||
@@ -28,6 +28,31 @@ pub struct StateStore {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct SessionOutputRecord {
|
||||
pub id: i64,
|
||||
pub session_id: String,
|
||||
pub line: OutputLine,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct SessionOutputBatch {
|
||||
pub cursor: i64,
|
||||
pub records: Vec<SessionOutputRecord>,
|
||||
}
|
||||
|
||||
/// Converts one persisted output row into the dashboard's typed record.
|
||||
fn output_record_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<SessionOutputRecord> {
|
||||
let stream: String = row.get(2)?;
|
||||
let text: String = row.get(3)?;
|
||||
let timestamp: String = row.get(4)?;
|
||||
Ok(SessionOutputRecord {
|
||||
id: row.get(0)?,
|
||||
session_id: row.get(1)?,
|
||||
line: OutputLine::new(OutputStream::from_db_value(&stream), text, timestamp),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct HarnessAuditEntry {
|
||||
pub id: i64,
|
||||
@@ -4000,6 +4025,53 @@ impl StateStore {
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
/// Returns a bounded recent-output snapshot and its highest persisted row ID.
|
||||
pub(crate) fn get_output_snapshot(
|
||||
&self,
|
||||
limit_per_session: usize,
|
||||
) -> Result<SessionOutputBatch> {
|
||||
let limit_per_session = i64::try_from(limit_per_session.max(1)).unwrap_or(i64::MAX);
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, session_id, stream, line, timestamp
|
||||
FROM (
|
||||
SELECT id, session_id, stream, line, timestamp,
|
||||
ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY id DESC) AS row_num
|
||||
FROM session_output
|
||||
)
|
||||
WHERE row_num <= ?1
|
||||
ORDER BY id ASC",
|
||||
)?;
|
||||
let records = stmt
|
||||
.query_map(rusqlite::params![limit_per_session], output_record_from_row)?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let cursor = records.last().map(|record| record.id).unwrap_or(0);
|
||||
|
||||
Ok(SessionOutputBatch { cursor, records })
|
||||
}
|
||||
|
||||
/// Returns at most `limit` output rows newer than `cursor` in insertion order.
|
||||
pub(crate) fn get_output_since(
|
||||
&self,
|
||||
cursor: i64,
|
||||
limit: usize,
|
||||
) -> Result<SessionOutputBatch> {
|
||||
let cursor = cursor.max(0);
|
||||
let limit = i64::try_from(limit.max(1)).unwrap_or(i64::MAX);
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, session_id, stream, line, timestamp
|
||||
FROM session_output
|
||||
WHERE id > ?1
|
||||
ORDER BY id ASC
|
||||
LIMIT ?2",
|
||||
)?;
|
||||
let records = stmt
|
||||
.query_map(rusqlite::params![cursor, limit], output_record_from_row)?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let cursor = records.last().map(|record| record.id).unwrap_or(cursor);
|
||||
|
||||
Ok(SessionOutputBatch { cursor, records })
|
||||
}
|
||||
|
||||
pub fn insert_tool_log(
|
||||
&self,
|
||||
session_id: &str,
|
||||
@@ -7382,6 +7454,69 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_cursor_reads_a_bounded_snapshot_then_only_new_rows() -> Result<()> {
|
||||
let tempdir = TestDir::new("store-output-cursor")?;
|
||||
let db = StateStore::open(&tempdir.path().join("state.db"))?;
|
||||
|
||||
db.insert_session(&build_session("session-1", SessionState::Running))?;
|
||||
db.insert_session(&build_session("session-2", SessionState::Running))?;
|
||||
db.append_output_line("session-1", OutputStream::Stdout, "one-a")?;
|
||||
db.append_output_line("session-2", OutputStream::Stderr, "two-a")?;
|
||||
db.append_output_line("session-1", OutputStream::Stdout, "one-b")?;
|
||||
db.append_output_line("session-2", OutputStream::Stdout, "two-b")?;
|
||||
db.append_output_line("session-1", OutputStream::Stdout, "one-c")?;
|
||||
|
||||
let snapshot = db.get_output_snapshot(2)?;
|
||||
assert_eq!(snapshot.cursor, 5);
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.records
|
||||
.iter()
|
||||
.map(|record| (record.session_id.as_str(), record.line.text.as_str()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
("session-2", "two-a"),
|
||||
("session-1", "one-b"),
|
||||
("session-2", "two-b"),
|
||||
("session-1", "one-c"),
|
||||
]
|
||||
);
|
||||
|
||||
db.append_output_line("session-2", OutputStream::Stderr, "two-c")?;
|
||||
db.append_output_line("session-1", OutputStream::Stdout, "one-d")?;
|
||||
let delta = db.get_output_since(snapshot.cursor, 1)?;
|
||||
assert_eq!(delta.cursor, 6);
|
||||
assert_eq!(delta.records.len(), 1);
|
||||
assert_eq!(delta.records[0].session_id, "session-2");
|
||||
assert_eq!(delta.records[0].line.text, "two-c");
|
||||
|
||||
let next = db.get_output_since(delta.cursor, 1)?;
|
||||
assert_eq!(next.cursor, 7);
|
||||
assert_eq!(next.records.len(), 1);
|
||||
assert_eq!(next.records[0].session_id, "session-1");
|
||||
assert_eq!(next.records[0].line.text, "one-d");
|
||||
|
||||
let empty = db.get_output_since(next.cursor, 1)?;
|
||||
assert_eq!(empty.cursor, next.cursor);
|
||||
assert!(empty.records.is_empty());
|
||||
|
||||
let query_plan = db
|
||||
.conn
|
||||
.prepare(
|
||||
"EXPLAIN QUERY PLAN SELECT id FROM session_output WHERE id > ?1 ORDER BY id ASC",
|
||||
)?
|
||||
.query_map(rusqlite::params![snapshot.cursor], |row| {
|
||||
row.get::<_, String>(3)
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
assert!(query_plan
|
||||
.iter()
|
||||
.any(|detail| detail.contains("INTEGER PRIMARY KEY") && detail.contains("rowid>?")));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_round_trip_tracks_unread_counts_and_read_state() -> Result<()> {
|
||||
let tempdir = TestDir::new("store-messages")?;
|
||||
|
||||
+343
-48
@@ -10,7 +10,6 @@ use ratatui::{
|
||||
use regex::Regex;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use std::time::UNIX_EPOCH;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use super::widgets::{budget_state, format_currency, format_token_count, BudgetState, TokenMeter};
|
||||
use crate::comms;
|
||||
@@ -19,12 +18,12 @@ use crate::notifications::{DesktopNotifier, NotificationEvent, WebhookNotifier};
|
||||
use crate::observability::ToolLogEntry;
|
||||
use crate::session::manager;
|
||||
use crate::session::output::{
|
||||
OutputEvent, OutputLine, OutputStream, SessionOutputStore, OUTPUT_BUFFER_LIMIT,
|
||||
OutputLine, OutputStream, OUTPUT_BUFFER_LIMIT, OUTPUT_DELTA_BATCH_LIMIT,
|
||||
};
|
||||
use crate::session::store::{DaemonActivity, FileActivityOverlap, StateStore};
|
||||
use crate::session::store::{DaemonActivity, FileActivityOverlap, SessionOutputRecord, StateStore};
|
||||
use crate::session::{
|
||||
ContextObservationPriority, DecisionLogEntry, FileActivityEntry, Session, SessionGrouping,
|
||||
SessionBoardMeta, SessionHarnessInfo, SessionMessage, SessionState,
|
||||
ContextObservationPriority, DecisionLogEntry, FileActivityEntry, Session, SessionBoardMeta,
|
||||
SessionGrouping, SessionHarnessInfo, SessionMessage, SessionState,
|
||||
};
|
||||
use crate::worktree;
|
||||
|
||||
@@ -79,16 +78,42 @@ struct TestRunSummary {
|
||||
passed: usize,
|
||||
}
|
||||
|
||||
/// Consumes an output cache and returns a new bounded cache with `records` appended.
|
||||
fn append_output_records(
|
||||
mut cache: HashMap<String, Vec<OutputLine>>,
|
||||
records: Vec<SessionOutputRecord>,
|
||||
) -> HashMap<String, Vec<OutputLine>> {
|
||||
let mut touched_sessions = HashSet::new();
|
||||
for record in records {
|
||||
cache
|
||||
.entry(record.session_id.clone())
|
||||
.or_default()
|
||||
.push(record.line);
|
||||
touched_sessions.insert(record.session_id);
|
||||
}
|
||||
|
||||
for session_id in touched_sessions {
|
||||
if let Some(lines) = cache.get_mut(&session_id) {
|
||||
let overflow = lines.len().saturating_sub(OUTPUT_BUFFER_LIMIT);
|
||||
if overflow > 0 {
|
||||
lines.drain(..overflow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache
|
||||
}
|
||||
|
||||
pub struct Dashboard {
|
||||
db: StateStore,
|
||||
cfg: Config,
|
||||
output_store: SessionOutputStore,
|
||||
output_rx: broadcast::Receiver<OutputEvent>,
|
||||
notifier: DesktopNotifier,
|
||||
webhook_notifier: WebhookNotifier,
|
||||
sessions: Vec<Session>,
|
||||
session_harnesses: HashMap<String, SessionHarnessInfo>,
|
||||
session_output_cache: HashMap<String, Vec<OutputLine>>,
|
||||
session_output_generations: HashMap<String, chrono::DateTime<Utc>>,
|
||||
output_cursor: Option<i64>,
|
||||
unread_message_counts: HashMap<String, usize>,
|
||||
approval_queue_counts: HashMap<String, usize>,
|
||||
approval_queue_preview: Vec<SessionMessage>,
|
||||
@@ -502,15 +527,8 @@ fn load_session_harnesses(
|
||||
}
|
||||
|
||||
impl Dashboard {
|
||||
/// Builds the dashboard and hydrates its initial bounded output snapshot.
|
||||
pub fn new(db: StateStore, cfg: Config) -> Self {
|
||||
Self::with_output_store(db, cfg, SessionOutputStore::default())
|
||||
}
|
||||
|
||||
pub fn with_output_store(
|
||||
db: StateStore,
|
||||
cfg: Config,
|
||||
output_store: SessionOutputStore,
|
||||
) -> Self {
|
||||
let pane_size_percent = configured_pane_size(&cfg, cfg.pane_layout);
|
||||
let initial_cost_metrics_signature = metrics_file_signature(&cfg.cost_metrics_path());
|
||||
let initial_tool_activity_signature =
|
||||
@@ -528,12 +546,15 @@ impl Dashboard {
|
||||
.iter()
|
||||
.map(|session| (session.id.clone(), session.state.clone()))
|
||||
.collect();
|
||||
let session_output_generations = sessions
|
||||
.iter()
|
||||
.map(|session| (session.id.clone(), session.created_at))
|
||||
.collect();
|
||||
let initial_approval_message_id = db
|
||||
.latest_unread_approval_message()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|message| message.id);
|
||||
let output_rx = output_store.subscribe();
|
||||
let notifier = DesktopNotifier::new(cfg.desktop_notifications.clone());
|
||||
let webhook_notifier = WebhookNotifier::new(cfg.webhook_notifications.clone());
|
||||
let mut session_table_state = TableState::default();
|
||||
@@ -544,13 +565,13 @@ impl Dashboard {
|
||||
let mut dashboard = Self {
|
||||
db,
|
||||
cfg,
|
||||
output_store,
|
||||
output_rx,
|
||||
notifier,
|
||||
webhook_notifier,
|
||||
sessions,
|
||||
session_harnesses,
|
||||
session_output_cache: HashMap::new(),
|
||||
session_output_generations,
|
||||
output_cursor: None,
|
||||
unread_message_counts: HashMap::new(),
|
||||
approval_queue_counts: HashMap::new(),
|
||||
approval_queue_preview: Vec::new(),
|
||||
@@ -624,6 +645,7 @@ impl Dashboard {
|
||||
dashboard.sync_handoff_backlog_counts();
|
||||
dashboard.sync_board_meta();
|
||||
dashboard.sync_global_handoff_backlog();
|
||||
dashboard.sync_output_cache();
|
||||
dashboard.sync_selected_output();
|
||||
dashboard.sync_selected_diff();
|
||||
dashboard.sync_selected_messages();
|
||||
@@ -3211,6 +3233,7 @@ impl Dashboard {
|
||||
));
|
||||
}
|
||||
|
||||
/// Refreshes persisted dashboard state while preserving the output cursor.
|
||||
pub fn refresh(&mut self) {
|
||||
self.sync_from_store();
|
||||
}
|
||||
@@ -3993,15 +4016,6 @@ impl Dashboard {
|
||||
}
|
||||
|
||||
pub async fn tick(&mut self) {
|
||||
loop {
|
||||
match self.output_rx.try_recv() {
|
||||
Ok(_event) => {}
|
||||
Err(broadcast::error::TryRecvError::Empty) => break,
|
||||
Err(broadcast::error::TryRecvError::Lagged(_)) => continue,
|
||||
Err(broadcast::error::TryRecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(error) = manager::activate_pending_worktree_sessions(&self.db, &self.cfg).await {
|
||||
tracing::warn!("Failed to activate queued worktree sessions: {error}");
|
||||
}
|
||||
@@ -4073,18 +4087,22 @@ impl Dashboard {
|
||||
)
|
||||
}
|
||||
|
||||
/// Synchronizes dashboard state, deferring output recovery until sessions load.
|
||||
fn sync_from_store(&mut self) {
|
||||
let (heartbeat_enforcement, budget_enforcement, conflict_enforcement) =
|
||||
self.sync_runtime_metrics();
|
||||
let selected_id = self.selected_session_id().map(ToOwned::to_owned);
|
||||
self.sessions = match self.db.list_sessions() {
|
||||
let sessions_refreshed = match self.db.list_sessions() {
|
||||
Ok(mut sessions) => {
|
||||
sort_sessions_for_display(&mut sessions);
|
||||
sessions
|
||||
self.sessions = sessions;
|
||||
true
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("Failed to refresh sessions: {error}");
|
||||
Vec::new()
|
||||
self.output_cursor = None;
|
||||
self.sessions.clear();
|
||||
false
|
||||
}
|
||||
};
|
||||
self.session_harnesses = load_session_harnesses(&self.db, &self.cfg, &self.sessions);
|
||||
@@ -4103,7 +4121,9 @@ impl Dashboard {
|
||||
self.sync_approval_notifications();
|
||||
self.sync_global_handoff_backlog();
|
||||
self.sync_daemon_activity();
|
||||
self.sync_output_cache();
|
||||
if sessions_refreshed {
|
||||
self.sync_output_cache();
|
||||
}
|
||||
self.sync_selection_by_id(selected_id.as_deref());
|
||||
self.ensure_selected_pane_visible();
|
||||
self.sync_selected_output();
|
||||
@@ -4481,25 +4501,43 @@ impl Dashboard {
|
||||
}
|
||||
|
||||
fn sync_output_cache(&mut self) {
|
||||
let active_session_ids: HashSet<_> = self
|
||||
let active_session_generations: HashMap<_, _> = self
|
||||
.sessions
|
||||
.iter()
|
||||
.map(|session| session.id.as_str())
|
||||
.map(|session| (session.id.clone(), session.created_at))
|
||||
.collect();
|
||||
self.session_output_cache
|
||||
.retain(|session_id, _| active_session_ids.contains(session_id.as_str()));
|
||||
let cached_generations = &self.session_output_generations;
|
||||
self.session_output_cache = std::mem::take(&mut self.session_output_cache)
|
||||
.into_iter()
|
||||
.filter(|(session_id, _)| {
|
||||
active_session_generations.get(session_id) == cached_generations.get(session_id)
|
||||
})
|
||||
.collect();
|
||||
self.session_output_generations = active_session_generations;
|
||||
|
||||
for session in &self.sessions {
|
||||
match self.db.get_output_lines(&session.id, OUTPUT_BUFFER_LIMIT) {
|
||||
Ok(lines) => {
|
||||
self.output_store.replace_lines(&session.id, lines.clone());
|
||||
self.session_output_cache.insert(session.id.clone(), lines);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("Failed to load session output for {}: {error}", session.id);
|
||||
}
|
||||
let batch = match self.output_cursor {
|
||||
Some(cursor) => self
|
||||
.db
|
||||
.get_output_since(cursor, OUTPUT_DELTA_BATCH_LIMIT),
|
||||
None => self.db.get_output_snapshot(OUTPUT_BUFFER_LIMIT),
|
||||
};
|
||||
let batch = match batch {
|
||||
Ok(batch) => batch,
|
||||
Err(error) => {
|
||||
tracing::warn!("Failed to refresh session output cache: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if self.output_cursor.is_none() {
|
||||
self.session_output_cache = HashMap::new();
|
||||
}
|
||||
self.output_cursor = Some(batch.cursor);
|
||||
|
||||
self.session_output_cache = append_output_records(
|
||||
std::mem::take(&mut self.session_output_cache),
|
||||
batch.records,
|
||||
);
|
||||
}
|
||||
|
||||
fn ensure_selected_pane_visible(&mut self) {
|
||||
@@ -5212,6 +5250,7 @@ impl Dashboard {
|
||||
.map(|session| session.id.as_str())
|
||||
}
|
||||
|
||||
/// Returns the selected session's currently cached output window.
|
||||
fn selected_output_lines(&self) -> &[OutputLine] {
|
||||
self.selected_session_id()
|
||||
.and_then(|session_id| self.session_output_cache.get(session_id))
|
||||
@@ -13147,6 +13186,260 @@ diff --git a/src/lib.rs b/src/lib.rs
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_cache_appends_rows_written_by_another_process_without_rehydrating() -> Result<()> {
|
||||
let db_path =
|
||||
std::env::temp_dir().join(format!("ecc2-output-cursor-{}.db", Uuid::new_v4()));
|
||||
let db = StateStore::open(&db_path)?;
|
||||
let session = sample_session("session-1", "claude", SessionState::Running, None, 0, 0);
|
||||
db.insert_session(&session)?;
|
||||
db.append_output_line("session-1", OutputStream::Stdout, "persisted-before-open")?;
|
||||
|
||||
let mut dashboard = Dashboard::new(db, Config::default());
|
||||
assert!(dashboard
|
||||
.selected_output_text()
|
||||
.contains("persisted-before-open"));
|
||||
dashboard
|
||||
.session_output_cache
|
||||
.entry("session-1".to_string())
|
||||
.or_default()
|
||||
.push(test_output_line(OutputStream::Stdout, "cache-only"));
|
||||
|
||||
let child = Command::new(std::env::current_exe()?)
|
||||
.args([
|
||||
"--exact",
|
||||
"tui::dashboard::tests::output_cursor_child_writer",
|
||||
"--ignored",
|
||||
"--nocapture",
|
||||
])
|
||||
.env("ECC2_OUTPUT_CURSOR_CHILD_DB", &db_path)
|
||||
.status()?;
|
||||
assert!(child.success(), "child output writer should succeed");
|
||||
dashboard.refresh();
|
||||
|
||||
let text = dashboard.selected_output_text();
|
||||
assert!(text.contains("persisted-before-open"));
|
||||
assert!(text.contains("cache-only"));
|
||||
assert!(text.contains("persisted-after-open"));
|
||||
|
||||
dashboard.sync_output_cache();
|
||||
assert_eq!(
|
||||
dashboard
|
||||
.selected_output_lines()
|
||||
.iter()
|
||||
.filter(|line| line.text == "persisted-after-open")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(db_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "helper invoked by output cursor cross-process test"]
|
||||
fn output_cursor_child_writer() -> Result<()> {
|
||||
let Some(db_path) = std::env::var_os("ECC2_OUTPUT_CURSOR_CHILD_DB") else {
|
||||
return Ok(());
|
||||
};
|
||||
StateStore::open(Path::new(&db_path))?.append_output_line(
|
||||
"session-1",
|
||||
OutputStream::Stderr,
|
||||
"persisted-after-open",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_cache_rehydrates_after_transient_session_list_failure() -> Result<()> {
|
||||
let db_path =
|
||||
std::env::temp_dir().join(format!("ecc2-output-recovery-{}.db", Uuid::new_v4()));
|
||||
let db = StateStore::open(&db_path)?;
|
||||
let session = sample_session("session-1", "claude", SessionState::Running, None, 0, 0);
|
||||
db.insert_session(&session)?;
|
||||
db.append_output_line("session-1", OutputStream::Stdout, "persisted-output")?;
|
||||
|
||||
let mut dashboard = Dashboard::new(db, Config::default());
|
||||
assert!(dashboard
|
||||
.selected_output_text()
|
||||
.contains("persisted-output"));
|
||||
dashboard
|
||||
.session_output_cache
|
||||
.entry("session-1".to_string())
|
||||
.or_default()
|
||||
.push(test_output_line(OutputStream::Stdout, "cache-only"));
|
||||
|
||||
let schema = rusqlite::Connection::open(&db_path)?;
|
||||
schema.execute("ALTER TABLE sessions RENAME TO unavailable_sessions", [])?;
|
||||
dashboard.sync_from_store();
|
||||
assert!(dashboard.sessions.is_empty());
|
||||
assert!(dashboard.session_output_cache["session-1"]
|
||||
.iter()
|
||||
.any(|line| line.text == "cache-only"));
|
||||
assert!(dashboard.output_cursor.is_none());
|
||||
|
||||
dashboard.sync_from_store();
|
||||
assert!(dashboard.session_output_cache["session-1"]
|
||||
.iter()
|
||||
.any(|line| line.text == "cache-only"));
|
||||
assert!(dashboard.output_cursor.is_none());
|
||||
|
||||
schema.execute("ALTER TABLE unavailable_sessions RENAME TO sessions", [])?;
|
||||
dashboard.sync_from_store();
|
||||
|
||||
assert_eq!(dashboard.sessions.len(), 1);
|
||||
assert!(dashboard
|
||||
.selected_output_text()
|
||||
.contains("persisted-output"));
|
||||
assert!(!dashboard.selected_output_text().contains("cache-only"));
|
||||
|
||||
let _ = std::fs::remove_file(db_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_cache_tracks_session_add_delete_and_same_id_recreation() -> Result<()> {
|
||||
let db_path =
|
||||
std::env::temp_dir().join(format!("ecc2-output-lifecycle-{}.db", Uuid::new_v4()));
|
||||
let db = StateStore::open(&db_path)?;
|
||||
db.insert_session(&sample_session(
|
||||
"session-1",
|
||||
"claude",
|
||||
SessionState::Running,
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
))?;
|
||||
db.append_output_line("session-1", OutputStream::Stdout, "first-session")?;
|
||||
|
||||
let mut dashboard = Dashboard::new(db, Config::default());
|
||||
let external = StateStore::open(&db_path)?;
|
||||
external.insert_session(&sample_session(
|
||||
"session-2",
|
||||
"codex",
|
||||
SessionState::Running,
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
))?;
|
||||
external.append_output_line("session-2", OutputStream::Stderr, "new-session")?;
|
||||
dashboard.sync_from_store();
|
||||
|
||||
assert!(dashboard
|
||||
.sessions
|
||||
.iter()
|
||||
.any(|session| session.id == "session-2"));
|
||||
assert_eq!(
|
||||
dashboard.session_output_cache["session-2"][0].text,
|
||||
"new-session"
|
||||
);
|
||||
|
||||
external.delete_session("session-2")?;
|
||||
let replacement_time = Utc::now() + chrono::Duration::seconds(1);
|
||||
external.insert_session(&Session {
|
||||
created_at: replacement_time,
|
||||
updated_at: replacement_time,
|
||||
last_heartbeat_at: replacement_time,
|
||||
..sample_session("session-2", "codex", SessionState::Running, None, 0, 0)
|
||||
})?;
|
||||
external.append_output_line("session-2", OutputStream::Stdout, "replacement-session")?;
|
||||
dashboard.sync_from_store();
|
||||
|
||||
let replacement = &dashboard.session_output_cache["session-2"];
|
||||
assert_eq!(replacement.len(), 1);
|
||||
assert_eq!(replacement[0].text, "replacement-session");
|
||||
|
||||
let _ = std::fs::remove_file(db_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_cache_retries_delta_after_transient_output_query_failure() -> Result<()> {
|
||||
let db_path =
|
||||
std::env::temp_dir().join(format!("ecc2-output-query-retry-{}.db", Uuid::new_v4()));
|
||||
let db = StateStore::open(&db_path)?;
|
||||
db.insert_session(&sample_session(
|
||||
"session-1",
|
||||
"claude",
|
||||
SessionState::Running,
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
))?;
|
||||
db.append_output_line("session-1", OutputStream::Stdout, "persisted-before")?;
|
||||
|
||||
let mut dashboard = Dashboard::new(db, Config::default());
|
||||
dashboard
|
||||
.session_output_cache
|
||||
.get_mut("session-1")
|
||||
.expect("hydrated output")
|
||||
.push(test_output_line(OutputStream::Stdout, "cache-only"));
|
||||
let cursor = dashboard.output_cursor;
|
||||
|
||||
let schema = rusqlite::Connection::open(&db_path)?;
|
||||
schema.execute(
|
||||
"ALTER TABLE session_output RENAME TO unavailable_session_output",
|
||||
[],
|
||||
)?;
|
||||
dashboard.sync_output_cache();
|
||||
assert_eq!(dashboard.output_cursor, cursor);
|
||||
assert!(dashboard.session_output_cache["session-1"]
|
||||
.iter()
|
||||
.any(|line| line.text == "cache-only"));
|
||||
|
||||
schema.execute(
|
||||
"ALTER TABLE unavailable_session_output RENAME TO session_output",
|
||||
[],
|
||||
)?;
|
||||
StateStore::open(&db_path)?.append_output_line(
|
||||
"session-1",
|
||||
OutputStream::Stderr,
|
||||
"persisted-after",
|
||||
)?;
|
||||
dashboard.sync_output_cache();
|
||||
|
||||
let output = &dashboard.session_output_cache["session-1"];
|
||||
assert!(output.iter().any(|line| line.text == "cache-only"));
|
||||
assert_eq!(
|
||||
output
|
||||
.iter()
|
||||
.filter(|line| line.text == "persisted-after")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(db_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_output_records_bounds_each_session_to_the_latest_window() {
|
||||
let mut cache = HashMap::from([(
|
||||
"session-2".to_string(),
|
||||
vec![test_output_line(OutputStream::Stderr, "other-session")],
|
||||
)]);
|
||||
let records = (0..(OUTPUT_BUFFER_LIMIT + 5))
|
||||
.map(|index| crate::session::store::SessionOutputRecord {
|
||||
id: index as i64 + 1,
|
||||
session_id: "session-1".to_string(),
|
||||
line: test_output_line(OutputStream::Stdout, &format!("line-{index}")),
|
||||
})
|
||||
.collect();
|
||||
|
||||
cache = append_output_records(cache, records);
|
||||
|
||||
let session_lines = cache.get("session-1").expect("session output");
|
||||
assert_eq!(session_lines.len(), OUTPUT_BUFFER_LIMIT);
|
||||
assert_eq!(
|
||||
session_lines.first().map(|line| line.text.as_str()),
|
||||
Some("line-5")
|
||||
);
|
||||
assert_eq!(
|
||||
session_lines.last().map(|line| line.text.as_str()),
|
||||
Some(format!("line-{}", OUTPUT_BUFFER_LIMIT + 4).as_str())
|
||||
);
|
||||
assert_eq!(cache["session-2"][0].text, "other-session");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_search_tracks_matches_and_sets_navigation_note() {
|
||||
let mut dashboard = test_dashboard(
|
||||
@@ -14917,8 +15210,10 @@ diff --git a/src/lib.rs b/src/lib.rs
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let output_store = SessionOutputStore::default();
|
||||
let output_rx = output_store.subscribe();
|
||||
let session_output_generations = sessions
|
||||
.iter()
|
||||
.map(|session| (session.id.clone(), session.created_at))
|
||||
.collect();
|
||||
let mut session_table_state = TableState::default();
|
||||
if !sessions.is_empty() {
|
||||
session_table_state.select(Some(selected_session));
|
||||
@@ -14928,13 +15223,13 @@ diff --git a/src/lib.rs b/src/lib.rs
|
||||
db: StateStore::open(Path::new(":memory:")).expect("open test db"),
|
||||
pane_size_percent: configured_pane_size(&cfg, cfg.pane_layout),
|
||||
cfg,
|
||||
output_store,
|
||||
output_rx,
|
||||
notifier,
|
||||
webhook_notifier,
|
||||
sessions,
|
||||
session_harnesses,
|
||||
session_output_cache: HashMap::new(),
|
||||
session_output_generations,
|
||||
output_cursor: None,
|
||||
unread_message_counts: HashMap::new(),
|
||||
approval_queue_counts: HashMap::new(),
|
||||
approval_queue_preview: Vec::new(),
|
||||
|
||||
+25
-7
@@ -19,6 +19,10 @@ User request → Claude picks a tool → PreToolUse hook runs → Tool executes
|
||||
Memory persistence lifecycle definitions live in `hooks/memory-persistence/`.
|
||||
The executable hook graph remains `hooks/hooks.json`; the memory persistence directory is the stable contract for SessionStart, PreCompact, observation, activity tracking, and SessionEnd behavior.
|
||||
|
||||
Stable hook IDs and descriptions live in `hooks/hooks.metadata.json`, aligned by event and index with `hooks/hooks.json`. Claude Code validates a plugin's `hooks.json` against its own schema and reports any other key (`$schema`, `id`, `description`) as unknown at load time, so `hooks.json` carries only what the harness accepts. ECC's installer, validator, and dashboard merge the sidecar back in through `scripts/lib/hooks-config.js`; `node scripts/ci/validate-hooks.js` fails if the two files drift apart, and `node scripts/ci/check-hooks-schema-keys.js` fails if `hooks.json` or `hooks/codex-hooks.json` carry any key outside their loader's documented set.
|
||||
|
||||
Each sidecar entry also carries a `fingerprint` of the matcher entry it describes (matcher plus hook commands), so reordering `hooks.json` without reordering the sidecar, or editing a command without updating the sidecar, is caught rather than silently swapping IDs. When reordering hooks, move the matching sidecar entries first. Then run `node scripts/ci/validate-hooks.js --update-fingerprints` to refresh changed commands and commit both files. The updater rejects known fingerprints at different positions and writes only after validation succeeds.
|
||||
|
||||
## Installing These Hooks Manually
|
||||
|
||||
For Claude Code manual installs, do not paste the raw repo `hooks.json` into `~/.claude/settings.json` or copy it directly into `~/.claude/hooks/hooks.json`. The checked-in file is plugin/repo-oriented and is meant to be installed through the ECC installer or loaded as a plugin.
|
||||
@@ -110,6 +114,18 @@ export ECC_HOOK_PROFILE=standard
|
||||
# Disable specific hook IDs (comma-separated)
|
||||
export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:edit:typecheck"
|
||||
|
||||
# Lower the hook input cap in bytes (default and maximum: 1048576).
|
||||
# run-with-flags.js adds runner-level fail-closed handling for
|
||||
# pre:edit-write:gateguard-fact-force and pre:mcp-health-check because they
|
||||
# cannot inspect the complete request. Other safety hooks, including the Bash
|
||||
# dispatcher and config protection, retain their own fail-closed behavior.
|
||||
# If a trusted tool call legitimately exceeds the cap, retry with a smaller
|
||||
# input or temporarily set ECC_GATEGUARD=off (or GATEGUARD_DISABLED=1) for
|
||||
# GateGuard, or ECC_MCP_HEALTH_FAIL_OPEN=yes for MCP health, then restore it.
|
||||
# These switches reduce only the named protection while enabled; they do not
|
||||
# bypass the Bash dispatcher or config-protection checks.
|
||||
export ECC_HOOK_INPUT_MAX_BYTES=524288
|
||||
|
||||
# Disable only GateGuard during setup or recovery
|
||||
export ECC_GATEGUARD=off
|
||||
|
||||
@@ -143,7 +159,10 @@ update the plugin and change those preferences.
|
||||
|
||||
### Writing Your Own Hook
|
||||
|
||||
Hooks are shell commands that receive tool input as JSON on stdin and must output JSON on stdout.
|
||||
Hooks are shell commands that receive tool input as JSON on stdin. A hook with
|
||||
no decision or context to return should leave stdout empty. Only explicit hook
|
||||
output, such as a deny decision or `additionalContext`, should be written to
|
||||
stdout; the input payload must not be echoed as a no-op response.
|
||||
|
||||
**Basic structure:**
|
||||
|
||||
@@ -165,8 +184,7 @@ process.stdin.on('end', () => {
|
||||
// Block (PreToolUse only): exit with code 2
|
||||
// process.exit(2);
|
||||
|
||||
// Always output the original data to stdout
|
||||
console.log(data);
|
||||
// No opinion: leave stdout empty.
|
||||
});
|
||||
```
|
||||
|
||||
@@ -217,7 +235,7 @@ Async hooks run in the background. They cannot block tool execution.
|
||||
"matcher": "Edit",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const ns=i.tool_input?.new_string||'';if(/TODO|FIXME|HACK/.test(ns)){console.error('[Hook] New TODO/FIXME added - consider creating an issue')}console.log(d)})\""
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const ns=i.tool_input?.new_string||'';if(/TODO|FIXME|HACK/.test(ns)){console.error('[Hook] New TODO/FIXME added - consider creating an issue')}})\""
|
||||
}],
|
||||
"description": "Warn when adding TODO/FIXME comments"
|
||||
}
|
||||
@@ -230,7 +248,7 @@ Async hooks run in the background. They cannot block tool execution.
|
||||
"matcher": "Write",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const c=i.tool_input?.content||'';const lines=c.split('\\n').length;if(lines>800){console.error('[Hook] BLOCKED: File exceeds 800 lines ('+lines+' lines)');console.error('[Hook] Split into smaller, focused modules');process.exit(2)}console.log(d)})\""
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const c=i.tool_input?.content||'';const lines=c.split('\\n').length;if(lines>800){console.error('[Hook] BLOCKED: File exceeds 800 lines ('+lines+' lines)');console.error('[Hook] Split into smaller, focused modules');process.exit(2)}})\""
|
||||
}],
|
||||
"description": "Block creation of files larger than 800 lines"
|
||||
}
|
||||
@@ -243,7 +261,7 @@ Async hooks run in the background. They cannot block tool execution.
|
||||
"matcher": "Edit",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/\\.py$/.test(p)){const{execFileSync}=require('child_process');try{execFileSync('ruff',['format',p],{stdio:'pipe'})}catch(e){}}console.log(d)})\""
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/\\.py$/.test(p)){const{execFileSync}=require('child_process');try{execFileSync('ruff',['format',p],{stdio:'pipe'})}catch(e){}}})\""
|
||||
}],
|
||||
"description": "Auto-format Python files with ruff after edits"
|
||||
}
|
||||
@@ -256,7 +274,7 @@ Async hooks run in the background. They cannot block tool execution.
|
||||
"matcher": "Write",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/src\\/.*\\.(ts|js)$/.test(p)&&!/\\.test\\.|\\.spec\\./.test(p)){const testPath=p.replace(/\\.(ts|js)$/,'.test.$1');if(!fs.existsSync(testPath)){console.error('[Hook] No test file found for: '+p);console.error('[Hook] Expected: '+testPath);console.error('[Hook] Consider writing tests first (/tdd)')}}console.log(d)})\""
|
||||
"command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/src\\/.*\\.(ts|js)$/.test(p)&&!/\\.test\\.|\\.spec\\./.test(p)){const testPath=p.replace(/\\.(ts|js)$/,'.test.$1');if(!fs.existsSync(testPath)){console.error('[Hook] No test file found for: '+p);console.error('[Hook] Expected: '+testPath);console.error('[Hook] Consider writing tests first (/tdd)')}}})\""
|
||||
}],
|
||||
"description": "Remind to create tests when adding new source files"
|
||||
}
|
||||
|
||||
+34
-83
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
@@ -9,9 +8,7 @@
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/pre-bash-dispatcher.js"
|
||||
}
|
||||
],
|
||||
"description": "Consolidated Bash preflight dispatcher for quality, tmux, push, and GateGuard checks",
|
||||
"id": "pre:bash:dispatcher"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "PowerShell",
|
||||
@@ -21,9 +18,7 @@
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:powershell:gateguard-fact-force scripts/hooks/gateguard-fact-force.js standard,strict",
|
||||
"timeout": 5
|
||||
}
|
||||
],
|
||||
"description": "PowerShell fact-forcing gate: inspect destructive commands without running unrelated Bash-only preflight hooks",
|
||||
"id": "pre:powershell:gateguard-fact-force"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Write",
|
||||
@@ -32,9 +27,7 @@
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:write:doc-file-warning scripts/hooks/doc-file-warning.js standard,strict"
|
||||
}
|
||||
],
|
||||
"description": "Doc file warning: warn about non-standard documentation files (exit code 0; warns only)",
|
||||
"id": "pre:write:doc-file-warning"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit|Write",
|
||||
@@ -43,9 +36,7 @@
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:edit-write:suggest-compact scripts/hooks/suggest-compact.js standard,strict"
|
||||
}
|
||||
],
|
||||
"description": "Suggest manual compaction at logical intervals",
|
||||
"id": "pre:edit-write:suggest-compact"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": ".*",
|
||||
@@ -56,9 +47,7 @@
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
],
|
||||
"description": "Capture tool use observations for continuous learning",
|
||||
"id": "pre:observe:continuous-learning"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Bash|PowerShell|Write|Edit|MultiEdit",
|
||||
@@ -68,9 +57,7 @@
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node 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",
|
||||
"id": "pre:governance-capture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Write|Edit|MultiEdit",
|
||||
@@ -80,9 +67,7 @@
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:config-protection scripts/hooks/config-protection.js standard,strict",
|
||||
"timeout": 5
|
||||
}
|
||||
],
|
||||
"description": "Block modifications to linter/formatter config files. Steers agent to fix code instead of weakening configs.",
|
||||
"id": "pre:config-protection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": ".*",
|
||||
@@ -91,9 +76,7 @@
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:mcp-health-check scripts/hooks/mcp-health-check.js standard,strict"
|
||||
}
|
||||
],
|
||||
"description": "Check MCP server health before MCP tool execution and block unhealthy MCP calls",
|
||||
"id": "pre:mcp-health-check"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
@@ -103,9 +86,7 @@
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:edit-write:gateguard-fact-force scripts/hooks/gateguard-fact-force.js standard,strict",
|
||||
"timeout": 5
|
||||
}
|
||||
],
|
||||
"description": "Fact-forcing gate: block first Edit/Write/MultiEdit per file and demand investigation (importers, data schemas, user instruction) before allowing",
|
||||
"id": "pre:edit-write:gateguard-fact-force"
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
@@ -116,9 +97,7 @@
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:compact scripts/hooks/pre-compact.js standard,strict"
|
||||
}
|
||||
],
|
||||
"description": "Save state before context compaction",
|
||||
"id": "pre:compact"
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionStart": [
|
||||
@@ -129,9 +108,7 @@
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/session-start-bootstrap.js"
|
||||
}
|
||||
],
|
||||
"description": "Load previous context and detect package manager on new session",
|
||||
"id": "session:start"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": ".*",
|
||||
@@ -140,9 +117,7 @@
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js session-start:plan-canvas-sessions scripts/hooks/plan-canvas-sessions.js standard,strict"
|
||||
}
|
||||
],
|
||||
"description": "Surface open Plan Canvas review sessions so a fresh session can resume the loop",
|
||||
"id": "session-start:plan-canvas-sessions"
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
@@ -151,25 +126,21 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/posttooluse-dispatcher.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.env.ECC_POSTTOOLUSE_PASSTHROUGH='1';process.argv.splice(1,0,s);require(s).cli()\" sync",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/posttooluse-dispatcher.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s).cli()\" sync",
|
||||
"timeout": 30
|
||||
}
|
||||
],
|
||||
"description": "Run synchronous PostToolUse hooks in one process while preserving per-hook controls",
|
||||
"id": "post:dispatcher:sync"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": ".*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/posttooluse-dispatcher.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.env.ECC_POSTTOOLUSE_PASSTHROUGH='1';process.argv.splice(1,0,s);require(s).cli()\" async",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/posttooluse-dispatcher.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s).cli()\" async",
|
||||
"async": true,
|
||||
"timeout": 45
|
||||
}
|
||||
],
|
||||
"description": "Run background PostToolUse hooks in one process while preserving per-hook controls",
|
||||
"id": "post:dispatcher:async"
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUseFailure": [
|
||||
@@ -180,9 +151,7 @@
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:mcp-health-check scripts/hooks/mcp-health-check.js standard,strict"
|
||||
}
|
||||
],
|
||||
"description": "Track failed MCP tool calls, mark unhealthy servers, and attempt reconnect",
|
||||
"id": "post:mcp-health-check"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Skill",
|
||||
@@ -191,9 +160,7 @@
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:skill:track scripts/hooks/skill-run-tracker.js standard,strict"
|
||||
}
|
||||
],
|
||||
"description": "Record hard Skill tool failures for skill-health telemetry",
|
||||
"id": "post:skill:track"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
@@ -202,86 +169,72 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:plan-canvas-pending','scripts/hooks/plan-canvas-pending.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\""
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const n=process.platform==='win32'&&r.charAt(0)==='/'&&/[a-zA-Z]/.test(r.charAt(1))&&(r.length===2||r.charAt(2)==='/')?r.charAt(1).toUpperCase()+':/'+r.slice(3):r;const s=p.join(n,'scripts/hooks/lifecycle-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=n;if(require('fs').existsSync(s)){process.argv.splice(1,0,s);require(s).cli()}else{process.stderr.write('[Hook] lifecycle bootstrap unavailable; skipping hook'+String.fromCharCode(10))}\" stop:plan-canvas-pending scripts/hooks/plan-canvas-pending.js minimal,standard,strict 30000"
|
||||
}
|
||||
],
|
||||
"description": "Deliver undelivered Plan Canvas browser feedback before the agent stops",
|
||||
"id": "stop:plan-canvas-pending"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": ".*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:format-typecheck','scripts/hooks/stop-format-typecheck.js','standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:300000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const n=process.platform==='win32'&&r.charAt(0)==='/'&&/[a-zA-Z]/.test(r.charAt(1))&&(r.length===2||r.charAt(2)==='/')?r.charAt(1).toUpperCase()+':/'+r.slice(3):r;const s=p.join(n,'scripts/hooks/lifecycle-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=n;if(require('fs').existsSync(s)){process.argv.splice(1,0,s);require(s).cli()}else{process.stderr.write('[Hook] lifecycle bootstrap unavailable; skipping hook'+String.fromCharCode(10))}\" stop:format-typecheck scripts/hooks/stop-format-typecheck.js standard,strict 300000",
|
||||
"timeout": 300
|
||||
}
|
||||
],
|
||||
"description": "Batch format (Biome/Prettier) and typecheck (tsc) all JS/TS files edited this response — runs once at Stop instead of after every Edit",
|
||||
"id": "stop:format-typecheck"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": ".*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:check-console-log','scripts/hooks/check-console-log.js','standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\""
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const n=process.platform==='win32'&&r.charAt(0)==='/'&&/[a-zA-Z]/.test(r.charAt(1))&&(r.length===2||r.charAt(2)==='/')?r.charAt(1).toUpperCase()+':/'+r.slice(3):r;const s=p.join(n,'scripts/hooks/lifecycle-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=n;if(require('fs').existsSync(s)){process.argv.splice(1,0,s);require(s).cli()}else{process.stderr.write('[Hook] lifecycle bootstrap unavailable; skipping hook'+String.fromCharCode(10))}\" stop:check-console-log scripts/hooks/check-console-log.js standard,strict 30000"
|
||||
}
|
||||
],
|
||||
"description": "Check for console.log in modified files after each response",
|
||||
"id": "stop:check-console-log"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": ".*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:session-end','scripts/hooks/session-end.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const n=process.platform==='win32'&&r.charAt(0)==='/'&&/[a-zA-Z]/.test(r.charAt(1))&&(r.length===2||r.charAt(2)==='/')?r.charAt(1).toUpperCase()+':/'+r.slice(3):r;const s=p.join(n,'scripts/hooks/lifecycle-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=n;if(require('fs').existsSync(s)){process.argv.splice(1,0,s);require(s).cli()}else{process.stderr.write('[Hook] lifecycle bootstrap unavailable; skipping hook'+String.fromCharCode(10))}\" stop:session-end scripts/hooks/session-end.js minimal,standard,strict 30000",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
],
|
||||
"description": "Persist session state after each response (Stop carries transcript_path)",
|
||||
"id": "stop:session-end"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": ".*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:evaluate-session','scripts/hooks/evaluate-session.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const n=process.platform==='win32'&&r.charAt(0)==='/'&&/[a-zA-Z]/.test(r.charAt(1))&&(r.length===2||r.charAt(2)==='/')?r.charAt(1).toUpperCase()+':/'+r.slice(3):r;const s=p.join(n,'scripts/hooks/lifecycle-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=n;if(require('fs').existsSync(s)){process.argv.splice(1,0,s);require(s).cli()}else{process.stderr.write('[Hook] lifecycle bootstrap unavailable; skipping hook'+String.fromCharCode(10))}\" stop:evaluate-session scripts/hooks/evaluate-session.js minimal,standard,strict 30000",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
],
|
||||
"description": "Evaluate session for extractable patterns",
|
||||
"id": "stop:evaluate-session"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": ".*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:cost-tracker','scripts/hooks/cost-tracker.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const n=process.platform==='win32'&&r.charAt(0)==='/'&&/[a-zA-Z]/.test(r.charAt(1))&&(r.length===2||r.charAt(2)==='/')?r.charAt(1).toUpperCase()+':/'+r.slice(3):r;const s=p.join(n,'scripts/hooks/lifecycle-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=n;if(require('fs').existsSync(s)){process.argv.splice(1,0,s);require(s).cli()}else{process.stderr.write('[Hook] lifecycle bootstrap unavailable; skipping hook'+String.fromCharCode(10))}\" stop:cost-tracker scripts/hooks/cost-tracker.js minimal,standard,strict 30000",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
],
|
||||
"description": "Track token and cost metrics per session",
|
||||
"id": "stop:cost-tracker"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": ".*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:desktop-notify','scripts/hooks/desktop-notify.js','standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const n=process.platform==='win32'&&r.charAt(0)==='/'&&/[a-zA-Z]/.test(r.charAt(1))&&(r.length===2||r.charAt(2)==='/')?r.charAt(1).toUpperCase()+':/'+r.slice(3):r;const s=p.join(n,'scripts/hooks/lifecycle-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=n;if(require('fs').existsSync(s)){process.argv.splice(1,0,s);require(s).cli()}else{process.stderr.write('[Hook] lifecycle bootstrap unavailable; skipping hook'+String.fromCharCode(10))}\" stop:desktop-notify scripts/hooks/desktop-notify.js standard,strict 30000",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
],
|
||||
"description": "Send desktop notification (macOS/WSL) with task summary when Claude responds",
|
||||
"id": "stop:desktop-notify"
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
@@ -290,13 +243,11 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'session:end:marker','scripts/hooks/session-end-marker.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});const stdout=typeof result.stdout==='string'?result.stdout:'';if(stdout)process.stdout.write(stdout);else process.stdout.write(raw);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[SessionEnd] ERROR: hook runner failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);}process.stderr.write('[SessionEnd] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);\"",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const n=process.platform==='win32'&&r.charAt(0)==='/'&&/[a-zA-Z]/.test(r.charAt(1))&&(r.length===2||r.charAt(2)==='/')?r.charAt(1).toUpperCase()+':/'+r.slice(3):r;const s=p.join(n,'scripts/hooks/lifecycle-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=n;if(require('fs').existsSync(s)){process.argv.splice(1,0,s);require(s).cli()}else{process.stderr.write('[Hook] lifecycle bootstrap unavailable; skipping hook'+String.fromCharCode(10))}\" session:end:marker scripts/hooks/session-end-marker.js minimal,standard,strict 30000",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
],
|
||||
"description": "Session end lifecycle marker (non-blocking)",
|
||||
"id": "session:end:marker"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user