diff --git a/AGENTS.md b/AGENTS.md index c77188aa..0a6e0482 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — Agent Instructions -This is a **production-ready AI coding plugin** providing 48 specialized agents, 182 skills, 68 commands, and automated hook workflows for software development. +This is a **production-ready AI coding plugin** providing 48 specialized agents, 185 skills, 68 commands, and automated hook workflows for software development. **Version:** 2.0.0-rc.1 @@ -146,7 +146,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat ``` agents/ — 48 specialized subagents -skills/ — 182 workflow skills and domain knowledge +skills/ — 185 workflow skills and domain knowledge commands/ — 68 slash commands hooks/ — Trigger-based automations rules/ — Always-follow guidelines (common + per-language) diff --git a/README.md b/README.md index eac2726e..e5930f4f 100644 --- a/README.md +++ b/README.md @@ -350,7 +350,7 @@ If you stacked methods, clean up in this order: /plugin list everything-claude-code@everything-claude-code ``` -**That's it!** You now have access to 48 agents, 182 skills, and 68 legacy command shims. +**That's it!** You now have access to 48 agents, 185 skills, and 68 legacy command shims. ### Dashboard GUI @@ -1338,7 +1338,7 @@ The configuration is automatically detected from `.opencode/opencode.json`. |---------|-------------|----------|--------| | Agents | PASS: 48 agents | PASS: 12 agents | **Claude Code leads** | | Commands | PASS: 68 commands | PASS: 31 commands | **Claude Code leads** | -| Skills | PASS: 182 skills | PASS: 37 skills | **Claude Code leads** | +| Skills | PASS: 185 skills | PASS: 37 skills | **Claude Code leads** | | Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** | | Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** | | MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** | @@ -1443,7 +1443,7 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e |---------|------------|------------|-----------|----------| | **Agents** | 48 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | | **Commands** | 68 | Shared | Instruction-based | 31 | -| **Skills** | 182 | Shared | 10 (native format) | 37 | +| **Skills** | 185 | Shared | 10 (native format) | 37 | | **Hook Events** | 8 types | 15 types | None yet | 11 types | | **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | | **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions | diff --git a/README.zh-CN.md b/README.zh-CN.md index 9e1e9690..7e329d05 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list everything-claude-code@everything-claude-code ``` -**完成!** 你现在可以使用 48 个代理、182 个技能和 68 个命令。 +**完成!** 你现在可以使用 48 个代理、185 个技能和 68 个命令。 ### multi-* 命令需要额外配置 diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md index e9f7fec5..04055406 100644 --- a/docs/zh-CN/AGENTS.md +++ b/docs/zh-CN/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — 智能体指令 -这是一个**生产就绪的 AI 编码插件**,提供 48 个专业代理、182 项技能、68 条命令以及自动化钩子工作流,用于软件开发。 +这是一个**生产就绪的 AI 编码插件**,提供 48 个专业代理、185 项技能、68 条命令以及自动化钩子工作流,用于软件开发。 **版本:** 2.0.0-rc.1 @@ -147,7 +147,7 @@ ``` agents/ — 48 个专业子代理 -skills/ — 182 个工作流技能和领域知识 +skills/ — 185 个工作流技能和领域知识 commands/ — 68 个斜杠命令 hooks/ — 基于触发的自动化 rules/ — 始终遵循的指导方针(通用 + 每种语言) diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index a83f0031..a32f549b 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list everything-claude-code@everything-claude-code ``` -**搞定!** 你现在可以使用 48 个智能体、182 项技能和 68 个命令了。 +**搞定!** 你现在可以使用 48 个智能体、185 项技能和 68 个命令了。 *** @@ -1134,7 +1134,7 @@ opencode |---------|-------------|----------|--------| | 智能体 | PASS: 48 个 | PASS: 12 个 | **Claude Code 领先** | | 命令 | PASS: 68 个 | PASS: 31 个 | **Claude Code 领先** | -| 技能 | PASS: 182 项 | PASS: 37 项 | **Claude Code 领先** | +| 技能 | PASS: 185 项 | PASS: 37 项 | **Claude Code 领先** | | 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** | | 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** | | MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** | @@ -1242,7 +1242,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以 |---------|------------|------------|-----------|----------| | **智能体** | 48 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | | **命令** | 68 | 共享 | 基于指令 | 31 | -| **技能** | 182 | 共享 | 10 (原生格式) | 37 | +| **技能** | 185 | 共享 | 10 (原生格式) | 37 | | **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 | | **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 | | **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 | diff --git a/scripts/hooks/gateguard-fact-force.js b/scripts/hooks/gateguard-fact-force.js index d83b557e..eb0356aa 100644 --- a/scripts/hooks/gateguard-fact-force.js +++ b/scripts/hooks/gateguard-fact-force.js @@ -376,6 +376,21 @@ function withRecoveryHint(message, hookIds = [EDIT_WRITE_HOOK_ID]) { ].join('\n'); } +function isSubagentInvocation(data) { + if (!data || typeof data !== 'object') { + return false; + } + + const candidates = [ + data.agent_id, + data.agentId, + data.parent_tool_use_id, + data.parentToolUseId + ]; + + return candidates.some(candidate => typeof candidate === 'string' && candidate.trim()); +} + // --- Deny helper --- function denyResult(reason, options = {}) { @@ -422,6 +437,7 @@ function run(rawInput) { // Normalize: case-insensitive matching via lookup map const TOOL_MAP = { edit: 'Edit', write: 'Write', multiedit: 'MultiEdit', bash: 'Bash' }; const toolName = TOOL_MAP[rawToolName.toLowerCase()] || rawToolName; + const inSubagent = isSubagentInvocation(data); if (toolName === 'Edit' || toolName === 'Write') { const filePath = toolInput.file_path || ''; @@ -429,6 +445,10 @@ function run(rawInput) { return rawInput; // allow } + if (inSubagent) { + return rawInput; // parent session already passed the first-touch file gate + } + if (!isChecked(filePath)) { if (!markChecked(filePath)) { return allowWithStateWarning(); @@ -440,6 +460,10 @@ function run(rawInput) { } if (toolName === 'MultiEdit') { + if (inSubagent) { + return rawInput; // parent session already passed the first-touch file gate + } + const edits = toolInput.edits || []; for (const edit of edits) { const filePath = edit.file_path || ''; diff --git a/skills/flox-environments/SKILL.md b/skills/flox-environments/SKILL.md index 38fee516..6441de52 100644 --- a/skills/flox-environments/SKILL.md +++ b/skills/flox-environments/SKILL.md @@ -32,7 +32,6 @@ Flox environments are defined in `.flox/env/manifest.toml` and activated with `f - `$FLOX_ENV_CACHE` — Persistent local storage for caches, venvs, data (survives rebuilds) - `$FLOX_ENV_PROJECT` — Project root directory (where `.flox/` lives) - ## Essential Commands ```bash diff --git a/skills/ios-icon-gen/SKILL.md b/skills/ios-icon-gen/SKILL.md index a8762551..1f06f842 100644 --- a/skills/ios-icon-gen/SKILL.md +++ b/skills/ios-icon-gen/SKILL.md @@ -131,7 +131,7 @@ Options: `--size ` (default: 68), `--color ` (default: 8E8E93), `--weig | `carbon` | Carbon | 2400+ | IBM design language | | `heroicons` | HeroIcons | 1200+ | Tailwind CSS companion | -Browse all: https://icon-sets.iconify.design/ +Browse all: ## Scripts Reference diff --git a/skills/tinystruct-patterns/SKILL.md b/skills/tinystruct-patterns/SKILL.md index ea382972..e2d474af 100644 --- a/skills/tinystruct-patterns/SKILL.md +++ b/skills/tinystruct-patterns/SKILL.md @@ -20,7 +20,7 @@ Architecture and implementation patterns for building modules with the **tinystr ## How It Works -The tinystruct framework treats any method annotated with `@Action` as a routable endpoint for both terminal and web environments. Applications are created by extending `AbstractApplication`, which provides core lifecycle hooks like `init()` and access to the request `Context`. +The tinystruct framework treats any method annotated with `@Action` as a routable endpoint for both terminal and web environments. Applications are created by extending `AbstractApplication`, which provides core lifecycle hooks like `init()` and access to the request `Context`. Routing is handled by the `ActionRegistry`, which automatically maps path segments to method arguments and injects dependencies. For data-only services, the native `Builder` component should be used for JSON serialization to maintain a zero-dependency footprint. The framework also includes a utility in `ApplicationManager` to bootstrap the project's execution environment by generating the `bin/dispatcher` script. diff --git a/skills/tinystruct-patterns/references/testing.md b/skills/tinystruct-patterns/references/testing.md index ea4d979f..10152665 100644 --- a/skills/tinystruct-patterns/references/testing.md +++ b/skills/tinystruct-patterns/references/testing.md @@ -6,7 +6,7 @@ Use the testing patterns described here when writing units tests for your tinyst ## How It Works -Testing tinystruct applications requires a specific setup to ensure framework-level features like annotation processing and configuration management are active. By creating a new instance of your application and passing it a `Settings` object in the `setUp()` method, you trigger the `init()` lifecycle. This ensures all `@Action` methods are discovered and registered. +Testing tinystruct applications requires a specific setup to ensure framework-level features like annotation processing and configuration management are active. By creating a new instance of your application and passing it a `Settings` object in the `setUp()` method, you trigger the `init()` lifecycle. This ensures all `@Action` methods are discovered and registered. Because the `ActionRegistry` is a singleton, it is critical to maintain isolation between tests by properly initializing your application state before each test execution, preventing side effects from leaking across the test suite. diff --git a/tests/hooks/gateguard-fact-force.test.js b/tests/hooks/gateguard-fact-force.test.js index 98f5ed32..0443d54d 100644 --- a/tests/hooks/gateguard-fact-force.test.js +++ b/tests/hooks/gateguard-fact-force.test.js @@ -981,6 +981,168 @@ function runTests() { assert.ok(fs.existsSync(freshState), 'fresh state file should remain'); })) passed++; else failed++; + function runFreshSessionEdit(filePath, extra = {}) { + return runHook({ + tool_name: 'Edit', + tool_input: { file_path: filePath, old_string: 'a', new_string: 'b' }, + session_id: 'subagent-fresh-session', + ...extra + }, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' }); + } + + function runFreshSessionBash(command, extra = {}) { + return runBashHook({ + tool_name: 'Bash', + tool_input: { command }, + session_id: 'subagent-fresh-session', + ...extra + }, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' }); + } + + // --- Test 30: top-level Edit denies; subagent Edit allows --- + clearState(); + if (test('A/B: same Edit denies at top level and allows with agent_id', () => { + const topLevel = runFreshSessionEdit('/src/subagent-edit.js'); + const topOut = parseOutput(topLevel.stdout); + assert.ok(topOut, 'top-level edit should produce JSON output'); + assert.strictEqual(topOut.hookSpecificOutput.permissionDecision, 'deny'); + + clearState(); + const subagent = runFreshSessionEdit('/src/subagent-edit.js', { agent_id: 'agent-abc-123' }); + const subOut = parseOutput(subagent.stdout); + assert.ok(subOut, 'subagent edit should produce JSON output'); + assert.ok(!subOut.hookSpecificOutput || subOut.hookSpecificOutput.permissionDecision !== 'deny', + 'subagent edit should bypass the first-touch file gate'); + })) passed++; else failed++; + + // --- Test 31: top-level Write denies; subagent Write allows --- + clearState(); + if (test('A/B: same Write denies at top level and allows with agent_id', () => { + const topLevel = runHook({ + tool_name: 'Write', + tool_input: { file_path: '/src/subagent-write.js', content: 'module.exports = {};' }, + session_id: 'subagent-fresh-session' + }, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' }); + const topOut = parseOutput(topLevel.stdout); + assert.ok(topOut, 'top-level write should produce JSON output'); + assert.strictEqual(topOut.hookSpecificOutput.permissionDecision, 'deny'); + + clearState(); + const subagent = runHook({ + tool_name: 'Write', + tool_input: { file_path: '/src/subagent-write.js', content: 'module.exports = {};' }, + session_id: 'subagent-fresh-session', + agent_id: 'agent-abc-123' + }, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' }); + const subOut = parseOutput(subagent.stdout); + assert.ok(subOut, 'subagent write should produce JSON output'); + assert.ok(!subOut.hookSpecificOutput || subOut.hookSpecificOutput.permissionDecision !== 'deny', + 'subagent write should bypass the first-touch file gate'); + })) passed++; else failed++; + + // --- Test 32: top-level MultiEdit denies; subagent MultiEdit allows --- + clearState(); + if (test('A/B: same MultiEdit denies at top level and allows with agent_id', () => { + const edits = [ + { file_path: '/src/subagent-multi-a.js', old_string: 'a', new_string: 'b' }, + { file_path: '/src/subagent-multi-b.js', old_string: 'c', new_string: 'd' } + ]; + + const topLevel = runHook({ + tool_name: 'MultiEdit', + tool_input: { edits }, + session_id: 'subagent-fresh-session' + }, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' }); + const topOut = parseOutput(topLevel.stdout); + assert.ok(topOut, 'top-level MultiEdit should produce JSON output'); + assert.strictEqual(topOut.hookSpecificOutput.permissionDecision, 'deny'); + + clearState(); + const subagent = runHook({ + tool_name: 'MultiEdit', + tool_input: { edits }, + session_id: 'subagent-fresh-session', + agent_id: 'agent-abc-123' + }, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' }); + const subOut = parseOutput(subagent.stdout); + assert.ok(subOut, 'subagent MultiEdit should produce JSON output'); + assert.ok(!subOut.hookSpecificOutput || subOut.hookSpecificOutput.permissionDecision !== 'deny', + 'subagent MultiEdit should bypass the first-touch file gate'); + })) passed++; else failed++; + + // --- Test 33: Bash stays gated inside subagents --- + clearState(); + if (test('routine Bash remains gated in subagent context', () => { + const result = runFreshSessionBash('pwd', { agent_id: 'agent-abc-123' }); + const output = parseOutput(result.stdout); + assert.ok(output, 'subagent Bash should produce JSON output'); + assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny'); + assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('current user request')); + })) passed++; else failed++; + + // --- Test 34: destructive Bash stays gated inside subagents --- + clearState(); + if (test('destructive Bash remains gated in subagent context', () => { + const result = runFreshSessionBash('rm -rf /tmp/demo-path', { agent_id: 'agent-abc-123' }); + const output = parseOutput(result.stdout); + assert.ok(output, 'subagent destructive Bash should produce JSON output'); + assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny'); + assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('Destructive command detected')); + })) passed++; else failed++; + + // --- Test 35: parent tool IDs also mark subagent context --- + clearState(); + if (test('parent_tool_use_id and parentToolUseId mark subagent file edits', () => { + const snake = runFreshSessionEdit('/src/subagent-parent-snake.js', { parent_tool_use_id: 'toolu_parent_01' }); + const snakeOut = parseOutput(snake.stdout); + assert.ok(snakeOut, 'snake-case parent marker should produce JSON output'); + assert.ok(!snakeOut.hookSpecificOutput || snakeOut.hookSpecificOutput.permissionDecision !== 'deny', + 'parent_tool_use_id should bypass the first-touch file gate'); + + clearState(); + const camel = runFreshSessionEdit('/src/subagent-parent-camel.js', { parentToolUseId: 'toolu_parent_02' }); + const camelOut = parseOutput(camel.stdout); + assert.ok(camelOut, 'camel-case parent marker should produce JSON output'); + assert.ok(!camelOut.hookSpecificOutput || camelOut.hookSpecificOutput.permissionDecision !== 'deny', + 'parentToolUseId should bypass the first-touch file gate'); + })) passed++; else failed++; + + // --- Test 36: only non-empty string markers count --- + clearState(); + if (test('empty and non-string subagent markers do not bypass file gates', () => { + const cases = [ + ['empty', { agent_id: '' }], + ['whitespace', { agent_id: ' ' }], + ['numeric', { agent_id: 12345 }], + ['null', { agent_id: null }] + ]; + + for (const [name, extra] of cases) { + clearState(); + const result = runFreshSessionEdit(`/src/subagent-marker-${name}.js`, extra); + const output = parseOutput(result.stdout); + assert.ok(output, `${name} marker should produce JSON output`); + assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny', + `${name} marker should not bypass the first-touch file gate`); + } + })) passed++; else failed++; + + // --- Test 37: two sequential subagent Edits on different files pass --- + clearState(); + if (test('two sequential subagent Edits on different files both pass', () => { + const first = runFreshSessionEdit('/src/subagent-seq-a.js', { agent_id: 'agent-seq' }); + const firstOut = parseOutput(first.stdout); + assert.ok(firstOut, 'first subagent edit should produce JSON output'); + assert.ok(!firstOut.hookSpecificOutput || firstOut.hookSpecificOutput.permissionDecision !== 'deny', + 'first subagent edit should pass'); + + const second = runFreshSessionEdit('/src/subagent-seq-b.js', { agent_id: 'agent-seq' }); + const secondOut = parseOutput(second.stdout); + assert.ok(secondOut, 'second subagent edit should produce JSON output'); + assert.ok(!secondOut.hookSpecificOutput || secondOut.hookSpecificOutput.permissionDecision !== 'deny', + 'second subagent edit should pass even on a new file'); + })) passed++; else failed++; + // Cleanup only the temp directory created by this test file. try { if (fs.existsSync(stateDir)) {