From 1e895858d0c357f7bcc43581ca9f3f5e86ae1419 Mon Sep 17 00:00:00 2001 From: Eric Bellavance Date: Thu, 1 Oct 2026 13:23:13 -0400 Subject: [PATCH 01/39] docs(kiro): use always inclusion for always-on steering files Kiro only loads steering files automatically in every conversation when they use `inclusion: always`; `auto` loads them only when the request matches the description. Switch the always-on steering files to `always` and update README, install.sh and docs accordingly. Co-Authored-By: Claude Opus 5.5 --- .kiro/README.md | 46 +++++++++++++------------- .kiro/docs/longform-guide.md | 11 +++--- .kiro/docs/security-guide.md | 8 ++--- .kiro/docs/shortform-guide.md | 11 +++--- .kiro/install.sh | 2 +- .kiro/steering/coding-style.md | 2 +- .kiro/steering/development-workflow.md | 2 +- .kiro/steering/git-workflow.md | 2 +- .kiro/steering/lessons-learned.md | 2 +- .kiro/steering/patterns.md | 2 +- .kiro/steering/performance.md | 2 +- .kiro/steering/security.md | 2 +- .kiro/steering/testing.md | 2 +- 13 files changed, 48 insertions(+), 46 deletions(-) diff --git a/.kiro/README.md b/.kiro/README.md index 65c187287..5ef666ac0 100644 --- a/.kiro/README.md +++ b/.kiro/README.md @@ -158,14 +158,14 @@ Steering files provide always-on rules and context that shape how the agent work | File | Inclusion | Description | |------|-----------|-------------| -| `coding-style.md` | auto | Core coding style rules: immutability, file organization, error handling, and code quality standards. Loaded in every conversation. | -| `security.md` | auto | Security best practices including mandatory checks, secret management, and security response protocol. Loaded in every conversation. | -| `testing.md` | auto | Testing requirements: 80% coverage minimum, TDD workflow, and test types (unit, integration, E2E). Loaded in every conversation. | -| `development-workflow.md` | auto | Development process, PR workflow, and collaboration patterns. Loaded in every conversation. | -| `git-workflow.md` | auto | Git commit conventions, branching strategies, and version control best practices. Loaded in every conversation. | -| `patterns.md` | auto | Common design patterns and architectural principles. Loaded in every conversation. | -| `performance.md` | auto | Performance optimization guidelines and profiling strategies. Loaded in every conversation. | -| `lessons-learned.md` | auto | Project-specific patterns and learnings. Edit this file to capture your team's conventions. Loaded in every conversation. | +| `coding-style.md` | always | Core coding style rules: immutability, file organization, error handling, and code quality standards. Loaded in every conversation. | +| `security.md` | always | Security best practices including mandatory checks, secret management, and security response protocol. Loaded in every conversation. | +| `testing.md` | always | Testing requirements: 80% coverage minimum, TDD workflow, and test types (unit, integration, E2E). Loaded in every conversation. | +| `development-workflow.md` | always | Development process, PR workflow, and collaboration patterns. Loaded in every conversation. | +| `git-workflow.md` | always | Git commit conventions, branching strategies, and version control best practices. Loaded in every conversation. | +| `patterns.md` | always | Common design patterns and architectural principles. Loaded in every conversation. | +| `performance.md` | always | Performance optimization guidelines and profiling strategies. Loaded in every conversation. | +| `lessons-learned.md` | always | Project-specific patterns and learnings. Edit this file to capture your team's conventions. Loaded in every conversation. | | `typescript-patterns.md` | fileMatch: `*.ts,*.tsx` | TypeScript-specific patterns, type safety, and best practices. Loaded when editing TypeScript files. | | `python-patterns.md` | fileMatch: `*.py` | Python-specific patterns, type hints, and best practices. Loaded when editing Python files. | | `golang-patterns.md` | fileMatch: `*.go` | Go-specific patterns, concurrency, and best practices. Loaded when editing Go files. | @@ -181,15 +181,15 @@ Steering files provide always-on rules and context that shape how the agent work | `review-mode.md` | manual | Code review context mode. Invoke with `#review-mode` for thorough reviews. | | `research-mode.md` | manual | Research context mode. Invoke with `#research-mode` for exploration and learning. | -Steering files with `auto` inclusion are loaded automatically. No action needed — they apply as soon as you install them. +Steering files with `always` inclusion are loaded automatically. No action needed — they apply as soon as you install them. To create your own, add a markdown file to `.kiro/steering/` with YAML frontmatter: ```yaml --- -inclusion: auto # auto | fileMatch | manual -name: my-steering # required if inclusion is auto -description: Brief explanation of what this steering file contains +inclusion: always # always | auto | fileMatch | manual +name: my-steering # required if inclusion is auto +description: Brief explanation of what this steering file contains # required if inclusion is auto fileMatchPattern: "*.ts" # required if inclusion is fileMatch --- @@ -325,14 +325,14 @@ Shell scripts used by hooks to perform quality checks and formatting. │ ├── ... and 26 more # (testing, deployment, docker, etc.) │ └── (each skill has a SKILL.md with YAML frontmatter) ├── steering/ # 22 steering files -│ ├── coding-style.md # Auto-loaded coding style rules -│ ├── security.md # Auto-loaded security rules -│ ├── testing.md # Auto-loaded testing rules -│ ├── development-workflow.md # Auto-loaded dev workflow -│ ├── git-workflow.md # Auto-loaded git workflow -│ ├── patterns.md # Auto-loaded design patterns -│ ├── performance.md # Auto-loaded performance rules -│ ├── lessons-learned.md # Auto-loaded project patterns +│ ├── coding-style.md # Always-loaded coding style rules +│ ├── security.md # Always-loaded security rules +│ ├── testing.md # Always-loaded testing rules +│ ├── development-workflow.md # Always-loaded dev workflow +│ ├── git-workflow.md # Always-loaded git workflow +│ ├── patterns.md # Always-loaded design patterns +│ ├── performance.md # Always-loaded performance rules +│ ├── lessons-learned.md # Always-loaded project patterns │ ├── typescript-patterns.md # Loaded for .ts/.tsx files │ ├── typescript-security.md # Loaded for .ts/.tsx files │ ├── python-patterns.md # Loaded for .py files @@ -395,7 +395,7 @@ All files are yours to modify after installation. The installer never overwrites 5. **Run quality gate**: Trigger the `quality-gate` hook before committing 6. **Verify comprehensively**: Use the `verification-loop` skill before creating PRs -The auto-loaded steering files (coding-style, security, testing) ensure consistent standards throughout your session. +The always-loaded steering files (coding-style, security, testing) ensure consistent standards throughout your session. ## Usage Examples @@ -466,7 +466,7 @@ kiro-cli --agent code-reviewer > /agent swap security-reviewer > "Analyze the API endpoints for security vulnerabilities" -# 4. The security.md steering file is auto-loaded, ensuring: +# 4. The security.md steering file is always-loaded, ensuring: # - No hardcoded secrets # - Proper error handling # - Secure crypto usage @@ -486,7 +486,7 @@ kiro-cli --agent python-reviewer > "Review the type hints and error handling" > /python-patterns # Invoke Python-specific patterns skill -# Language-specific steering files are auto-loaded: +# Language-specific steering files are loaded automatically on file match: # - golang-patterns.md loads when editing .go files # - python-patterns.md loads when editing .py files # - typescript-patterns.md loads when editing .ts/.tsx files diff --git a/.kiro/docs/longform-guide.md b/.kiro/docs/longform-guide.md index 2216a2cf5..34c88432a 100644 --- a/.kiro/docs/longform-guide.md +++ b/.kiro/docs/longform-guide.md @@ -41,9 +41,10 @@ Skills provide structured guidance for complex, multi-step processes. ### 3. Steering Files as Persistent Context -Steering files inject rules and patterns into every conversation: +Steering files inject rules and patterns into conversations: -- **Auto-inclusion**: Always-on rules (coding style, security, testing) +- **Always-inclusion**: Always-on rules (coding style, security, testing) +- **Auto-inclusion**: Files are automatically included when your request matches the description. - **File-match**: Conditional rules based on file type (TypeScript patterns for .ts files) - **Manual**: Context modes you invoke explicitly (dev-mode, review-mode) @@ -146,7 +147,7 @@ The `lessons-learned.md` steering file acts as your project's evolving knowledge ```markdown --- -inclusion: auto +inclusion: always description: Project-specific patterns and decisions --- @@ -236,9 +237,9 @@ If an agent's output isn't quite right, provide specific feedback and let it ite Don't try to describe a complex workflow in a single prompt. Use skills that encode best practices. -### 8. Combine Auto and Manual Steering +### 8. Combine Steering Inclusion Modes -Use auto-inclusion for universal rules, file-match for language-specific patterns, and manual for context switching. +Use always-inclusion for universal rules, auto-inclusion for context-heavy guidance that should only load when relevant, file-match for language-specific patterns, and manual for context switching. ## Common Pitfalls diff --git a/.kiro/docs/security-guide.md b/.kiro/docs/security-guide.md index c034bdfb1..fcb94a48a 100644 --- a/.kiro/docs/security-guide.md +++ b/.kiro/docs/security-guide.md @@ -49,7 +49,7 @@ AI agents are powerful development tools, but they introduce unique security con **Why**: It's easier to maintain security from the start than to retrofit it later. **Practice**: -- Enable auto-inclusion security steering files +- Enable always-inclusion security steering files - Use TDD workflow with security test cases - Include security requirements in planning phase - Document security decisions in lessons-learned @@ -81,7 +81,7 @@ Plan a user authentication feature with these security requirements: **Risk**: May introduce vulnerabilities like SQL injection, XSS, or insecure deserialization. **Mitigation**: -- Enable security steering files (auto-loaded) +- Enable security steering files (always-loaded) - Use git-push-review hook to catch issues before commit - Run security-reviewer agent after implementation - Include security test cases in TDD workflow @@ -203,7 +203,7 @@ Plan a user authentication feature with these security requirements: **Key Rules to Include**: ```markdown --- -inclusion: auto +inclusion: always description: Security best practices and vulnerability prevention --- @@ -434,7 +434,7 @@ For production: ### Before Starting Development -- [ ] Security steering files enabled (auto-inclusion) +- [ ] Security steering files enabled (always-inclusion) - [ ] Security-focused hooks enabled (git-push-review, console-log-check) - [ ] MCP servers reviewed and configured securely - [ ] Secrets management strategy in place diff --git a/.kiro/docs/shortform-guide.md b/.kiro/docs/shortform-guide.md index ace257f31..1f099eaf2 100644 --- a/.kiro/docs/shortform-guide.md +++ b/.kiro/docs/shortform-guide.md @@ -78,7 +78,7 @@ Type `/` in chat and select from the menu, or use: ## Steering Files -### Auto-Loaded (Always Active) +### Always-Loaded (Always Active) - `coding-style.md` - Code organization and naming - `development-workflow.md` - Dev process and PR workflow @@ -267,9 +267,10 @@ cat .kiro/skills/skill-name/SKILL.md ### Steering File Not Loading -1. Check frontmatter: `inclusion: auto` or `fileMatch` or `manual` +1. Check frontmatter: `inclusion: always` or `auto` or `fileMatch` or `manual` 2. For fileMatch, verify pattern: `fileMatchPattern: "*.ts,*.tsx"` -3. For manual, invoke with: `#filename` +3. For auto, verify `name` and `description` are set and the description matches your request +4. For manual, invoke with: `#filename` ### Script Not Executing @@ -326,7 +327,7 @@ Instructions for the agent... 1. Create `.kiro/steering/my-rules.md`: ```markdown --- -inclusion: auto +inclusion: always description: My custom rules --- @@ -335,7 +336,7 @@ description: My custom rules Rules and patterns... ``` -2. Auto-loaded in every conversation +2. Always-loaded in every conversation ### Add Your Own Hook diff --git a/.kiro/install.sh b/.kiro/install.sh index 2d5f0c26a..f42a92555 100755 --- a/.kiro/install.sh +++ b/.kiro/install.sh @@ -138,6 +138,6 @@ echo "Next steps:" echo " 1. Open your project in Kiro" echo " 2. Agents: Automatic in IDE, /agent swap in CLI" echo " 3. Skills: Available via / menu in chat" -echo " 4. Steering files with 'auto' inclusion load automatically" +echo " 4. Steering files with 'always' inclusion load automatically" echo " 5. Toggle hooks in the Agent Hooks panel" echo " 6. Copy desired MCP servers from .kiro/settings/mcp.json.example to .kiro/settings/mcp.json" diff --git a/.kiro/steering/coding-style.md b/.kiro/steering/coding-style.md index e972e9a47..12e3f07e2 100644 --- a/.kiro/steering/coding-style.md +++ b/.kiro/steering/coding-style.md @@ -1,5 +1,5 @@ --- -inclusion: auto +inclusion: always name: coding-style description: Core coding style rules including immutability, file organization, error handling, and code quality standards. --- diff --git a/.kiro/steering/development-workflow.md b/.kiro/steering/development-workflow.md index ba29d83db..89958c290 100644 --- a/.kiro/steering/development-workflow.md +++ b/.kiro/steering/development-workflow.md @@ -1,5 +1,5 @@ --- -inclusion: auto +inclusion: always name: development-workflow description: Development workflow guidelines for planning, TDD, code review, and commit pipeline --- diff --git a/.kiro/steering/git-workflow.md b/.kiro/steering/git-workflow.md index b78b2163d..3c72d2a85 100644 --- a/.kiro/steering/git-workflow.md +++ b/.kiro/steering/git-workflow.md @@ -1,5 +1,5 @@ --- -inclusion: auto +inclusion: always name: git-workflow description: Git workflow guidelines for conventional commits and pull request process --- diff --git a/.kiro/steering/lessons-learned.md b/.kiro/steering/lessons-learned.md index 0c96707dd..2287b0799 100644 --- a/.kiro/steering/lessons-learned.md +++ b/.kiro/steering/lessons-learned.md @@ -1,5 +1,5 @@ --- -inclusion: auto +inclusion: always name: lessons-learned description: Project-specific patterns, preferences, and lessons learned over time (user-editable) --- diff --git a/.kiro/steering/patterns.md b/.kiro/steering/patterns.md index 9139aa03d..570c33d0c 100644 --- a/.kiro/steering/patterns.md +++ b/.kiro/steering/patterns.md @@ -1,5 +1,5 @@ --- -inclusion: auto +inclusion: always name: patterns description: Common design patterns including repository pattern, API response format, and skeleton project approach --- diff --git a/.kiro/steering/performance.md b/.kiro/steering/performance.md index 7cae57041..2d18d5e2a 100644 --- a/.kiro/steering/performance.md +++ b/.kiro/steering/performance.md @@ -1,5 +1,5 @@ --- -inclusion: auto +inclusion: always name: performance description: Performance optimization guidelines including model selection strategy, context window management, and build troubleshooting --- diff --git a/.kiro/steering/security.md b/.kiro/steering/security.md index 335d2836d..3d68c069c 100644 --- a/.kiro/steering/security.md +++ b/.kiro/steering/security.md @@ -1,5 +1,5 @@ --- -inclusion: auto +inclusion: always name: security description: Security best practices including mandatory checks, secret management, and security response protocol. --- diff --git a/.kiro/steering/testing.md b/.kiro/steering/testing.md index e3478e7c8..a3c1522d0 100644 --- a/.kiro/steering/testing.md +++ b/.kiro/steering/testing.md @@ -1,5 +1,5 @@ --- -inclusion: auto +inclusion: always name: testing description: Testing requirements including 80% coverage, TDD workflow, and test types. --- From eb32557b7f565f8480f75967846b51929211cbb1 Mon Sep 17 00:00:00 2001 From: Eric Bellavance Date: Thu, 1 Oct 2026 15:09:33 -0400 Subject: [PATCH 02/39] docs(kiro): document steering upgrade for existing installs The installer never overwrites steering files that already exist, so projects installed before the switch to `inclusion: always` keep `auto`. Document the manual frontmatter update alongside the no-action-needed note. Co-Authored-By: Claude Opus 5.5 --- .kiro/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.kiro/README.md b/.kiro/README.md index 5ef666ac0..abc1e94a5 100644 --- a/.kiro/README.md +++ b/.kiro/README.md @@ -183,6 +183,8 @@ Steering files provide always-on rules and context that shape how the agent work Steering files with `always` inclusion are loaded automatically. No action needed — they apply as soon as you install them. +> **Upgrading an existing install:** the installer never overwrites steering files that already exist in your project. If you installed before these files switched to `always`, change `inclusion: auto` to `inclusion: always` in the frontmatter of `coding-style.md`, `security.md`, `testing.md`, `development-workflow.md`, `git-workflow.md`, `patterns.md`, `performance.md`, and `lessons-learned.md` in your `.kiro/steering/` directory, or delete those files and rerun the installer. + To create your own, add a markdown file to `.kiro/steering/` with YAML frontmatter: ```yaml From a7f316a219866e13b4a7d1bd29e7fa4e84773f58 Mon Sep 17 00:00:00 2001 From: Eric Bellavance Date: Thu, 1 Oct 2026 15:09:33 -0400 Subject: [PATCH 03/39] test(kiro): assert steering inclusion modes match the README Pin the core steering files to `inclusion: always`, check every steering file's frontmatter against the .kiro/README.md table, and require the fields each inclusion mode needs. Co-Authored-By: Claude Opus 5.5 --- tests/docs/kiro-steering-inclusion.test.js | 106 +++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/docs/kiro-steering-inclusion.test.js diff --git a/tests/docs/kiro-steering-inclusion.test.js b/tests/docs/kiro-steering-inclusion.test.js new file mode 100644 index 000000000..beb3c6b91 --- /dev/null +++ b/tests/docs/kiro-steering-inclusion.test.js @@ -0,0 +1,106 @@ +/** + * Regression coverage for Kiro steering file inclusion modes. + * + * Kiro only loads a steering file in every conversation with `inclusion: always`; + * `auto` loads it only when the request matches its description. + */ + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const KIRO_DIR = path.join(__dirname, '..', '..', '.kiro'); +const STEERING_DIR = path.join(KIRO_DIR, 'steering'); +const README = path.join(KIRO_DIR, 'README.md'); + +const ALWAYS_ON = [ + 'coding-style.md', + 'security.md', + 'testing.md', + 'development-workflow.md', + 'git-workflow.md', + 'patterns.md', + 'performance.md', + 'lessons-learned.md', +]; + +function test(name, fn) { + try { + fn(); + console.log(` ✓ ${name}`); + return true; + } catch (error) { + console.log(` ✗ ${name}`); + console.log(` Error: ${error.message}`); + return false; + } +} + +function readFrontmatter(file) { + const content = fs.readFileSync(path.join(STEERING_DIR, file), 'utf8'); + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + assert.ok(match, `${file} should start with YAML frontmatter`); + const fields = {}; + for (const line of match[1].split(/\r?\n/)) { + const field = line.match(/^(\w+):\s*(.*)$/); + if (field) fields[field[1]] = field[2].trim(); + } + return fields; +} + +function readReadmeInclusions() { + const readme = fs.readFileSync(README, 'utf8'); + const inclusions = {}; + for (const row of readme.matchAll(/^\| `([\w-]+\.md)` \| (always|auto|manual|fileMatch)\b/gm)) { + inclusions[row[1]] = row[2]; + } + return inclusions; +} + +function runTests() { + console.log('\n=== Testing Kiro steering inclusion modes ===\n'); + + let passed = 0; + let failed = 0; + + const steeringFiles = fs.readdirSync(STEERING_DIR).filter(file => file.endsWith('.md')); + + if (test('core steering files use always inclusion', () => { + for (const file of ALWAYS_ON) { + assert.strictEqual( + readFrontmatter(file).inclusion, + 'always', + `${file} should use inclusion: always so it loads in every conversation` + ); + } + })) passed++; else failed++; + + if (test('README steering table matches each file frontmatter', () => { + const documented = readReadmeInclusions(); + for (const file of steeringFiles) { + assert.ok(documented[file], `${file} should be listed in the .kiro/README.md steering table`); + assert.strictEqual( + readFrontmatter(file).inclusion, + documented[file], + `${file} frontmatter inclusion should match the README table` + ); + } + })) passed++; else failed++; + + if (test('steering files declare the fields their inclusion mode requires', () => { + for (const file of steeringFiles) { + const fields = readFrontmatter(file); + if (fields.inclusion === 'auto') { + assert.ok(fields.name && fields.description, `${file} uses auto inclusion and needs name and description`); + } + if (fields.inclusion === 'fileMatch') { + assert.ok(fields.fileMatchPattern, `${file} uses fileMatch inclusion and needs fileMatchPattern`); + } + } + })) passed++; else failed++; + + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); + process.exit(failed > 0 ? 1 : 0); +} + +runTests(); From 828e7c41fa5e61a55368901c8de744d433b878a7 Mon Sep 17 00:00:00 2001 From: Eric Bellavance Date: Thu, 1 Oct 2026 15:17:51 -0400 Subject: [PATCH 04/39] Update .kiro/README.md Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .kiro/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.kiro/README.md b/.kiro/README.md index abc1e94a5..c9f3cc7cb 100644 --- a/.kiro/README.md +++ b/.kiro/README.md @@ -183,7 +183,7 @@ Steering files provide always-on rules and context that shape how the agent work Steering files with `always` inclusion are loaded automatically. No action needed — they apply as soon as you install them. -> **Upgrading an existing install:** the installer never overwrites steering files that already exist in your project. If you installed before these files switched to `always`, change `inclusion: auto` to `inclusion: always` in the frontmatter of `coding-style.md`, `security.md`, `testing.md`, `development-workflow.md`, `git-workflow.md`, `patterns.md`, `performance.md`, and `lessons-learned.md` in your `.kiro/steering/` directory, or delete those files and rerun the installer. +> **Upgrading an existing install:** the installer never overwrites steering files that already exist in your project. If you installed before these files switched to `always`, change `inclusion: auto` to `inclusion: always` in the frontmatter of `coding-style.md`, `security.md`, `testing.md`, `development-workflow.md`, `git-workflow.md`, `patterns.md`, `performance.md`, and `lessons-learned.md` in your `.kiro/steering/` directory. To create your own, add a markdown file to `.kiro/steering/` with YAML frontmatter: From 8c560ac0cdb6129161cc0dae47b06fa7ff1f7977 Mon Sep 17 00:00:00 2001 From: sybertoooth7-lgtm Date: Mon, 5 Oct 2026 10:17:49 +0300 Subject: [PATCH 05/39] fix(ci): close catalog gate holes on README project tree The catalog gate validated only the first README project-tree block and had no skills/commands counterpart to the agents pattern, so: - README.md project tree (skills) and (commands) were never checked. - A stale count in a second tree block was invisible because the agents pattern used String.match with a non-global regex, which reads only the first occurrence. Both are now fail-closed. parseReadmeExpectations walks every tree block for all three categories via matchAll, reports the offending line number in the source label, and throws when an entry is missing entirely rather than skipping it. syncEnglishReadme uses replaceAllOrThrow so --write repairs every block instead of only the first; replaceOrThrow could not be reused because it replaces only the first match and its regex.test() probe is unsafe once the pattern carries the g flag. README.md:847 documented 67 agents against an actual 68 and was never checked, so it was corrected. Fixtures in tests/ci/catalog.test.js and tests/ci/validators.test.js now emit the skills and commands tree lines the gate requires, and both gain regression coverage that fails against the previous parser (5 new tests). --- README.md | 2 +- scripts/ci/catalog.js | 64 ++++++++++++++++++++++++------ tests/ci/catalog.test.js | 75 ++++++++++++++++++++++++++++++++++- tests/ci/validators.test.js | 79 ++++++++++++++++++++++++++++++++++++- 4 files changed, 205 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 95673de4b..0fe60d44e 100644 --- a/README.md +++ b/README.md @@ -848,7 +848,7 @@ ECC/ | |-- plugin.json # Plugin metadata and component paths | |-- marketplace.json # Marketplace catalog for /plugin marketplace add | -|-- agents/ # 67 specialized subagents for delegation +|-- agents/ # 68 specialized subagents for delegation | |-- planner.md # Feature implementation planning | |-- architect.md # System design decisions | |-- tdd-guide.md # Test-driven development diff --git a/scripts/ci/catalog.js b/scripts/ci/catalog.js index fffa37a3d..566178f5c 100644 --- a/scripts/ci/catalog.js +++ b/scripts/ci/catalog.js @@ -87,6 +87,23 @@ function replaceOrThrow(content, regex, replacer, source) { return content.replace(regex, replacer); } +function countMarkers(content, regex) { + return [...content.matchAll(regex)].length; +} + +function replaceAllOrThrow(content, regex, replacer, source, expectedCount) { + const found = countMarkers(content, regex); + if (found === 0) { + throw new Error(`${source} is missing the expected catalog marker`); + } + + if (expectedCount !== undefined && found !== expectedCount) { + throw new Error(`${source} expected ${expectedCount} marker(s), found ${found}`); + } + + return content.replace(regex, replacer); +} + function parseReadmeExpectations(readmeContent) { const expectations = []; @@ -103,17 +120,28 @@ function parseReadmeExpectations(readmeContent) { { category: 'commands', mode: 'exact', expected: Number(quickStartMatch[3]), source: 'README.md quick-start summary' } ); - const projectTreeAgentsMatch = readmeContent.match(/^\|\s*--\s*agents\/\s*#\s*(\d+)\s+specialized subagents for delegation\s*$/im); - if (!projectTreeAgentsMatch) { - throw new Error('README.md project tree is missing the agents count'); - } + const projectTreePatterns = [ + { category: 'agents', regex: /^\|\s*--\s*agents\/\s*#\s*(\d+)\s+specialized subagents for delegation\s*$/gim, source: 'README.md project tree (agents)' }, + { category: 'commands', regex: /^\|\s*--\s*commands\/\s*#\s*(\d+)\s+maintained slash-command shims\s*$/gim, source: 'README.md project tree (commands)' }, + { category: 'skills', regex: /^\|\s*--\s*skills\/\s*#\s*(\d+)\s+reusable workflows loaded on demand\s*$/gim, source: 'README.md project tree (skills)' } + ]; - expectations.push({ - category: 'agents', - mode: 'exact', - expected: Number(projectTreeAgentsMatch[1]), - source: 'README.md project tree (agents)' - }); + for (const pattern of projectTreePatterns) { + const matches = [...readmeContent.matchAll(pattern.regex)]; + + if (matches.length === 0) { + throw new Error(`${pattern.source} is missing the ${pattern.category} count`); + } + + for (const match of matches) { + expectations.push({ + category: pattern.category, + mode: 'exact', + expected: Number(match[1]), + source: `${pattern.source} block ${readmeContent.slice(0, match.index).split('\n').length}` + }); + } + } const tablePatterns = [ { category: 'agents', regex: /\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s+agents\s*\|/i, source: 'README.md comparison table' }, @@ -409,12 +437,24 @@ function syncEnglishReadme(content, catalog) { `${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsSuffix}${catalog.commands.count} legacy command shims`, 'README.md quick-start summary' ); - nextContent = replaceOrThrow( + nextContent = replaceAllOrThrow( nextContent, - /^(\|\s*--\s*agents\/\s*#\s*)(\d+)(\s+specialized subagents for delegation\s*)$/im, + /^(\|\s*--\s*agents\/\s*#\s*)(\d+)(\s+specialized subagents for delegation\s*)$/gim, (_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`, 'README.md project tree (agents)' ); + nextContent = replaceAllOrThrow( + nextContent, + /^(\|\s*--\s*commands\/\s*#\s*)(\d+)(\s+maintained slash-command shims\s*)$/gim, + (_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`, + 'README.md project tree (commands)' + ); + nextContent = replaceAllOrThrow( + nextContent, + /^(\|\s*--\s*skills\/\s*#\s*)(\d+)(\s+reusable workflows loaded on demand\s*)$/gim, + (_, prefix, __, suffix) => `${prefix}${catalog.skills.count}${suffix}`, + 'README.md project tree (skills)' + ); nextContent = replaceOrThrow( nextContent, /(\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s+agents\s*\|)/i, diff --git a/tests/ci/catalog.test.js b/tests/ci/catalog.test.js index f05eb6bc2..4eee19058 100644 --- a/tests/ci/catalog.test.js +++ b/tests/ci/catalog.test.js @@ -46,6 +46,9 @@ function writeEnglishReadme(root, counts, options = {}) { fs.writeFileSync(path.join(root, 'README.md'), `Access to ${counts.agents} agents, ${counts.skills} skills, and ${counts.commands} commands. - **Public surface synced to the live repo** - metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: ${counts.agents} agents, ${counts.skills} skills, and ${counts.commands} legacy command shims. |-- agents/ # ${counts.agents} specialized subagents for delegation +|-- skills/ # ${counts.skills} reusable workflows loaded on demand +|-- commands/ # ${counts.commands} maintained slash-command shims +${options.extraTreeBlock || ''} | Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode | | --- | --- | --- | --- | --- | | Agents | PASS: ${tableCounts.agents} agents | @@ -170,7 +173,7 @@ function writeCatalogFixture(root, options = {}) { fs.writeFileSync(path.join(root, 'commands', 'notes.txt'), 'not counted\n'); fs.mkdirSync(path.join(root, 'skills', 'missing-skill-file'), { recursive: true }); - writeEnglishReadme(root, documentedCounts, { unrelatedSkillsCount }); + writeEnglishReadme(root, documentedCounts, { unrelatedSkillsCount, extraTreeBlock: options.extraTreeBlock }); writeEnglishAgents(root, documentedCounts, { skillsMinimum }); writeCrossHarnessIdentityDocs(root, documentedCounts); writeZhRootReadme(root, documentedCounts); @@ -292,6 +295,76 @@ function runTests() { } })) passed++; else failed++; + if (test('flags a stale count hidden in a second project-tree block', () => { + const testDir = createTestDir(); + try { + writeCatalogFixture(testDir, { + actualCounts: { agents: 2, skills: 3, commands: 4 }, + documentedCounts: { agents: 2, skills: 3, commands: 4 }, + extraTreeBlock: '|-- agents/ # 1 specialized subagents for delegation', + }); + + const result = runCatalogCheck({ root: testDir }); + const failing = result.checks.filter(check => !check.ok && check.source.includes('project tree')); + + assert.strictEqual(failing.length, 1); + assert.match(failing[0].source, /project tree \(agents\) block \d+/); + assert.strictEqual(failing[0].expected, 1); + assert.strictEqual(failing[0].actual, 2); + } finally { + cleanupTestDir(testDir); + } + })) passed++; else failed++; + + if (test('write mode repairs every project-tree block, not just the first', () => { + const testDir = createTestDir(); + try { + writeCatalogFixture(testDir, { + actualCounts: { agents: 2, skills: 3, commands: 4 }, + documentedCounts: { agents: 2, skills: 3, commands: 4 }, + extraTreeBlock: [ + '|-- agents/ # 1 specialized subagents for delegation', + '|-- skills/ # 1 reusable workflows loaded on demand', + ].join('\n'), + }); + + const result = runCatalogCheck({ root: testDir, writeMode: true }); + + assert.strictEqual(result.checks.filter(check => !check.ok).length, 0); + + const readme = fs.readFileSync(path.join(testDir, 'README.md'), 'utf8'); + assert.ok(!readme.includes('# 1 specialized subagents for delegation')); + assert.ok(!readme.includes('# 1 reusable workflows loaded on demand')); + assert.strictEqual(readme.split('# 2 specialized subagents for delegation').length - 1, 2); + } finally { + cleanupTestDir(testDir); + } + })) passed++; else failed++; + + if (test('throws when a project-tree count entry is missing entirely', () => { + const testDir = createTestDir(); + try { + writeCatalogFixture(testDir, { + actualCounts: { agents: 2, skills: 3, commands: 4 }, + documentedCounts: { agents: 2, skills: 3, commands: 4 }, + }); + + const readmePath = path.join(testDir, 'README.md'); + const withoutSkills = fs.readFileSync(readmePath, 'utf8') + .split('\n') + .filter(line => !line.includes('reusable workflows loaded on demand')) + .join('\n'); + fs.writeFileSync(readmePath, withoutSkills); + + assert.throws( + () => runCatalogCheck({ root: testDir }), + /README\.md project tree \(skills\) is missing the skills count/ + ); + } finally { + cleanupTestDir(testDir); + } + })) passed++; else failed++; + if (test('throws a clear error for missing tracked documents', () => { const testDir = createTestDir(); try { diff --git a/tests/ci/validators.test.js b/tests/ci/validators.test.js index ea9c0b4a8..ace7ccbdb 100644 --- a/tests/ci/validators.test.js +++ b/tests/ci/validators.test.js @@ -260,6 +260,9 @@ function writeCatalogFixture(testDir, options = {}) { const { readmeCounts = { agents: 1, skills: 1, commands: 1 }, readmeProjectTreeAgents = readmeCounts.agents, + readmeProjectTreeSkills = readmeCounts.skills, + readmeProjectTreeCommands = readmeCounts.commands, + readmeExtraTreeBlock = '', readmeTableCounts = readmeCounts, readmeParityCounts = readmeCounts, readmeUnrelatedSkillsCount = 16, @@ -306,7 +309,10 @@ function writeCatalogFixture(testDir, options = {}) { fs.writeFileSync(path.join(testDir, 'commands', 'plan.md'), '---\ndescription: Plan\n---\n# Plan'); fs.writeFileSync(path.join(testDir, 'skills', 'demo-skill', 'SKILL.md'), '---\nname: demo-skill\ndescription: Demo skill\norigin: ECC\n---\n# Demo Skill'); - fs.writeFileSync(readmePath, `Access to ${readmeCounts.agents} agents, ${readmeCounts.skills} skills, and ${readmeCounts.commands} commands.\n- **Public surface synced to the live repo** - metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: ${readmeCounts.agents} agents, ${readmeCounts.skills} skills, and ${readmeCounts.commands} legacy command shims.\n|-- agents/ # ${readmeProjectTreeAgents} specialized subagents for delegation\n| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| Agents | PASS: ${readmeTableCounts.agents} agents | Shared | Shared | 1 |\n| Commands | PASS: ${readmeTableCounts.commands} commands | Shared | Shared | 1 |\n| Skills | PASS: ${readmeTableCounts.skills} skills | Shared | Shared | 1 |\n\n| Feature | Count | Format |\n|-----------|-------|---------|\n| Skills | ${readmeUnrelatedSkillsCount} | .agents/skills/ |\n\n## Cross-Tool Feature Parity\n\n| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| **Agents** | ${readmeParityCounts.agents} | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |\n| **Commands** | ${readmeParityCounts.commands} | Shared | Instruction-based | 31 |\n| **Skills** | ${readmeParityCounts.skills} | Shared | 10 (native format) | 37 |\n`); + fs.writeFileSync(readmePath, `Access to ${readmeCounts.agents} agents, ${readmeCounts.skills} skills, and ${readmeCounts.commands} commands.\n- **Public surface synced to the live repo** - metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: ${readmeCounts.agents} agents, ${readmeCounts.skills} skills, and ${readmeCounts.commands} legacy command shims.\n|-- agents/ # ${readmeProjectTreeAgents} specialized subagents for delegation +|-- skills/ # ${readmeProjectTreeSkills} reusable workflows loaded on demand +|-- commands/ # ${readmeProjectTreeCommands} maintained slash-command shims +${readmeExtraTreeBlock}\n| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| Agents | PASS: ${readmeTableCounts.agents} agents | Shared | Shared | 1 |\n| Commands | PASS: ${readmeTableCounts.commands} commands | Shared | Shared | 1 |\n| Skills | PASS: ${readmeTableCounts.skills} skills | Shared | Shared | 1 |\n\n| Feature | Count | Format |\n|-----------|-------|---------|\n| Skills | ${readmeUnrelatedSkillsCount} | .agents/skills/ |\n\n## Cross-Tool Feature Parity\n\n| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| **Agents** | ${readmeParityCounts.agents} | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |\n| **Commands** | ${readmeParityCounts.commands} | Shared | Instruction-based | 31 |\n| **Skills** | ${readmeParityCounts.skills} | Shared | 10 (native format) | 37 |\n`); fs.writeFileSync(agentsPath, `This is a **production-ready AI coding plugin** providing ${summaryCounts.agents} specialized agents, ${summaryCounts.skills} skills, ${summaryCounts.commands} commands, and automated hook workflows for software development.\n\n\`\`\`\n${structureLines.join('\n')}\n\`\`\`\n`); fs.writeFileSync(zhRootReadmePath, `**完成!** 你现在可以使用 ${zhRootReadmeCounts.agents} 个代理、${zhRootReadmeCounts.skills} 个技能和 ${zhRootReadmeCounts.commands} 个命令。\n`); fs.writeFileSync(zhDocsReadmePath, `**搞定!** 你现在可以使用 ${zhDocsReadmeCounts.agents} 个智能体、${zhDocsReadmeCounts.skills} 项技能和 ${zhDocsReadmeCounts.commands} 个命令了。\n| 功能特性 | Claude Code | OpenCode | 状态 |\n|---------|-------------|----------|--------|\n| 智能体 | \u2705 ${zhDocsTableCounts.agents} 个 | \u2705 12 个 | **Claude Code 领先** |\n| 命令 | \u2705 ${zhDocsTableCounts.commands} 个 | \u2705 31 个 | **Claude Code 领先** |\n| 技能 | \u2705 ${zhDocsTableCounts.skills} 项 | \u2705 37 项 | **Claude Code 领先** |\n\n| 功能特性 | 数量 | 格式 |\n|-----------|-------|---------|\n| 技能 | ${zhDocsUnrelatedSkillsCount} | .agents/skills/ |\n\n## 跨工具功能对等\n\n| 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| **智能体** | ${zhDocsParityCounts.agents} | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |\n| **命令** | ${zhDocsParityCounts.commands} | 共享 | 基于指令 | 31 |\n| **技能** | ${zhDocsParityCounts.skills} | 共享 | 10 (原生格式) | 37 |\n`); @@ -842,6 +848,77 @@ function runTests() { cleanupTestDir(testDir); })) passed++; else failed++; + if (test('flags a stale count hidden in a second README project-tree block', () => { + const testDir = createTestDir(); + const { + readmePath, + agentsPath, + zhRootReadmePath, + zhDocsReadmePath, + zhAgentsPath, + pluginJsonPath, + marketplaceJsonPath, + } = writeCatalogFixture(testDir, { + readmeExtraTreeBlock: '|-- agents/ # 9 specialized subagents for delegation', + }); + + const result = runCatalogValidator({ + ROOT: testDir, + README_PATH: readmePath, + AGENTS_PATH: agentsPath, + README_ZH_CN_PATH: zhRootReadmePath, + DOCS_ZH_CN_README_PATH: zhDocsReadmePath, + DOCS_ZH_CN_AGENTS_PATH: zhAgentsPath, + PLUGIN_JSON_PATH: pluginJsonPath, + MARKETPLACE_JSON_PATH: marketplaceJsonPath, + }); + + const output = result.stdout + result.stderr; + assert.strictEqual(result.code, 1, `Should fail on a stale second block, got ${output}`); + assert.ok( + output.includes('README.md project tree (agents) block'), + `Should name the offending block, got ${output}` + ); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (test('write mode repairs a stale second README project-tree block', () => { + const testDir = createTestDir(); + const { + readmePath, + agentsPath, + zhRootReadmePath, + zhDocsReadmePath, + zhAgentsPath, + pluginJsonPath, + marketplaceJsonPath, + } = writeCatalogFixture(testDir, { + readmeExtraTreeBlock: '|-- agents/ # 9 specialized subagents for delegation', + }); + + const result = runCatalogValidator({ + argv: ['--write', '--text'], + ROOT: testDir, + README_PATH: readmePath, + AGENTS_PATH: agentsPath, + README_ZH_CN_PATH: zhRootReadmePath, + DOCS_ZH_CN_README_PATH: zhDocsReadmePath, + DOCS_ZH_CN_AGENTS_PATH: zhAgentsPath, + PLUGIN_JSON_PATH: pluginJsonPath, + MARKETPLACE_JSON_PATH: marketplaceJsonPath, + }); + + assert.strictEqual(result.code, 0, `Should sync every block, got ${result.stderr}`); + const readme = fs.readFileSync(readmePath, 'utf8'); + assert.ok(!readme.includes('# 9 specialized subagents for delegation'), 'Should repair the second block too'); + assert.strictEqual( + readme.split('# 1 specialized subagents for delegation').length - 1, + 2, + 'Should correct both blocks' + ); + cleanupTestDir(testDir); + })) passed++; else failed++; + if (test('exits 0 when hooks.json does not exist', () => { const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', '/nonexistent/hooks.json'); assert.strictEqual(result.code, 0, 'Should skip when no hooks.json'); From 11fdfb146635c8c4e6d407dc1480c4d27e10dcd3 Mon Sep 17 00:00:00 2001 From: wanjinhao1 <394959433@qq.com> Date: Tue, 6 Oct 2026 09:06:08 +0800 Subject: [PATCH 06/39] docs(hooks): point permission configuration at settings.json instead of ~/.claude.json Fixes #3421 --- .cursor/rules/common-hooks.md | 2 +- docs/es/rules/common/hooks.md | 2 +- docs/ja-JP/rules/common/hooks.md | 2 +- docs/ko-KR/rules/hooks.md | 2 +- docs/pt-BR/rules/hooks.md | 2 +- docs/tr/rules/common/hooks.md | 2 +- docs/zh-CN/rules/common/hooks.md | 2 +- docs/zh-TW/rules/hooks.md | 2 +- rules/common/hooks.md | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.cursor/rules/common-hooks.md b/.cursor/rules/common-hooks.md index 0f9586fd1..9c62df956 100644 --- a/.cursor/rules/common-hooks.md +++ b/.cursor/rules/common-hooks.md @@ -16,7 +16,7 @@ Use with caution: - Enable for trusted, well-defined plans - Disable for exploratory work - Never use dangerously-skip-permissions flag -- Configure `allowedTools` in `~/.claude.json` instead +- Configure `permissions.allow` rules in `~/.claude/settings.json` (or a project's `.claude/settings.json`) instead ## TodoWrite Best Practices diff --git a/docs/es/rules/common/hooks.md b/docs/es/rules/common/hooks.md index 7f9c9a1c0..93d93b97b 100644 --- a/docs/es/rules/common/hooks.md +++ b/docs/es/rules/common/hooks.md @@ -12,7 +12,7 @@ Usar con precaución: - Habilitar para planes bien definidos y de confianza - Deshabilitar para trabajo exploratorio - Nunca usar la flag dangerously-skip-permissions -- Configurar `allowedTools` en `~/.claude.json` en su lugar +- Configurar reglas `permissions.allow` en `~/.claude/settings.json` (o en el `.claude/settings.json` del proyecto) en su lugar ## Buenas Prácticas de TodoWrite diff --git a/docs/ja-JP/rules/common/hooks.md b/docs/ja-JP/rules/common/hooks.md index 07356b82f..af1f67341 100644 --- a/docs/ja-JP/rules/common/hooks.md +++ b/docs/ja-JP/rules/common/hooks.md @@ -12,7 +12,7 @@ - 信頼できる、明確に定義された計画に対して有効化 - 探索的な作業では無効化 - dangerously-skip-permissions フラグを決して使用しない -- 代わりに `~/.claude.json` で `allowedTools` を設定 +- 代わりに `~/.claude/settings.json`(またはプロジェクトの `.claude/settings.json`)で `permissions.allow` ルールを設定 ## TodoWrite ベストプラクティス diff --git a/docs/ko-KR/rules/hooks.md b/docs/ko-KR/rules/hooks.md index 1f0c2daca..0647e3aa3 100644 --- a/docs/ko-KR/rules/hooks.md +++ b/docs/ko-KR/rules/hooks.md @@ -12,7 +12,7 @@ - 신뢰할 수 있는, 잘 정의된 계획에서만 활성화 - 탐색적 작업에서는 비활성화 - dangerously-skip-permissions 플래그를 절대 사용하지 않기 -- 대신 `~/.claude.json`에서 `allowedTools`를 설정 +- 대신 `~/.claude/settings.json`(또는 프로젝트의 `.claude/settings.json`)에서 `permissions.allow` 규칙을 설정 ## TodoWrite 모범 사례 diff --git a/docs/pt-BR/rules/hooks.md b/docs/pt-BR/rules/hooks.md index 5f2b85e8a..5965eb490 100644 --- a/docs/pt-BR/rules/hooks.md +++ b/docs/pt-BR/rules/hooks.md @@ -12,7 +12,7 @@ Use com cautela: - Habilite para planos confiáveis e bem definidos - Desabilite para trabalho exploratório - Nunca use a flag dangerously-skip-permissions -- Configure `allowedTools` em `~/.claude.json` em vez disso +- Configure regras `permissions.allow` em `~/.claude/settings.json` (ou no `.claude/settings.json` do projeto) em vez disso ## Melhores Práticas para TodoWrite diff --git a/docs/tr/rules/common/hooks.md b/docs/tr/rules/common/hooks.md index f36ab2b2d..3b19377e6 100644 --- a/docs/tr/rules/common/hooks.md +++ b/docs/tr/rules/common/hooks.md @@ -12,7 +12,7 @@ Dikkatli kullan: - Güvenilir, iyi tanımlanmış planlar için etkinleştir - Keşifsel çalışmalar için devre dışı bırak - Asla dangerously-skip-permissions flag'i kullanma -- Bunun yerine `~/.claude.json` içinde `allowedTools` yapılandır +- Bunun yerine `~/.claude/settings.json` (veya projenin `.claude/settings.json` dosyası) içinde `permissions.allow` kurallarını yapılandır ## TodoWrite En İyi Uygulamalar diff --git a/docs/zh-CN/rules/common/hooks.md b/docs/zh-CN/rules/common/hooks.md index 91c142173..8fd600a8f 100644 --- a/docs/zh-CN/rules/common/hooks.md +++ b/docs/zh-CN/rules/common/hooks.md @@ -13,7 +13,7 @@ * 为受信任、定义明确的计划启用 * 为探索性工作禁用 * 切勿使用 dangerously-skip-permissions 标志 -* 改为在 `~/.claude.json` 中配置 `allowedTools` +* 改为在 `~/.claude/settings.json`(或项目的 `.claude/settings.json`)中配置 `permissions.allow` 规则 ## TodoWrite 最佳实践 diff --git a/docs/zh-TW/rules/hooks.md b/docs/zh-TW/rules/hooks.md index 1be8003a4..83f6d090d 100644 --- a/docs/zh-TW/rules/hooks.md +++ b/docs/zh-TW/rules/hooks.md @@ -28,7 +28,7 @@ - 對受信任、定義明確的計畫啟用 - 對探索性工作停用 - 絕不使用 dangerously-skip-permissions flag -- 改為在 `~/.claude.json` 中設定 `allowedTools` +- 改為在 `~/.claude/settings.json`(或專案的 `.claude/settings.json`)中設定 `permissions.allow` 規則 ## TodoWrite 最佳實務 diff --git a/rules/common/hooks.md b/rules/common/hooks.md index 54394083e..4e0947ff0 100644 --- a/rules/common/hooks.md +++ b/rules/common/hooks.md @@ -12,7 +12,7 @@ Use with caution: - Enable for trusted, well-defined plans - Disable for exploratory work - Never use dangerously-skip-permissions flag -- Configure `allowedTools` in `~/.claude.json` instead +- Configure `permissions.allow` rules in `~/.claude/settings.json` (or a project's `.claude/settings.json`) instead ## TodoWrite Best Practices From 25d7e71be6674c28a5e6bfdec2b2a3ab170a7d71 Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:33:23 +0300 Subject: [PATCH 07/39] docs(opencode): describe enabled plugin hook checks --- .opencode/instructions/INSTRUCTIONS.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.opencode/instructions/INSTRUCTIONS.md b/.opencode/instructions/INSTRUCTIONS.md index 64d9d1d82..ea7fbc88e 100644 --- a/.opencode/instructions/INSTRUCTIONS.md +++ b/.opencode/instructions/INSTRUCTIONS.md @@ -301,7 +301,11 @@ interface Repository { ## OpenCode-Specific Notes -Since OpenCode does not support hooks, the following actions that were automated in Claude Code must be done manually: +OpenCode supports hooks through its [plugin system](https://opencode.ai/docs/plugins/). +The ECC plugin automates selected checks when installed and enabled; see +[Hook Runtime Controls](../README.md#hook-runtime-controls) for hook profiles and disabled hook IDs. +Formatting and type checking require the `strict` profile. Run the checks below manually +when the plugin is unavailable, hooks are disabled, or the active profile does not include them. ### After Writing/Editing Code - Run `prettier --write ` to format JS/TS files From b95a4170b7b1a78a0397ab9ce28911e19a7d242c Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:33:25 +0300 Subject: [PATCH 08/39] docs(performance): explain adaptive reasoning effort controls --- .cursor/rules/common-performance.md | 17 +++++++++++------ rules/common/performance.md | 17 +++++++++++------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.cursor/rules/common-performance.md b/.cursor/rules/common-performance.md index ef4114025..750bbb3a3 100644 --- a/.cursor/rules/common-performance.md +++ b/.cursor/rules/common-performance.md @@ -36,16 +36,21 @@ Lower context sensitivity tasks: ## Extended Thinking + Plan Mode -Extended thinking is enabled by default, reserving up to 31,999 tokens for internal reasoning. +In Claude Code, adaptive reasoning lets the model choose how much to think for each task. +For Fable models, Sonnet 5 and later, and Opus 4.7 and later, use effort to control reasoning +rather than a fixed token budget. Available levels depend on the model. -Control extended thinking via: -- **Toggle**: Option+T (macOS) / Alt+T (Windows/Linux) -- **Config**: Set `alwaysThinkingEnabled` in `~/.claude/settings.json` -- **Budget cap**: `export MAX_THINKING_TOKENS=10000` (bash) or `$env:MAX_THINKING_TOKENS = "10000"` (PowerShell) +Control thinking via: +- **Effort**: Use `/effort` in a session, `--effort ` at launch, or `CLAUDE_CODE_EFFORT_LEVEL`. Save a per-model level in `modelSettings` in `~/.claude/settings.json`; the older user-level `effortLevel` key does not apply to Opus 5.5 or newer models. +- **Toggle**: Option+T (macOS) / Alt+T (Windows/Linux), or `alwaysThinkingEnabled` in settings. These cannot disable thinking on Opus 5.5, Sonnet 5.5, or Fable models. +- **Fixed budget**: A positive `MAX_THINKING_TOKENS` does not cap adaptive reasoning. On Opus 4.6 or Sonnet 4.6, set `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` to use a fixed budget with `MAX_THINKING_TOKENS`. - **Verbose mode**: Ctrl+O to see thinking output +See [Claude Code model configuration](https://code.claude.com/docs/en/model-config#adjust-effort-level) +and [environment variables](https://code.claude.com/docs/en/env-vars) for supported controls. + For complex tasks requiring deep reasoning: -1. Ensure extended thinking is enabled (on by default) +1. Choose a higher supported effort level, such as `/effort high` 2. Enable **Plan Mode** for structured approach 3. Use multiple critique rounds for thorough analysis 4. Use split role sub-agents for diverse perspectives diff --git a/rules/common/performance.md b/rules/common/performance.md index 0a2f22977..e8daa4174 100644 --- a/rules/common/performance.md +++ b/rules/common/performance.md @@ -32,16 +32,21 @@ Lower context sensitivity tasks: ## Extended Thinking + Plan Mode -Extended thinking is enabled by default, reserving up to 31,999 tokens for internal reasoning. +In Claude Code, adaptive reasoning lets the model choose how much to think for each task. +For Fable models, Sonnet 5 and later, and Opus 4.7 and later, use effort to control reasoning +rather than a fixed token budget. Available levels depend on the model. -Control extended thinking via: -- **Toggle**: Option+T (macOS) / Alt+T (Windows/Linux) -- **Config**: Set `alwaysThinkingEnabled` in `~/.claude/settings.json` -- **Budget cap**: `export MAX_THINKING_TOKENS=10000` (bash) or `$env:MAX_THINKING_TOKENS = "10000"` (PowerShell) +Control thinking via: +- **Effort**: Use `/effort` in a session, `--effort ` at launch, or `CLAUDE_CODE_EFFORT_LEVEL`. Save a per-model level in `modelSettings` in `~/.claude/settings.json`; the older user-level `effortLevel` key does not apply to Opus 5.5 or newer models. +- **Toggle**: Option+T (macOS) / Alt+T (Windows/Linux), or `alwaysThinkingEnabled` in settings. These cannot disable thinking on Opus 5.5, Sonnet 5.5, or Fable models. +- **Fixed budget**: A positive `MAX_THINKING_TOKENS` does not cap adaptive reasoning. On Opus 4.6 or Sonnet 4.6, set `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` to use a fixed budget with `MAX_THINKING_TOKENS`. - **Verbose mode**: Ctrl+O to see thinking output +See [Claude Code model configuration](https://code.claude.com/docs/en/model-config#adjust-effort-level) +and [environment variables](https://code.claude.com/docs/en/env-vars) for supported controls. + For complex tasks requiring deep reasoning: -1. Ensure extended thinking is enabled (on by default) +1. Choose a higher supported effort level, such as `/effort high` 2. Enable **Plan Mode** for structured approach 3. Use multiple critique rounds for thorough analysis 4. Use split role sub-agents for diverse perspectives From cb1a5b981e5291ce0bba56557eafdfe3749fb532 Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:50:51 +0300 Subject: [PATCH 09/39] docs(opencode): retain mandatory manual verification --- .opencode/instructions/INSTRUCTIONS.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.opencode/instructions/INSTRUCTIONS.md b/.opencode/instructions/INSTRUCTIONS.md index ea7fbc88e..e6dbcea7d 100644 --- a/.opencode/instructions/INSTRUCTIONS.md +++ b/.opencode/instructions/INSTRUCTIONS.md @@ -304,8 +304,10 @@ interface Repository { OpenCode supports hooks through its [plugin system](https://opencode.ai/docs/plugins/). The ECC plugin automates selected checks when installed and enabled; see [Hook Runtime Controls](../README.md#hook-runtime-controls) for hook profiles and disabled hook IDs. -Formatting and type checking require the `strict` profile. Run the checks below manually -when the plugin is unavailable, hooks are disabled, or the active profile does not include them. +Formatting and type checking require the `strict` profile. Type checking runs only for +qualifying TypeScript edits; check newly written files manually. Confirm which checks +actually completed in the plugin output, then run any remaining checks below manually. +Always complete the security review, secret review, and full test suite before committing. ### After Writing/Editing Code - Run `prettier --write ` to format JS/TS files From 57ea31338514771aaec3fa8b1bc8c261903a613e Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:54:14 +0300 Subject: [PATCH 10/39] docs(performance): align thinking cost recommendations --- README.md | 6 ++++-- docs/token-optimization.md | 14 +++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 95673de4b..b1b306ac9 100644 --- a/README.md +++ b/README.md @@ -1718,7 +1718,7 @@ Add to `~/.claude/settings.json`: { "model": "sonnet", "env": { - "MAX_THINKING_TOKENS": "10000", + "CLAUDE_CODE_EFFORT_LEVEL": "medium", "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE": "50", "CLAUDE_CODE_SUBAGENT_MODEL": "haiku" } @@ -1728,10 +1728,12 @@ Add to `~/.claude/settings.json`: | Setting | Default | Recommended | Impact | |---------|---------|-------------|--------| | `model` | opus | **sonnet** | ~60% cost reduction; handles 80%+ of coding tasks | -| `MAX_THINKING_TOKENS` | 31,999 | **10,000** | ~70% reduction in hidden thinking cost per request | +| `CLAUDE_CODE_EFFORT_LEVEL` | Model-dependent | **medium** | Controls adaptive reasoning depth; lower effort trades reasoning depth for latency and cost | | `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` | 95 | **50** | Compacts earlier, better quality in long sessions | | `ECC_CONTEXT_MONITOR_COST_WARNINGS` | on | **off for subscription users** | Suppresses agent-facing API-rate estimate warnings while keeping context/scope/loop warnings | +For fixed-budget models, `MAX_THINKING_TOKENS` controls the thinking budget. On Opus 4.6 or Sonnet 4.6, also set `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` to use that mode. A positive token value does not cap adaptive reasoning; see the [performance rule](rules/common/performance.md#extended-thinking--plan-mode). + Switch to Opus only when you need deep architectural reasoning: ``` /model opus diff --git a/docs/token-optimization.md b/docs/token-optimization.md index 03a10e1f8..f3bf9259a 100644 --- a/docs/token-optimization.md +++ b/docs/token-optimization.md @@ -8,7 +8,7 @@ Practical settings and habits to reduce token consumption, extend session qualit ## Recommended Settings -These are recommended defaults for most users. Power users can tune values further based on their workload — for example, setting `MAX_THINKING_TOKENS` lower for simple tasks or higher for complex architectural work. +Tune reasoning effort to your task and model: lower effort can reduce latency and cost, while higher effort suits complex work. Available levels and defaults depend on the model; see [Claude Code model configuration](https://code.claude.com/docs/en/model-config#adjust-effort-level). Add to your `~/.claude/settings.json`: @@ -16,7 +16,7 @@ Add to your `~/.claude/settings.json`: { "model": "sonnet", "env": { - "MAX_THINKING_TOKENS": "10000", + "CLAUDE_CODE_EFFORT_LEVEL": "medium", "CLAUDE_CODE_SUBAGENT_MODEL": "haiku" } } @@ -27,17 +27,21 @@ Add to your `~/.claude/settings.json`: | Setting | Default | Recommended | Effect | |---------|---------|-------------|--------| | `model` | opus | **sonnet** | Sonnet handles ~80% of coding tasks well. Switch to Opus with `/model opus` for complex reasoning. ~60% cost reduction. | -| `MAX_THINKING_TOKENS` | 31,999 | **10,000** | Extended thinking reserves up to 31,999 output tokens per request for internal reasoning. Reducing this cuts hidden cost by ~70%. Set to `0` to disable for trivial tasks. | +| `CLAUDE_CODE_EFFORT_LEVEL` | Model-dependent | **medium** | Sets adaptive reasoning effort. Adjust it for task complexity; supported levels depend on the model. | | `CLAUDE_CODE_SUBAGENT_MODEL` | _(inherits main)_ | **haiku** | Subagents (Task tool) run on this model. Haiku is ~80% cheaper and sufficient for exploration, file reading, and test running. | | `ECC_CONTEXT_MONITOR_COST_WARNINGS` | on | **off for subscription users** | Suppresses agent-facing API-rate estimate warnings while keeping context exhaustion, scope, and loop warnings. | +### Fixed thinking budgets + +`MAX_THINKING_TOKENS` applies to fixed-budget thinking. For Opus 4.6 or Sonnet 4.6, set `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` to use this mode before setting a positive budget. A positive value is ignored on adaptive reasoning models, so it does not provide a general cost cap. See the [environment variable reference](https://code.claude.com/docs/en/env-vars). + ### Community note on auto-compaction overrides Some recent Claude Code builds have community reports that `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` can only lower the compaction threshold, which means values below the default may compact earlier instead of later. If that happens in your setup, remove the override and rely on manual `/compact` plus ECC's `strategic-compact` guidance. See [Troubleshooting](./TROUBLESHOOTING.md). ### Toggling extended thinking -- **Alt+T** (Windows/Linux) or **Option+T** (macOS) — toggle on/off +- **Alt+T** (Windows/Linux) or **Option+T** (macOS) — toggle on/off where supported. Opus 5.5, Sonnet 5.5, and Fable models always think. - **Ctrl+O** — see thinking output (verbose mode) --- @@ -150,7 +154,7 @@ The `configure-ecc` install wizard could offer to set these environment variable /cost # Check spending # Environment variables (add to ~/.claude/settings.json "env" block) -MAX_THINKING_TOKENS=10000 +CLAUDE_CODE_EFFORT_LEVEL=medium CLAUDE_CODE_SUBAGENT_MODEL=haiku CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 ``` From a8980776d8a6f11cfb109e84eeb277c8d11e3679 Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:22:07 +0300 Subject: [PATCH 11/39] docs: qualify effort settings by provider and model --- .cursor/rules/common-performance.md | 2 +- README.md | 7 ++++++- docs/token-optimization.md | 9 +++++++-- rules/common/performance.md | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.cursor/rules/common-performance.md b/.cursor/rules/common-performance.md index 750bbb3a3..bc7b1b62b 100644 --- a/.cursor/rules/common-performance.md +++ b/.cursor/rules/common-performance.md @@ -43,7 +43,7 @@ rather than a fixed token budget. Available levels depend on the model. Control thinking via: - **Effort**: Use `/effort` in a session, `--effort ` at launch, or `CLAUDE_CODE_EFFORT_LEVEL`. Save a per-model level in `modelSettings` in `~/.claude/settings.json`; the older user-level `effortLevel` key does not apply to Opus 5.5 or newer models. - **Toggle**: Option+T (macOS) / Alt+T (Windows/Linux), or `alwaysThinkingEnabled` in settings. These cannot disable thinking on Opus 5.5, Sonnet 5.5, or Fable models. -- **Fixed budget**: A positive `MAX_THINKING_TOKENS` does not cap adaptive reasoning. On Opus 4.6 or Sonnet 4.6, set `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` to use a fixed budget with `MAX_THINKING_TOKENS`. +- **Fixed budget**: Claude Code ignores positive `MAX_THINKING_TOKENS` values on adaptive reasoning models. On Opus 4.6 or Sonnet 4.6, set `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` to use a fixed budget with `MAX_THINKING_TOKENS`. - **Verbose mode**: Ctrl+O to see thinking output See [Claude Code model configuration](https://code.claude.com/docs/en/model-config#adjust-effort-level) diff --git a/README.md b/README.md index b1b306ac9..dcb2df895 100644 --- a/README.md +++ b/README.md @@ -1725,10 +1725,15 @@ Add to `~/.claude/settings.json`: } ``` +The effort setting requires a model that supports effort controls. On Amazon Bedrock, +Google Cloud's Agent Platform, and Microsoft Foundry, the `sonnet` alias currently +resolves to Sonnet 4.5, which does not support effort controls. Select an effort-capable +model available from your provider, or omit `CLAUDE_CODE_EFFORT_LEVEL`. + | Setting | Default | Recommended | Impact | |---------|---------|-------------|--------| | `model` | opus | **sonnet** | ~60% cost reduction; handles 80%+ of coding tasks | -| `CLAUDE_CODE_EFFORT_LEVEL` | Model-dependent | **medium** | Controls adaptive reasoning depth; lower effort trades reasoning depth for latency and cost | +| `CLAUDE_CODE_EFFORT_LEVEL` | Model-dependent | **medium**, if supported | Controls adaptive reasoning depth on models that support effort controls; lower effort trades reasoning depth for latency and cost | | `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` | 95 | **50** | Compacts earlier, better quality in long sessions | | `ECC_CONTEXT_MONITOR_COST_WARNINGS` | on | **off for subscription users** | Suppresses agent-facing API-rate estimate warnings while keeping context/scope/loop warnings | diff --git a/docs/token-optimization.md b/docs/token-optimization.md index f3bf9259a..b6e00520e 100644 --- a/docs/token-optimization.md +++ b/docs/token-optimization.md @@ -22,12 +22,17 @@ Add to your `~/.claude/settings.json`: } ``` +The effort setting requires a model that supports effort controls. On Amazon Bedrock, +Google Cloud's Agent Platform, and Microsoft Foundry, the `sonnet` alias currently +resolves to Sonnet 4.5, which does not support effort controls. Select an effort-capable +model available from your provider, or omit `CLAUDE_CODE_EFFORT_LEVEL`. + ### What each setting does | Setting | Default | Recommended | Effect | |---------|---------|-------------|--------| | `model` | opus | **sonnet** | Sonnet handles ~80% of coding tasks well. Switch to Opus with `/model opus` for complex reasoning. ~60% cost reduction. | -| `CLAUDE_CODE_EFFORT_LEVEL` | Model-dependent | **medium** | Sets adaptive reasoning effort. Adjust it for task complexity; supported levels depend on the model. | +| `CLAUDE_CODE_EFFORT_LEVEL` | Model-dependent | **medium**, if supported | Sets adaptive reasoning effort on models that support effort controls. Adjust it for task complexity; supported levels depend on the model. | | `CLAUDE_CODE_SUBAGENT_MODEL` | _(inherits main)_ | **haiku** | Subagents (Task tool) run on this model. Haiku is ~80% cheaper and sufficient for exploration, file reading, and test running. | | `ECC_CONTEXT_MONITOR_COST_WARNINGS` | on | **off for subscription users** | Suppresses agent-facing API-rate estimate warnings while keeping context exhaustion, scope, and loop warnings. | @@ -154,7 +159,7 @@ The `configure-ecc` install wizard could offer to set these environment variable /cost # Check spending # Environment variables (add to ~/.claude/settings.json "env" block) -CLAUDE_CODE_EFFORT_LEVEL=medium +CLAUDE_CODE_EFFORT_LEVEL=medium # Only for models that support effort controls CLAUDE_CODE_SUBAGENT_MODEL=haiku CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 ``` diff --git a/rules/common/performance.md b/rules/common/performance.md index e8daa4174..db80f04d5 100644 --- a/rules/common/performance.md +++ b/rules/common/performance.md @@ -39,7 +39,7 @@ rather than a fixed token budget. Available levels depend on the model. Control thinking via: - **Effort**: Use `/effort` in a session, `--effort ` at launch, or `CLAUDE_CODE_EFFORT_LEVEL`. Save a per-model level in `modelSettings` in `~/.claude/settings.json`; the older user-level `effortLevel` key does not apply to Opus 5.5 or newer models. - **Toggle**: Option+T (macOS) / Alt+T (Windows/Linux), or `alwaysThinkingEnabled` in settings. These cannot disable thinking on Opus 5.5, Sonnet 5.5, or Fable models. -- **Fixed budget**: A positive `MAX_THINKING_TOKENS` does not cap adaptive reasoning. On Opus 4.6 or Sonnet 4.6, set `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` to use a fixed budget with `MAX_THINKING_TOKENS`. +- **Fixed budget**: Claude Code ignores positive `MAX_THINKING_TOKENS` values on adaptive reasoning models. On Opus 4.6 or Sonnet 4.6, set `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` to use a fixed budget with `MAX_THINKING_TOKENS`. - **Verbose mode**: Ctrl+O to see thinking output See [Claude Code model configuration](https://code.claude.com/docs/en/model-config#adjust-effort-level) From f65a969745567a89376f7ba7cd3c237cdbdad31f Mon Sep 17 00:00:00 2001 From: aiscimi-code Date: Wed, 7 Oct 2026 13:46:43 +0100 Subject: [PATCH 12/39] feat(skills): add brownfield-retrospective workflow for mid-project ECC adoption --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .gemini/GEMINI.md | 2 +- AGENTS.md | 4 +- README.md | 4 +- README.zh-CN.md | 2 +- SOUL.md | 2 +- docs/zh-CN/AGENTS.md | 4 +- docs/zh-CN/README.md | 6 +- manifests/install-modules.json | 1 + skills/brownfield-retrospective/SKILL.md | 176 +++++++++++++++++++++++ 11 files changed, 191 insertions(+), 14 deletions(-) create mode 100644 skills/brownfield-retrospective/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 0d83b4a00..266ad2574 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ { "name": "ecc", "source": "./", - "description": "Harness-native ECC operator layer - 68 agents, 293 skills, 94 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses", + "description": "Harness-native ECC operator layer - 68 agents, 294 skills, 94 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses", "version": "2.2.3", "author": { "name": "Affaan Mustafa", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 64d1f8ede..d1eb21374 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ecc", "version": "2.2.3", - "description": "Harness-native ECC plugin for engineering teams - 68 agents, 293 skills, 94 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses", + "description": "Harness-native ECC plugin for engineering teams - 68 agents, 294 skills, 94 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/.gemini/GEMINI.md b/.gemini/GEMINI.md index 42ada430b..544993ad6 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -4,7 +4,7 @@ This file provides Gemini CLI with the baseline ECC workflow, review standards, ## Overview -ECC is a cross-harness coding system with 68 specialized agents, 293 skills, and 94 commands. +ECC is a cross-harness coding system with 68 specialized agents, 294 skills, and 94 commands. Gemini support is currently focused on a strong project-local instruction layer via `.gemini/GEMINI.md`, plus the shared MCP catalog and package-manager setup assets shipped by the installer. diff --git a/AGENTS.md b/AGENTS.md index e2e6847c1..f4a0f616d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # ECC — Agent Instructions -This is a **production-ready AI coding plugin** providing 68 specialized agents, 293 skills, 94 commands, and automated hook workflows for software development. +This is a **production-ready AI coding plugin** providing 68 specialized agents, 294 skills, 94 commands, and automated hook workflows for software development. **Version:** 2.2.3 @@ -201,7 +201,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat ``` agents/ — 68 specialized subagents -skills/ — 293 workflow skills and domain knowledge +skills/ — 294 workflow skills and domain knowledge commands/ — 94 slash commands hooks/ — Trigger-based automations rules/ — Always-follow guidelines (common + per-language) diff --git a/README.md b/README.md index 95673de4b..403910d82 100644 --- a/README.md +++ b/README.md @@ -139,12 +139,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, 293 skills, and 94 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, 294 skills, and 94 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 | 293 skills | TDD, research, security, docs, frontend, data, ML, operations, and more | +| Skills | 294 skills | TDD, research, security, docs, frontend, data, ML, operations, and more | | Commands | 94 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 | diff --git a/README.zh-CN.md b/README.zh-CN.md index c098f5f41..a896430b3 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -200,7 +200,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**完成!** 你现在可以使用 68 个代理、293 个技能和 94 个命令。 +**完成!** 你现在可以使用 68 个代理、294 个技能和 94 个命令。 ### multi-* 命令需要额外配置 diff --git a/SOUL.md b/SOUL.md index ccec20351..92688e1b1 100644 --- a/SOUL.md +++ b/SOUL.md @@ -1,7 +1,7 @@ # Soul ## Core Identity -ECC is a production-ready AI coding plugin with 68 specialized agents, 293 skills, 94 commands, and automated hook workflows for software development. +ECC is a production-ready AI coding plugin with 68 specialized agents, 294 skills, 94 commands, 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/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md index 781079a19..8bcad7767 100644 --- a/docs/zh-CN/AGENTS.md +++ b/docs/zh-CN/AGENTS.md @@ -1,6 +1,6 @@ # ECC — 智能体指令 -这是一个**生产就绪的 AI 编码插件**,提供 68 个专业代理、293 项技能、94 条命令以及自动化钩子工作流,用于软件开发。 +这是一个**生产就绪的 AI 编码插件**,提供 68 个专业代理、294 项技能、94 条命令以及自动化钩子工作流,用于软件开发。 **版本:** 2.2.3 @@ -147,7 +147,7 @@ ``` agents/ — 68 个专业子代理 -skills/ — 293 个工作流技能和领域知识 +skills/ — 294 个工作流技能和领域知识 commands/ — 94 个斜杠命令 hooks/ — 基于触发的自动化 rules/ — 始终遵循的指导方针(通用 + 每种语言) diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 4e64b2389..d9c221e57 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -264,7 +264,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**搞定!** 你现在可以使用 68 个智能体、293 项技能和 94 个命令了。 +**搞定!** 你现在可以使用 68 个智能体、294 项技能和 94 个命令了。 *** @@ -1178,7 +1178,7 @@ opencode |---------|---------------|----------|--------| | 智能体 | PASS: 68 个 | PASS: 12 个 | **Claude Code 领先** | | 命令 | PASS: 94 个 | PASS: 35 个 | **Claude Code 领先** | -| 技能 | PASS: 293 项 | PASS: 37 项 | **Claude Code 领先** | +| 技能 | PASS: 294 项 | PASS: 37 项 | **Claude Code 领先** | | 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** | | 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** | | MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** | @@ -1286,7 +1286,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以 |---------|-----------------------|------------|-----------|----------| | **智能体** | 68 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | | **命令** | 94 | 共享 | 基于指令 | 35 | -| **技能** | 293 | 共享 | 10 (原生格式) | 37 | +| **技能** | 294 | 共享 | 10 (原生格式) | 37 | | **钩子事件** | 8 种类型 | 15 种类型 | SessionStart(1 种类型) | 11 种类型 | | **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | 1 个 SessionStart 引导脚本 | 插件钩子 | | **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 | diff --git a/manifests/install-modules.json b/manifests/install-modules.json index b61d4e20f..5f4310145 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -342,6 +342,7 @@ "skills/windows-desktop-e2e", "skills/agent-self-evaluation", "skills/architecture-decision-records", + "skills/brownfield-retrospective", "skills/browser-qa", "skills/ck", "skills/click-path-audit", diff --git a/skills/brownfield-retrospective/SKILL.md b/skills/brownfield-retrospective/SKILL.md new file mode 100644 index 000000000..ab84f974a --- /dev/null +++ b/skills/brownfield-retrospective/SKILL.md @@ -0,0 +1,176 @@ +--- +name: brownfield-retrospective +description: Run the ECC engineering process retroactively against an existing project slice. Audit current state, reconstruct past decisions, produce a four-axis gap register (thinking, coding practice, missing features, technology selection), then execute fixes forward through the plan-TDD-review pipeline. Use when adopting ECC mid-project, taking over an inherited codebase, or asking what was missed and what would have been built differently from day one. +metadata: + origin: ECC +--- + +# Brownfield Retrospective + +Run ECC's engineering process retroactively against a project that was built without it. Pick any slice of an existing codebase, audit what exists, reconstruct how it got there, extract a four-axis gap register, then execute the fixes forward through the standard ECC pipeline. + +The workflow is slice-first: no whole-repo onboarding is required before it is useful, and every phase is re-enterable from a durable artifact on disk. Start in the middle, work backwards, then move forward. + +## When to Activate + +- Adopting ECC in a repository that already exists and is already shaped +- User asks to "audit my existing project", "retro this codebase", or "what did we miss?" +- Taking over, inheriting, or returning to a codebase after a long gap +- User asks "why is it like this?" followed by "should it stay like this?" +- A module, feature, or service needs a rethink before the next feature lands on top of it +- Deciding whether a legacy area should be fixed forward, rewritten, or left documented as-is + +## When Not to Use + +- Greenfield work with no existing code — use `planner` and `tdd-workflow` directly +- Onboarding to spec-driven development without the redesign loop — the `spec-miner` agent does that on its own +- A launch go/no-go production-readiness pass — use `production-audit` +- Security-only sweeps — use `security-review` +- Team process retrospectives (sprint ceremonies) — this audits a codebase, not people + +## The Working Set + +All artifacts are written inside the audited project, under `docs/retro//`: + +| File | Phase | Purpose | +|------|-------|---------| +| `gap-register.md` | 0-6 | The state machine. The only file required to resume mid-stream. | +| `baseline-audit.md` | 1 | Reviewer findings, test/CI snapshot, TODO inventory. | +| `reconstructed-spec.md` | 2 | What the code actually does today (Requirements + Invariants). | +| `target-design.md` | 4 | Target shape for the slice and the fix sequence. | + +Backfilled decisions go to `docs/adr/` per the `architecture-decision-records` skill. If the project uses OpenSpec, `reconstructed-spec.md` may instead be `openspec/specs//spec.md` produced by `spec-miner`. + +## How It Works + +### Phase 0 — Pick the Entry Point + +Start from what the user cares about today. Do not map the whole repo first. + +Any of these is a valid slice: + +| User signal | Slice | +|-------------|-------| +| Names a directory, module, or service | That path plus its direct callers | +| Names a feature or user flow | The files that implement it, traced from the entry point | +| Names a bug, incident, or pain point | The code that produced it plus one level of call chain | +| Names a dependency or tech choice | Everything that touches it | +| Names nothing | The 2-3 hottest paths by churn — propose them and let the user pick | + +Record the perimeter at the top of `gap-register.md` before anything else: paths in scope, entry points, callers, blast radius. Budget the slice at roughly 15 files or 2,000 lines; if the user's pick is bigger, cut it into named sub-slices and register each as its own row of work. A slice too big to audit in one session is a slice that never finishes. + +### Phase 1 — Present-State Audit + +Establish what exists now, with evidence. Do not fix anything yet. + +1. Recon the slice: manifest and framework fingerprint, entry points, test layout, CI checks (borrow the recon steps from `codebase-onboarding`, scoped to the perimeter). +2. Run the reviewers that match the stack, in parallel where possible. Every finding must carry a `file:line` pointer: + - The language reviewer for the stack (`typescript-reviewer`, `python-reviewer`, `go-reviewer`, ...) + - `security-reviewer` + - `silent-failure-hunter` for swallowed errors + - `type-design-analyzer` where invariants are encoded in types + - `performance-optimizer` only if the slice is on a hot path +3. Snapshot the current state: test count and coverage, lint/CI status, `TODO`/`FIXME`/`HACK` inventory inside the perimeter. + +Write everything to `baseline-audit.md`. Findings that get fixed during this phase corrupt the baseline — hold them for the register. + +### Phase 2 — Backward Reconstruction + +Work backwards from the present state to the decisions that produced it. + +1. Mine the behavioral spec: run `spec-miner` against the slice. Its Requirements (WHEN → THEN) and Invariants become `reconstructed-spec.md` — the baseline truth for Phase 5 tests. +2. Git archaeology on the perimeter: + - `git log --follow --oneline -- ` — age, authors, cadence + - Largest and most reverted commits — where the design changed under pressure + - Churn hotspots — files changed far more often than neighbors + - Dead ends — branches or scaffolding that never shipped +3. Decision inventory: list the significant decisions frozen into the code (stack and framework choices, data model, boundaries, error strategy, state management). Check each against `docs/adr/`, README, and comments. Every decision with no written record is itself a pre-existing thinking gap — record it in Phase 3, do not silently backfill it here. +4. Where behavior is opaque, use `code-explorer` to trace execution paths before guessing at intent. + +### Phase 3 — Gap Analysis (Four Axes) + +Convert the audit and reconstruction into register rows. One axis per class of gap: + +| Axis | Question | Sources | +|------|-----------|---------| +| Thinking | What was decided but never recorded? Which invariants live only in code? | Decision inventory, reconstructed spec | +| Coding practice | Where does the code diverge from how ECC would have written it? | Phase 1 findings, the `rules/` pack for the language, spec items with no test anchor | +| Missing features | What would a day-one build have included that this slice never got? | Boundary input validation, observability, rate limiting, idempotency, migrations plus rollback, a11y, i18n, user docs, CI gates | +| Technology selection | Is the chosen tech fighting the codebase? Outdated, duplicated, risky, or missing? | Dependency inventory, git archaeology, recon | + +Register format (the table is the skill's durable output): + +```markdown +| ID | Axis | Gap | Evidence | Forward fix | Severity | Effort | Status | +|----|------|-----|----------|-------------|----------|--------|--------| +| G-001 | practice | Errors swallowed in retry loop | src/api/retry.ts:42 | Propagate + map at boundary | P1 | S | open | +``` + +- Severity: `P0` security exposure or data loss, `P1` correctness, `P2` practice and observability, `P3` selection and cleanup. +- Effort: `S` under a session, `M` a few sessions, `L` needs its own plan. +- Status: `open`, `in-progress`, `done`, `wont-fix`. +- Every row needs an evidence pointer. A gap you cannot point at is an opinion. + +### Phase 4 — Redesign / Rethink + +Turn the register into a sequenced plan. + +1. Run `architect` or `code-architect` over the `P0` and `P1` rows to produce `target-design.md`: target shape for the slice, the fix sequence, and an explicit "what does not change" list to protect working behavior. +2. Backfill retro-ADRs for every undocumented decision from Phase 2. Mark them as reconstructed (`status: accepted (reconstructed)`, date of the retro, note the original decision predates the record). Never present a backfilled ADR as if it were written at decision time. +3. Triage every register row into one of: `fix-forward` (repair in place), `rewrite` (replace the slice), `leave-documented` (wont-fix). A `wont-fix` must record its reason in the register — a wont-fix with a reason closes the thinking gap; a silent one reopens it. +4. Sequence the backlog: `P0` first, then interleave quick wins with larger repairs to keep momentum. + +### Phase 5 — Forward Execution + +Work the register through the standard ECC pipeline, one row per session as a baseline pace: + +```text +planner (scope the row) + → tdd-guide (failing test first — for a legacy gap the first test + usually encodes the missing invariant from reconstructed-spec.md) + → implement + → code-reviewer + → security-reviewer (if the row touches input, auth, or data) + → doc-updater + ADR (if the decision changed) + → conventional commit + → mark the register row done +``` + +- Update `gap-register.md` statuses as rows close; the register is the durable state, not the conversation. +- On session boundaries, hand off through the project's memory mechanism (`unified-memory`, context carriers) so the next session resumes from the register alone. + +### Phase 6 — Closure and Re-Audit + +When every row is `done` or `wont-fix`: + +1. Re-run the Phase 1 audit over the same perimeter. +2. Diff against `baseline-audit.md` and verify with `verification-loop` — the gaps claimed closed must stay closed. +3. Record the delta (findings then, findings now, rows closed, rows deferred) at the bottom of `gap-register.md`. +4. Anything that resurfaced becomes a new row, not a failure — the register is a loop, not a one-shot. + +## Resuming Mid-Stream + +Any session can start in the middle. Read `docs/retro//gap-register.md` first: + +- Register exists with a row `in-progress` → Phase 5, finish that row. +- Register exists, all rows closed → Phase 6. +- No register, but `baseline-audit.md` exists → Phase 2. +- No artifacts at all → Phase 0 with the user. + +## Anti-Patterns + +- Auditing the whole repo before any slice — the workflow is slice-first by design. +- Fixing code during Phases 1-3 — audit phases produce the register; edits mid-audit corrupt the baseline. +- Register rows without evidence pointers. +- `wont-fix` without a recorded reason. +- Presenting backfilled ADRs as contemporaneous records. +- Skipping the Phase 6 re-audit — closure is verified, not declared. + +## Related Skills + +- `codebase-onboarding` — recon baseline for Phase 1 +- `architecture-decision-records` — retro-ADR backfill in Phase 4 +- `tdd-workflow` — Phase 5 execution discipline +- `verification-loop` — Phase 6 closure +- `production-audit` — launch-readiness lens (complementary, later) +- `security-review` — security-only requests From ad86a8d972e44a611140bbbd6213bee0078b41e8 Mon Sep 17 00:00:00 2001 From: aiscimi-code Date: Wed, 7 Oct 2026 13:55:25 +0100 Subject: [PATCH 13/39] fix(skills): track current phase in gap register for mid-stream resume --- skills/brownfield-retrospective/SKILL.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skills/brownfield-retrospective/SKILL.md b/skills/brownfield-retrospective/SKILL.md index ab84f974a..f37690ded 100644 --- a/skills/brownfield-retrospective/SKILL.md +++ b/skills/brownfield-retrospective/SKILL.md @@ -34,7 +34,7 @@ All artifacts are written inside the audited project, under `docs/retro// | File | Phase | Purpose | |------|-------|---------| -| `gap-register.md` | 0-6 | The state machine. The only file required to resume mid-stream. | +| `gap-register.md` | 0-6 | The state machine — perimeter, `Current phase:` marker, and rows. The only file required to resume mid-stream. | | `baseline-audit.md` | 1 | Reviewer findings, test/CI snapshot, TODO inventory. | | `reconstructed-spec.md` | 2 | What the code actually does today (Requirements + Invariants). | | `target-design.md` | 4 | Target shape for the slice and the fix sequence. | @@ -57,7 +57,7 @@ Any of these is a valid slice: | Names a dependency or tech choice | Everything that touches it | | Names nothing | The 2-3 hottest paths by churn — propose them and let the user pick | -Record the perimeter at the top of `gap-register.md` before anything else: paths in scope, entry points, callers, blast radius. Budget the slice at roughly 15 files or 2,000 lines; if the user's pick is bigger, cut it into named sub-slices and register each as its own row of work. A slice too big to audit in one session is a slice that never finishes. +Record the perimeter at the top of `gap-register.md` before anything else: paths in scope, entry points, callers, blast radius. Budget the slice at roughly 15 files or 2,000 lines; if the user's pick is bigger, cut it into named sub-slices and register each as its own row of work. A slice too big to audit in one session is a slice that never finishes. Keep a `Current phase:` marker beside the perimeter and advance it each time a phase completes. ### Phase 1 — Present-State Audit @@ -153,7 +153,8 @@ When every row is `done` or `wont-fix`: Any session can start in the middle. Read `docs/retro//gap-register.md` first: - Register exists with a row `in-progress` → Phase 5, finish that row. -- Register exists, all rows closed → Phase 6. +- Register exists with at least one row, all rows closed → Phase 6. +- Register exists, rows open, none in progress → resume at the `Current phase:` marker (Phase 3 register awaiting triage, or Phase 4 backlog awaiting its first row). - No register, but `baseline-audit.md` exists → Phase 2. - No artifacts at all → Phase 0 with the user. From 9c21aaa681adc68e9ccc1059f1f42afdf7620b23 Mon Sep 17 00:00:00 2001 From: David He <1282474+beetz12@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:07:00 -0400 Subject: [PATCH 14/39] fix(skills): set pipefail before piped gates in verification-loop Phase 1 pipes the build into `tail -20`. Without pipefail the pipeline's exit status is tail's, so a failed build exits 0. Phase 2 already sets pipefail, but only after Phase 1 has run, and each phase is its own code block. Set it at the top of the Phase 1, 3 and 4 blocks, which pipe a gate into head or tail. Mirrored in pi/core. Co-Authored-By: Claude Opus 5.5 --- pi/core/skills/verification-loop/SKILL.md | 3 +++ skills/verification-loop/SKILL.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/pi/core/skills/verification-loop/SKILL.md b/pi/core/skills/verification-loop/SKILL.md index d7eb1fa23..8437833d7 100644 --- a/pi/core/skills/verification-loop/SKILL.md +++ b/pi/core/skills/verification-loop/SKILL.md @@ -22,6 +22,7 @@ Invoke this skill: ### Phase 1: Build Verification ```bash +set -o pipefail # report the build's exit status, not tail's # Check if project builds npm run build 2>&1 | tail -20 # OR @@ -44,6 +45,7 @@ Report all type errors. Fix critical ones before continuing. ### Phase 3: Lint Check ```bash +set -o pipefail # JavaScript/TypeScript npm run lint 2>&1 | head -30 @@ -53,6 +55,7 @@ ruff check . 2>&1 | head -30 ### Phase 4: Test Suite ```bash +set -o pipefail # Run tests with coverage npm run test -- --coverage 2>&1 | tail -50 diff --git a/skills/verification-loop/SKILL.md b/skills/verification-loop/SKILL.md index d7eb1fa23..8437833d7 100644 --- a/skills/verification-loop/SKILL.md +++ b/skills/verification-loop/SKILL.md @@ -22,6 +22,7 @@ Invoke this skill: ### Phase 1: Build Verification ```bash +set -o pipefail # report the build's exit status, not tail's # Check if project builds npm run build 2>&1 | tail -20 # OR @@ -44,6 +45,7 @@ Report all type errors. Fix critical ones before continuing. ### Phase 3: Lint Check ```bash +set -o pipefail # JavaScript/TypeScript npm run lint 2>&1 | head -30 @@ -53,6 +55,7 @@ ruff check . 2>&1 | head -30 ### Phase 4: Test Suite ```bash +set -o pipefail # Run tests with coverage npm run test -- --coverage 2>&1 | tail -50 From 722f2193cb8008e6b8ec572999095eebb37dbebb Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 15:30:58 +0000 Subject: [PATCH 15/39] test(carriers): pin runtime closure checks --- .../scripts/validate-carrier-closure.test.js | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 tests/scripts/validate-carrier-closure.test.js diff --git a/tests/scripts/validate-carrier-closure.test.js b/tests/scripts/validate-carrier-closure.test.js new file mode 100644 index 000000000..2aa0b22c5 --- /dev/null +++ b/tests/scripts/validate-carrier-closure.test.js @@ -0,0 +1,98 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const path = require('node:path'); +const childProcess = require('node:child_process'); +const test = require('node:test'); +const { withFixture, write } = require('../lib/helpers/context-fixture'); + +const REPO_ROOT = path.resolve(__dirname, '../..'); +const SCRIPT_PATH = path.join(REPO_ROOT, 'scripts/ci/validate-carrier-closure.js'); + +function validator() { + return require(SCRIPT_PATH); +} + +function plantSkillScript(root, source) { + write(root, 'skills/ecc-guide/scripts/tool.js', source); +} + +function specifiersOf(failures) { + return failures.map(failure => failure.specifier); +} + +test('the real registry closes: every planned script resolves inside its carrier', () => { + const result = validator().validate(); + assert.equal(result.status, 'success'); + assert.deepEqual(result.failures, []); + assert.equal(result.layouts.length, 5); + assert.deepEqual(result.profiles, ['lean@1', 'full@1']); + assert.ok(result.scriptCount > 0, 'expected planned JavaScript files to inspect'); + assert.ok(result.resolvedCount > 0, 'expected at least one resolved relative specifier'); + assert.ok(result.plannedFileCount > result.scriptCount); +}); + +test('a planted require that leaves the carrier tree fails with its specifier', () => withFixture(root => { + const escape = '../../../../../scripts/lib/context-profile-support'; + plantSkillScript(root, `'use strict';\n\nconst support = require('${escape}');\n\nmodule.exports = { support };\n`); + const result = validator().validate(root); + assert.equal(result.status, 'failure'); + assert.ok(specifiersOf(result.failures).includes(escape)); + assert.ok(result.failures.some(failure => failure.reason === 'outside-carrier-tree')); + assert.ok(result.failures.every(failure => failure.file.endsWith('skills/ecc-guide/scripts/tool.js'))); +})); + +test('a relative require that stays inside the tree but is unplanned fails', () => withFixture(root => { + plantSkillScript(root, "'use strict';\n\nrequire('../../shared/helper');\n"); + const result = validator().validate(root); + assert.equal(result.status, 'failure'); + assert.ok(specifiersOf(result.failures).includes('../../shared/helper')); + assert.ok(result.failures.some(failure => failure.reason === 'unplanned-target')); +})); + +test('a dynamic require warns without failing', () => withFixture(root => { + plantSkillScript(root, "'use strict';\n\nmodule.exports = require(process.env.ECC_PLUGIN_PATH);\n"); + const result = validator().validate(root); + assert.equal(result.status, 'success'); + assert.deepEqual(result.failures, []); + assert.ok(result.warnings.length > 0); + assert.ok(result.warnings.every(warning => warning.expression === 'process.env.ECC_PLUGIN_PATH')); + assert.ok(result.warnings.every(warning => warning.file.endsWith('skills/ecc-guide/scripts/tool.js'))); + assert.equal(result.dynamicCount, result.warnings.length); +})); + +test('a require shape quoted inside a string literal is not a dependency', () => withFixture(root => { + plantSkillScript(root, "'use strict';\n\nconst snippet = \"require('../../../../../etc/passwd')\";\n\nmodule.exports = { snippet };\n"); + const result = validator().validate(root); + assert.equal(result.status, 'success'); + assert.deepEqual(result.warnings, []); +})); + +test('--json emits a machine-readable receipt and exits zero on a clean registry', () => { + const result = childProcess.spawnSync(process.execPath, [SCRIPT_PATH, '--json'], { + cwd: REPO_ROOT, encoding: 'utf8', timeout: 120_000, + }); + assert.equal(result.status, 0, result.stderr); + const receipt = JSON.parse(result.stdout); + assert.equal(receipt.status, 'success'); + assert.equal(receipt.schemaVersion, 'ecc.carrier-closure.v1'); + assert.equal(receipt.projectionCount, receipt.layouts.length * receipt.profiles.length); + assert.deepEqual(receipt.failures, []); + assert.ok(Array.isArray(receipt.warnings)); + assert.ok(Number.isInteger(receipt.scriptCount)); + assert.equal(receipt.loadSmoke, 'deferred'); +}); + +test('unknown flags are rejected', () => { + const result = childProcess.spawnSync(process.execPath, [SCRIPT_PATH, '--write'], { + cwd: REPO_ROOT, encoding: 'utf8', timeout: 120_000, + }); + assert.equal(result.status, 1); + assert.match(result.stderr, /Unknown argument/); +}); + +test('the gate is registered in the normal test workflow', () => { + const { scripts } = require('../../package.json'); + assert.equal(scripts['carrier-closure:check'], 'node scripts/ci/validate-carrier-closure.js'); + assert.ok(scripts.test.includes('validate-carrier-closure.js')); +}); From 840a72b6c1420c10a5d30d0b72fc12527c598b59 Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 15:35:03 +0000 Subject: [PATCH 16/39] feat(carriers): validate carrier runtime closure in CI --- package.json | 3 +- scripts/ci/validate-carrier-closure.js | 281 +++++++++++++++++++++++++ 2 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 scripts/ci/validate-carrier-closure.js diff --git a/package.json b/package.json index 983370b3a..790087013 100644 --- a/package.json +++ b/package.json @@ -475,6 +475,7 @@ "welcome": "echo '\\n ecc-universal installed!\\n Run: ecc typescript\\n Compat: ecc-install typescript\\n Docs: https://github.com/affaan-m/ECC\\n Run or self-host any open-source model.\\n Compute: Itô is the preferred compute sponsor — https://compute.itomarkets.com\\n Any GPU provider works. This sponsorship link is passive: it does not invoke an RFQ, reserve capacity, provision compute, or configure serving.\\n 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.\\n Managed inference through Itô is not live yet.\\n'", "catalog:check": "node scripts/ci/catalog.js --text", "context-profiles:check": "node scripts/ci/validate-context-profiles.js", + "carrier-closure:check": "node scripts/ci/validate-carrier-closure.js", "catalog:sync": "node scripts/ci/catalog.js --write --text", "command-registry:generate": "node scripts/ci/generate-command-registry.js", "command-registry:write": "node scripts/ci/generate-command-registry.js --write", @@ -498,7 +499,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/check-hooks-schema-keys.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-context-profiles.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-context-profiles.js && node scripts/ci/validate-carrier-closure.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", diff --git a/scripts/ci/validate-carrier-closure.js b/scripts/ci/validate-carrier-closure.js new file mode 100644 index 000000000..8e0e7a995 --- /dev/null +++ b/scripts/ci/validate-carrier-closure.js @@ -0,0 +1,281 @@ +#!/usr/bin/env node +/** + * Carrier runtime-closure validator. + * + * Carriers are read-only file projections. Nothing here executes a planned + * script: every relative module specifier is extracted statically and checked + * against the planned file set of the carrier it would ship in. + * + * The static require-graph extraction technique is adapted from community + * PR #2788 (affaan-m/ECC). The staged load smoke from that work -- actually + * loading entry scripts from inside a materialized carrier -- is deliberately + * not ported here, and neither is command pruning. + */ + +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const { planContextCarrier } = require('../lib/context-carriers'); +const { loadContextRegistry } = require('../lib/context-pack-registry'); +const { DEFAULT_REPO_ROOT } = require('../lib/context-profile-support'); + +const PROFILES = Object.freeze(['lean@1', 'full@1']); +const SCRIPT_EXTENSIONS = new Set(['.js', '.cjs', '.mjs']); +const MARK = '\u0000'; +const SLOT = `${MARK}(\\d+)${MARK}`; +const CALL_PATTERN = new RegExp(`\\b(?:require|import)\\(\\s*${SLOT}\\s*\\)`, 'g'); +const FROM_PATTERN = new RegExp(`\\bfrom\\s*${SLOT}`, 'g'); +const SIDE_EFFECT_PATTERN = new RegExp(`\\bimport\\s*${SLOT}`, 'g'); +const DIRNAME_JOIN_PATTERN = new RegExp( + `\\brequire\\(\\s*path\\.join\\(\\s*__dirname\\s*((?:,\\s*${SLOT}\\s*)+)\\)\\s*\\)`, 'g'); +const DYNAMIC_PATTERN = /\b(?:require|import)\(\s*((?:[^()]|\([^()]*\))+)\)/g; +const STATIC_JOIN_PATTERN = new RegExp(`^path\\.join\\(\\s*__dirname\\s*(?:,\\s*${SLOT}\\s*)+\\)$`); +const SLOT_PATTERN = new RegExp(SLOT, 'g'); +const REGEX_PRECEDES = /(?:[([{,;:=!&|?+\-*/%~^<>]|\b(?:return|typeof|case|in|of|new|delete|void|instanceof|do|else|yield|await))$/; + +function readRegexLiteral(source, start) { + let index = start + 1; + let inClass = false; + while (index < source.length) { + const char = source[index]; + if (char === '\\') { index += 2; continue; } + if (char === '\n') return index; + if (char === '[') inClass = true; + else if (char === ']') inClass = false; + else if (char === '/' && !inClass) return index + 1; + index += 1; + } + return index; +} + +/** + * Reduce source to a code skeleton in which every comment is gone and every + * string literal is replaced by an indexed slot. A `require` shape written + * inside a comment, a string, or a regex therefore cannot be mistaken for a + * dependency, while `${...}` interpolations stay code. + */ +function scanSource(rawSource) { + const source = String(rawSource || ''); + const literals = []; + const stack = []; + let code = ''; + let index = 0; + const slot = value => { code += `${MARK}${literals.push(value) - 1}${MARK}`; }; + const frame = () => (stack.length ? stack[stack.length - 1] : null); + + while (index < source.length) { + const char = source[index]; + const next = source[index + 1]; + const open = frame(); + + if (open && open.type === 'template') { + if (char === '\\') { open.text += source.substr(index, 2); index += 2; continue; } + if (char === '`') { stack.pop(); slot(open.text); index += 1; continue; } + if (char === '$' && next === '{') { + slot(open.text); + open.text = ''; + stack.push({ type: 'interpolation', depth: 0 }); + index += 2; + continue; + } + open.text += char; + index += 1; + continue; + } + if (char === '/' && next === '/') { + while (index < source.length && source[index] !== '\n') index += 1; + continue; + } + if (char === '/' && next === '*') { + index = source.indexOf('*/', index + 2); + index = index === -1 ? source.length : index + 2; + continue; + } + if (char === '/' && REGEX_PRECEDES.test(code.trimEnd())) { + index = readRegexLiteral(source, index); + code += ' '; + continue; + } + if (char === '\'' || char === '"') { + let cursor = index + 1; + let value = ''; + while (cursor < source.length && source[cursor] !== char && source[cursor] !== '\n') { + if (source[cursor] === '\\') { value += source.substr(cursor, 2); cursor += 2; continue; } + value += source[cursor]; + cursor += 1; + } + slot(value); + index = source[cursor] === char ? cursor + 1 : cursor; + continue; + } + if (char === '`') { stack.push({ type: 'template', text: '' }); index += 1; continue; } + if (open && open.type === 'interpolation') { + if (char === '{') open.depth += 1; + else if (char === '}') { + if (open.depth === 0) { stack.pop(); index += 1; code += ' '; continue; } + open.depth -= 1; + } + } + code += char; + index += 1; + } + if (frame() && frame().type === 'template') slot(frame().text); + return { code, literals }; +} + +function collect(pattern, code, handler) { + pattern.lastIndex = 0; + let match = pattern.exec(code); + while (match !== null) { + handler(match); + match = pattern.exec(code); + } +} + +function renderExpression(text, literals) { + return text.replace(SLOT_PATTERN, (_whole, slotIndex) => `'${literals[Number(slotIndex)]}'`).trim(); +} + +/** + * Extract the relative module specifiers a script declares, plus the + * non-literal `require`/`import` arguments that cannot be resolved without + * running the code. + */ +function extractReferences(rawSource) { + const { code, literals } = scanSource(rawSource); + const specifiers = new Set(); + const dynamic = new Set(); + const relative = value => typeof value === 'string' && value.startsWith('.'); + const literalAt = match => literals[Number(match[1])]; + + for (const pattern of [CALL_PATTERN, FROM_PATTERN, SIDE_EFFECT_PATTERN]) { + collect(pattern, code, match => { + if (relative(literalAt(match))) specifiers.add(literalAt(match)); + }); + } + collect(DIRNAME_JOIN_PATTERN, code, match => { + const segments = []; + collect(SLOT_PATTERN, match[1], slotMatch => segments.push(literals[Number(slotMatch[1])])); + specifiers.add(`./${segments.join('/')}`); + }); + collect(DYNAMIC_PATTERN, code, match => { + const argument = match[1].trim(); + if (new RegExp(`^${SLOT}$`).test(argument) || STATIC_JOIN_PATTERN.test(argument)) return; + dynamic.add(renderExpression(argument, literals)); + }); + return { specifiers: [...specifiers], dynamic: [...dynamic] }; +} + +function candidateDestinations(destinationPath, specifier) { + const base = path.posix.join(path.posix.dirname(destinationPath), specifier); + if (base === '..' || base.startsWith('../')) return { base, candidates: null }; + const candidates = destinationPath.endsWith('.mjs') + ? [base] + : [base, `${base}.js`, `${base}.cjs`, `${base}.mjs`, `${base}.json`, `${base}/index.js`]; + return { base, candidates }; +} + +function sourceReader(repoRoot) { + const cache = new Map(); + return file => { + if (file.kind === 'generated') return file.content; + if (!cache.has(file.sourcePath)) { + cache.set(file.sourcePath, fs.readFileSync(path.join(repoRoot, ...file.sourcePath.split('/')), 'utf8')); + } + return cache.get(file.sourcePath); + }; +} + +function inspectProjection(carrier, profileId, read, report) { + const planned = new Set(carrier.files.map(file => file.destinationPath)); + const scripts = carrier.files.filter(file => SCRIPT_EXTENSIONS.has(path.posix.extname(file.destinationPath))); + for (const file of scripts) { + const { specifiers, dynamic } = extractReferences(read(file)); + const location = { + target: carrier.target, layout: carrier.layout.id, profileId, + file: file.destinationPath, source: file.sourcePath || null, + }; + for (const specifier of specifiers) { + report.specifierCount += 1; + const { base, candidates } = candidateDestinations(file.destinationPath, specifier); + if (!candidates) { + report.failures.push({ ...location, specifier, resolved: base, reason: 'outside-carrier-tree' }); + } else if (candidates.some(candidate => planned.has(candidate))) { + report.resolvedCount += 1; + } else { + report.failures.push({ ...location, specifier, resolved: base, reason: 'unplanned-target' }); + } + } + for (const expression of dynamic) { + report.dynamicCount += 1; + report.warnings.push({ ...location, expression }); + } + } + report.plannedFileCount += carrier.files.length; + report.scriptCount += scripts.length; +} + +function validate(repoRoot = DEFAULT_REPO_ROOT) { + const registry = loadContextRegistry({ repoRoot }); + const read = sourceReader(repoRoot); + const report = { + plannedFileCount: 0, scriptCount: 0, specifierCount: 0, resolvedCount: 0, dynamicCount: 0, + failures: [], warnings: [], + }; + const layouts = []; + const unsupportedTargets = []; + + for (const target of registry.targets) { + let supported = null; + for (const profileId of PROFILES) { + const carrier = planContextCarrier({ repoRoot, profileId, target }); + if (!carrier.layout) break; + supported = { target, layout: carrier.layout.id, skillRoot: carrier.layout.skillRoot }; + inspectProjection(carrier, profileId, read, report); + } + if (supported) layouts.push(supported); else unsupportedTargets.push(target); + } + + return { + schemaVersion: 'ecc.carrier-closure.v1', + status: report.failures.length ? 'failure' : 'success', + profiles: [...PROFILES], layouts, unsupportedTargets, + projectionCount: layouts.length * PROFILES.length, + registryDigest: registry.registryDigest, + loadSmoke: 'deferred', + lineage: 'static require-graph technique adapted from community PR #2788', + ...report, + }; +} + +function uniqueLines(entries, render) { + return [...new Set(entries.map(render))]; +} + +function main(args = process.argv.slice(2)) { + try { + for (const arg of args) { + if (arg !== '--json') throw new Error(`Unknown argument: ${arg}`); + } + const result = validate(); + if (args.includes('--json')) { + console.log(JSON.stringify(result, null, 2)); + } else { + for (const line of uniqueLines(result.warnings, w => `${w.file} -> ${w.expression}`)) { + console.warn(`Dynamic require, not resolvable statically: ${line}`); + } + for (const line of uniqueLines(result.failures, f => `${f.layout} ${f.profileId}: ${f.file} -> ${f.specifier} (${f.reason})`)) { + console.error(line); + } + console.log(`Carrier closure ${result.status}: ${result.layouts.length} layouts x ${result.profiles.length} profiles = ${result.projectionCount} projections, ${result.plannedFileCount} planned files, ${result.scriptCount} scripts, ${result.resolvedCount}/${result.specifierCount} relative specifiers resolved, ${result.failures.length} escapes, ${result.dynamicCount} dynamic requires. Load smoke: deferred.`); + } + return result.status === 'success' ? 0 : 1; + } catch (error) { + console.error(`Carrier closure validation failed: ${error.message}`); + return 1; + } +} + +if (require.main === module) process.exitCode = main(); +module.exports = { extractReferences, main, scanSource, validate }; From 4d8e3cfac0f903ebb1caad6a25c529f92e1310c7 Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 15:36:04 +0000 Subject: [PATCH 17/39] docs(carriers): record the runtime-closure gate slice --- docs/design/context-carriers.tdd.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/design/context-carriers.tdd.md b/docs/design/context-carriers.tdd.md index 8655fee37..2628773ef 100644 --- a/docs/design/context-carriers.tdd.md +++ b/docs/design/context-carriers.tdd.md @@ -76,3 +76,17 @@ Lower-priority review suggestions remain explicit follow-ups: failing projection ## Claims deliberately left unobserved Native discovery, exact native exclusions, invocation, executable-mode needs, workflow outcomes, activation, hook consent, rollback, automatic routing and actual token savings still require their own gates. Schema validation checks shape; it cannot certify supplied artifact semantics. The independently compiled fixture checks exact layout, selection, file set and bytes. It uses a trusted private temporary parent and does not certify an arbitrary-destination transaction writer against hostile concurrent mutation. No native provider, model, container or VM was launched, and no package was published. + +## October 8 static runtime-closure gate + +Date: October 8, 2026. `scripts/ci/validate-carrier-closure.js` now plans every supported carrier layout for both Lean and Full and checks that each planned JavaScript file's relative `require`/`import` specifiers resolve to a file the same carrier plans. The static require-graph extraction technique is adapted from community PR #2788; that work's staged load smoke, which materializes a carrier and actually loads entry scripts from inside it, is deliberately not ported, and neither is command pruning. Both wait for carrier scope to widen beyond skills. + +RED: `tests/scripts/validate-carrier-closure.test.js` added 8 `node:test` cases; all 8 failed, seven because the validator module was absent and the eighth because the npm gate was unregistered. GREEN: all 8 pass. The supported-layout list is derived by planning every registry target and keeping the ones that return a layout, so a new layout is picked up without editing the validator. + +Against the real registry the gate reports 5 supported layouts of 16 declared targets — `claude-plugin@1`, `codex-plugin@1`, `pi-package@1`, `opencode-project@1` and `cursor-project@1` — giving 10 projections and 2,941 planned files. 65 of those planned files are JavaScript: 13 per Full projection and none in Lean. They declare 35 relative specifiers, all 35 resolving inside their own carrier; every one is a sibling `./shared.mjs` import within `skills/ck`. The validator finds zero escapes and zero dynamic requires. + +Zero escapes is the expected result and not evidence the check is idle. Carriers are skill-only today, so no skill script has a reason to reach past its own directory. The gate exists so that the first command, agent, rule or hook admitted into carrier scope cannot silently ship a script reference that the projection does not contain. Detection is pinned by fixtures rather than by the live inventory: a planted skill script requiring `../../../../../scripts/lib/context-profile-support` fails as `outside-carrier-tree` in all five layouts and names the specifier, a planted sibling-skill reference fails as `unplanned-target`, a planted non-literal `require` warns without failing, and a `require` shape quoted inside a string literal produces neither. + +Limits. Nothing is executed; the check reads bytes and resolves paths. Bare specifiers are out of scope, so a skill script that depends on an npm package or a Node builtin is not examined for availability inside a carrier. Resolution assumes CommonJS extension candidates for `.js` and `.cjs` and exact specifiers for `.mjs`; it does not model conditional exports, a nested `package.json` type field, or import maps. A non-literal `require` is reported, not resolved, so a carrier can still fail at load time through a path computed at runtime. Native discovery, executable mode and actual loading remain unobserved, exactly as before. + +Focused verification: 22 carrier cases, 12 compiler cases, 8 closure cases, 3 CI cases and 2 publish-surface cases pass with zero failures or skips. ESLint passes on the validator and the new test. The validator is CI-only and stays out of the packed surface, matching `scripts/ci/validate-context-profiles.js`. From 3c8042fb33d2f622dc5b3e47a1ad2bb4f71c79cc Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 15:46:49 +0000 Subject: [PATCH 18/39] test(carriers): pin scanner edge cases and share one registry run --- .../scripts/validate-carrier-closure.test.js | 63 +++++++++++++------ 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/tests/scripts/validate-carrier-closure.test.js b/tests/scripts/validate-carrier-closure.test.js index 2aa0b22c5..61dfd07f4 100644 --- a/tests/scripts/validate-carrier-closure.test.js +++ b/tests/scripts/validate-carrier-closure.test.js @@ -13,6 +13,17 @@ function validator() { return require(SCRIPT_PATH); } +let realRun = null; +function realRegistryRun() { + if (!realRun) { + const result = childProcess.spawnSync(process.execPath, [SCRIPT_PATH, '--json'], { + cwd: REPO_ROOT, encoding: 'utf8', timeout: 120_000, + }); + realRun = { status: result.status, stderr: result.stderr, receipt: JSON.parse(result.stdout) }; + } + return realRun; +} + function plantSkillScript(root, source) { write(root, 'skills/ecc-guide/scripts/tool.js', source); } @@ -21,17 +32,6 @@ function specifiersOf(failures) { return failures.map(failure => failure.specifier); } -test('the real registry closes: every planned script resolves inside its carrier', () => { - const result = validator().validate(); - assert.equal(result.status, 'success'); - assert.deepEqual(result.failures, []); - assert.equal(result.layouts.length, 5); - assert.deepEqual(result.profiles, ['lean@1', 'full@1']); - assert.ok(result.scriptCount > 0, 'expected planned JavaScript files to inspect'); - assert.ok(result.resolvedCount > 0, 'expected at least one resolved relative specifier'); - assert.ok(result.plannedFileCount > result.scriptCount); -}); - test('a planted require that leaves the carrier tree fails with its specifier', () => withFixture(root => { const escape = '../../../../../scripts/lib/context-profile-support'; plantSkillScript(root, `'use strict';\n\nconst support = require('${escape}');\n\nmodule.exports = { support };\n`); @@ -69,20 +69,47 @@ test('a require shape quoted inside a string literal is not a dependency', () => })); test('--json emits a machine-readable receipt and exits zero on a clean registry', () => { - const result = childProcess.spawnSync(process.execPath, [SCRIPT_PATH, '--json'], { - cwd: REPO_ROOT, encoding: 'utf8', timeout: 120_000, - }); - assert.equal(result.status, 0, result.stderr); - const receipt = JSON.parse(result.stdout); - assert.equal(receipt.status, 'success'); + const { status, stderr, receipt } = realRegistryRun(); + assert.equal(status, 0, stderr); assert.equal(receipt.schemaVersion, 'ecc.carrier-closure.v1'); assert.equal(receipt.projectionCount, receipt.layouts.length * receipt.profiles.length); - assert.deepEqual(receipt.failures, []); assert.ok(Array.isArray(receipt.warnings)); assert.ok(Number.isInteger(receipt.scriptCount)); assert.equal(receipt.loadSmoke, 'deferred'); }); +test('the real registry closes: every planned script resolves inside its carrier', () => { + const { receipt } = realRegistryRun(); + assert.equal(receipt.status, 'success'); + assert.deepEqual(receipt.failures, []); + assert.equal(receipt.layouts.length, 5); + assert.deepEqual(receipt.profiles, ['lean@1', 'full@1']); + assert.ok(receipt.scriptCount > 0, 'expected planned JavaScript files to inspect'); + assert.ok(receipt.resolvedCount > 0, 'expected at least one resolved relative specifier'); + assert.ok(receipt.plannedFileCount > receipt.scriptCount); +}); + +const SCANNER_CASES = [ + ['division before a require', "const r = a / b; require('./after-division');", ['./after-division']], + ['regex holding a quote', "const re = /it's/; require('./after-regex');", ['./after-regex']], + ['regex holding a slash in a class', "const re = /[/]/g; require('./after-class');", ['./after-class']], + ['regex after return', "function f() { return /x/.test(y); } require('./after-return');", ['./after-return']], + ['division after a closing paren', "const v = (a) / 2 / 3; require('./after-paren');", ['./after-paren']], + ['object braces inside an interpolation', "const s = `a ${ {k: 1}.k } b`; require('./after-template');", ['./after-template']], + ['require text inside a template', "const s = `require('./fake')`;", []], + ['require inside an interpolation', "const s = `${require('./in-interpolation')}`;", ['./in-interpolation']], + ['block comment', "/* require('./commented') */ require('./real');", ['./real']], + ['multi-line import', "import {\n a,\n b\n} from\n './multi';", ['./multi']], + ['re-export', 'export * from "./reexport.js";', ['./reexport.js']], + ['path.join from __dirname', "require(path.join(__dirname, '..', 'lib', 'x.js'));", ['./../lib/x.js']], +]; + +for (const [label, source, expected] of SCANNER_CASES) { + test(`scanner: ${label}`, () => { + assert.deepEqual(validator().extractReferences(source).specifiers.sort(), [...expected].sort()); + }); +} + test('unknown flags are rejected', () => { const result = childProcess.spawnSync(process.execPath, [SCRIPT_PATH, '--write'], { cwd: REPO_ROOT, encoding: 'utf8', timeout: 120_000, From 5f7c2e9d7a8a9a83ffcd44e308cc2d47d5007430 Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 15:47:10 +0000 Subject: [PATCH 19/39] test(carriers): flag absolute and package imports a carrier cannot ship --- .../scripts/validate-carrier-closure.test.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/scripts/validate-carrier-closure.test.js b/tests/scripts/validate-carrier-closure.test.js index 61dfd07f4..df3747a00 100644 --- a/tests/scripts/validate-carrier-closure.test.js +++ b/tests/scripts/validate-carrier-closure.test.js @@ -56,11 +56,29 @@ test('a dynamic require warns without failing', () => withFixture(root => { assert.equal(result.status, 'success'); assert.deepEqual(result.failures, []); assert.ok(result.warnings.length > 0); - assert.ok(result.warnings.every(warning => warning.expression === 'process.env.ECC_PLUGIN_PATH')); + assert.ok(result.warnings.every(warning => warning.kind === 'dynamic' && warning.expression === 'process.env.ECC_PLUGIN_PATH')); assert.ok(result.warnings.every(warning => warning.file.endsWith('skills/ecc-guide/scripts/tool.js'))); assert.equal(result.dynamicCount, result.warnings.length); })); +test('an absolute require leaves the carrier tree and fails', () => withFixture(root => { + plantSkillScript(root, "'use strict';\n\nrequire('/opt/shared/helper');\n"); + const result = validator().validate(root); + assert.equal(result.status, 'failure'); + assert.ok(specifiersOf(result.failures).includes('/opt/shared/helper')); + assert.ok(result.failures.every(failure => failure.reason === 'outside-carrier-tree')); +})); + +test('a package import warns because carriers ship no node_modules; builtins do not', () => withFixture(root => { + plantSkillScript(root, "'use strict';\n\nconst yaml = require('js-yaml');\nconst { x } = require('@scope/pkg/deep');\n" + + "const fs = require('node:fs');\nconst path = require('path');\nmodule.exports = { yaml, x, fs, path };\n"); + const result = validator().validate(root); + assert.equal(result.status, 'success'); + const packages = result.warnings.filter(warning => warning.kind === 'package').map(warning => warning.package); + assert.deepEqual([...new Set(packages)].sort(), ['@scope/pkg', 'js-yaml']); + assert.equal(result.packageCount, packages.length); +})); + test('a require shape quoted inside a string literal is not a dependency', () => withFixture(root => { plantSkillScript(root, "'use strict';\n\nconst snippet = \"require('../../../../../etc/passwd')\";\n\nmodule.exports = { snippet };\n"); const result = validator().validate(root); From 6abf02bb499d57ccecb0edd81bec48ff1f17d98d Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 15:47:34 +0000 Subject: [PATCH 20/39] feat(carriers): flag absolute and package imports a carrier cannot ship --- docs/design/context-carriers.tdd.md | 10 ++++++--- scripts/ci/validate-carrier-closure.js | 31 ++++++++++++++++++-------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/design/context-carriers.tdd.md b/docs/design/context-carriers.tdd.md index 2628773ef..1f0d9d675 100644 --- a/docs/design/context-carriers.tdd.md +++ b/docs/design/context-carriers.tdd.md @@ -81,12 +81,16 @@ Native discovery, exact native exclusions, invocation, executable-mode needs, wo Date: October 8, 2026. `scripts/ci/validate-carrier-closure.js` now plans every supported carrier layout for both Lean and Full and checks that each planned JavaScript file's relative `require`/`import` specifiers resolve to a file the same carrier plans. The static require-graph extraction technique is adapted from community PR #2788; that work's staged load smoke, which materializes a carrier and actually loads entry scripts from inside it, is deliberately not ported, and neither is command pruning. Both wait for carrier scope to widen beyond skills. -RED: `tests/scripts/validate-carrier-closure.test.js` added 8 `node:test` cases; all 8 failed, seven because the validator module was absent and the eighth because the npm gate was unregistered. GREEN: all 8 pass. The supported-layout list is derived by planning every registry target and keeping the ones that return a layout, so a new layout is picked up without editing the validator. +RED: `tests/scripts/validate-carrier-closure.test.js` added 8 `node:test` cases; all 8 failed, seven because the validator module was absent and the eighth because the npm gate was unregistered. GREEN: all 8 pass. A second RED for absolute specifiers and package imports failed 3 cases (the absolute path was ignored, packages were ignored, warnings had no kind); GREEN passes them. The supported-layout list is derived by planning every registry target and keeping the ones that return a layout, so a new layout is picked up without editing the validator. Against the real registry the gate reports 5 supported layouts of 16 declared targets — `claude-plugin@1`, `codex-plugin@1`, `pi-package@1`, `opencode-project@1` and `cursor-project@1` — giving 10 projections and 2,941 planned files. 65 of those planned files are JavaScript: 13 per Full projection and none in Lean. They declare 35 relative specifiers, all 35 resolving inside their own carrier; every one is a sibling `./shared.mjs` import within `skills/ck`. The validator finds zero escapes and zero dynamic requires. Zero escapes is the expected result and not evidence the check is idle. Carriers are skill-only today, so no skill script has a reason to reach past its own directory. The gate exists so that the first command, agent, rule or hook admitted into carrier scope cannot silently ship a script reference that the projection does not contain. Detection is pinned by fixtures rather than by the live inventory: a planted skill script requiring `../../../../../scripts/lib/context-profile-support` fails as `outside-carrier-tree` in all five layouts and names the specifier, a planted sibling-skill reference fails as `unplanned-target`, a planted non-literal `require` warns without failing, and a `require` shape quoted inside a string literal produces neither. -Limits. Nothing is executed; the check reads bytes and resolves paths. Bare specifiers are out of scope, so a skill script that depends on an npm package or a Node builtin is not examined for availability inside a carrier. Resolution assumes CommonJS extension candidates for `.js` and `.cjs` and exact specifiers for `.mjs`; it does not model conditional exports, a nested `package.json` type field, or import maps. A non-literal `require` is reported, not resolved, so a carrier can still fail at load time through a path computed at runtime. Native discovery, executable mode and actual loading remain unobserved, exactly as before. +Absolute specifiers fail as `outside-carrier-tree`. A package import warns rather than fails, because carriers ship no `node_modules`; Node builtins, bare or `node:`-prefixed, are exempt. No skill script imports a package today. -Focused verification: 22 carrier cases, 12 compiler cases, 8 closure cases, 3 CI cases and 2 publish-surface cases pass with zero failures or skips. ESLint passes on the validator and the new test. The validator is CI-only and stays out of the packed surface, matching `scripts/ci/validate-context-profiles.js`. +Limits. Nothing is executed; the check reads bytes and resolves paths. A package warning names the dependency but cannot tell whether the host environment provides it. Resolution assumes CommonJS extension candidates for `.js` and `.cjs` and exact specifiers for `.mjs`; it does not model conditional exports, a nested `package.json` type field, or import maps. A non-literal `require` is reported, not resolved, so a carrier can still fail at load time through a path computed at runtime. Native discovery, executable mode and actual loading remain unobserved, exactly as before. + +The scanner is pinned by twelve edge cases (regex versus division, regex classes and quotes, template interpolations with object braces, block comments, multi-line and re-exported `from`, and `path.join(__dirname, …)`). Across all 655 JavaScript files in the repository it found every relative specifier a plain pattern match finds outside comments and strings, with no scanner errors. The suite validates the real registry once, through the CLI, and shares that receipt between its real-registry cases: 4.8 s per run instead of 9.1 s, which matters because the test job runs in about 33 matrix cells. + +Focused verification: 22 carrier cases, 12 compiler cases, 22 closure cases, 3 CI cases and 2 publish-surface cases pass with zero failures or skips. ESLint passes on the validator and the new test. The validator is CI-only and stays out of the packed surface, matching `scripts/ci/validate-context-profiles.js`. diff --git a/scripts/ci/validate-carrier-closure.js b/scripts/ci/validate-carrier-closure.js index 8e0e7a995..8738db360 100644 --- a/scripts/ci/validate-carrier-closure.js +++ b/scripts/ci/validate-carrier-closure.js @@ -16,11 +16,13 @@ const fs = require('node:fs'); const path = require('node:path'); +const { builtinModules } = require('node:module'); const { planContextCarrier } = require('../lib/context-carriers'); const { loadContextRegistry } = require('../lib/context-pack-registry'); const { DEFAULT_REPO_ROOT } = require('../lib/context-profile-support'); const PROFILES = Object.freeze(['lean@1', 'full@1']); +const BUILTINS = new Set(builtinModules); const SCRIPT_EXTENSIONS = new Set(['.js', '.cjs', '.mjs']); const MARK = '\u0000'; const SLOT = `${MARK}(\\d+)${MARK}`; @@ -145,13 +147,17 @@ function renderExpression(text, literals) { function extractReferences(rawSource) { const { code, literals } = scanSource(rawSource); const specifiers = new Set(); + const packages = new Set(); const dynamic = new Set(); - const relative = value => typeof value === 'string' && value.startsWith('.'); const literalAt = match => literals[Number(match[1])]; for (const pattern of [CALL_PATTERN, FROM_PATTERN, SIDE_EFFECT_PATTERN]) { collect(pattern, code, match => { - if (relative(literalAt(match))) specifiers.add(literalAt(match)); + const value = literalAt(match); + if (value.startsWith('.') || value.startsWith('/')) specifiers.add(value); + else if (!value.startsWith('node:') && !BUILTINS.has(value.split('/')[0])) { + packages.add(value.startsWith('@') ? value.split('/').slice(0, 2).join('/') : value.split('/')[0]); + } }); } collect(DIRNAME_JOIN_PATTERN, code, match => { @@ -164,10 +170,11 @@ function extractReferences(rawSource) { if (new RegExp(`^${SLOT}$`).test(argument) || STATIC_JOIN_PATTERN.test(argument)) return; dynamic.add(renderExpression(argument, literals)); }); - return { specifiers: [...specifiers], dynamic: [...dynamic] }; + return { specifiers: [...specifiers], packages: [...packages], dynamic: [...dynamic] }; } function candidateDestinations(destinationPath, specifier) { + if (specifier.startsWith('/')) return { base: specifier, candidates: null }; const base = path.posix.join(path.posix.dirname(destinationPath), specifier); if (base === '..' || base.startsWith('../')) return { base, candidates: null }; const candidates = destinationPath.endsWith('.mjs') @@ -191,7 +198,7 @@ function inspectProjection(carrier, profileId, read, report) { const planned = new Set(carrier.files.map(file => file.destinationPath)); const scripts = carrier.files.filter(file => SCRIPT_EXTENSIONS.has(path.posix.extname(file.destinationPath))); for (const file of scripts) { - const { specifiers, dynamic } = extractReferences(read(file)); + const { specifiers, packages, dynamic } = extractReferences(read(file)); const location = { target: carrier.target, layout: carrier.layout.id, profileId, file: file.destinationPath, source: file.sourcePath || null, @@ -209,7 +216,11 @@ function inspectProjection(carrier, profileId, read, report) { } for (const expression of dynamic) { report.dynamicCount += 1; - report.warnings.push({ ...location, expression }); + report.warnings.push({ ...location, kind: 'dynamic', expression }); + } + for (const name of packages) { + report.packageCount += 1; + report.warnings.push({ ...location, kind: 'package', package: name }); } } report.plannedFileCount += carrier.files.length; @@ -220,7 +231,7 @@ function validate(repoRoot = DEFAULT_REPO_ROOT) { const registry = loadContextRegistry({ repoRoot }); const read = sourceReader(repoRoot); const report = { - plannedFileCount: 0, scriptCount: 0, specifierCount: 0, resolvedCount: 0, dynamicCount: 0, + plannedFileCount: 0, scriptCount: 0, specifierCount: 0, resolvedCount: 0, dynamicCount: 0, packageCount: 0, failures: [], warnings: [], }; const layouts = []; @@ -262,13 +273,15 @@ function main(args = process.argv.slice(2)) { if (args.includes('--json')) { console.log(JSON.stringify(result, null, 2)); } else { - for (const line of uniqueLines(result.warnings, w => `${w.file} -> ${w.expression}`)) { - console.warn(`Dynamic require, not resolvable statically: ${line}`); + for (const line of uniqueLines(result.warnings, w => (w.kind === 'package' + ? `Package import, not shipped in carriers: ${w.file} -> ${w.package}` + : `Dynamic require, not resolvable statically: ${w.file} -> ${w.expression}`))) { + console.warn(line); } for (const line of uniqueLines(result.failures, f => `${f.layout} ${f.profileId}: ${f.file} -> ${f.specifier} (${f.reason})`)) { console.error(line); } - console.log(`Carrier closure ${result.status}: ${result.layouts.length} layouts x ${result.profiles.length} profiles = ${result.projectionCount} projections, ${result.plannedFileCount} planned files, ${result.scriptCount} scripts, ${result.resolvedCount}/${result.specifierCount} relative specifiers resolved, ${result.failures.length} escapes, ${result.dynamicCount} dynamic requires. Load smoke: deferred.`); + console.log(`Carrier closure ${result.status}: ${result.layouts.length} layouts x ${result.profiles.length} profiles = ${result.projectionCount} projections, ${result.plannedFileCount} planned files, ${result.scriptCount} scripts, ${result.resolvedCount}/${result.specifierCount} relative specifiers resolved, ${result.failures.length} escapes, ${result.dynamicCount} dynamic requires, ${result.packageCount} package imports. Load smoke: deferred.`); } return result.status === 'success' ? 0 : 1; } catch (error) { From 333e755f509240705d0886ead42a80a82e912317 Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 16:47:54 +0000 Subject: [PATCH 21/39] test(carriers): cover spaced calls, escaped specifiers, Windows paths and index.json --- .../scripts/validate-carrier-closure.test.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/scripts/validate-carrier-closure.test.js b/tests/scripts/validate-carrier-closure.test.js index df3747a00..a0ce842aa 100644 --- a/tests/scripts/validate-carrier-closure.test.js +++ b/tests/scripts/validate-carrier-closure.test.js @@ -69,6 +69,24 @@ test('an absolute require leaves the carrier tree and fails', () => withFixture( assert.ok(result.failures.every(failure => failure.reason === 'outside-carrier-tree')); })); +for (const specifier of ['C:/shared/helper', 'C:\\shared\\helper', '\\\\server\\share\\helper', 'file:///opt/helper.js']) { + test(`an absolute Windows or file URL require fails: ${specifier}`, () => withFixture(root => { + plantSkillScript(root, `'use strict';\n\nrequire(${JSON.stringify(specifier)});\n`); + const result = validator().validate(root); + assert.equal(result.status, 'failure'); + assert.ok(result.failures.length > 0); + assert.ok(result.failures.every(failure => failure.reason === 'outside-carrier-tree')); + assert.deepEqual(result.warnings, []); + })); +} + +test('a directory require resolves through index.json', () => withFixture(root => { + write(root, 'skills/ecc-guide/scripts/data/index.json', '{}\n'); + plantSkillScript(root, "'use strict';\n\nmodule.exports = require('./data');\n"); + const result = validator().validate(root); + assert.equal(result.status, 'success', JSON.stringify(result.failures)); +})); + test('a package import warns because carriers ship no node_modules; builtins do not', () => withFixture(root => { plantSkillScript(root, "'use strict';\n\nconst yaml = require('js-yaml');\nconst { x } = require('@scope/pkg/deep');\n" + "const fs = require('node:fs');\nconst path = require('path');\nmodule.exports = { yaml, x, fs, path };\n"); @@ -120,6 +138,12 @@ const SCANNER_CASES = [ ['multi-line import', "import {\n a,\n b\n} from\n './multi';", ['./multi']], ['re-export', 'export * from "./reexport.js";', ['./reexport.js']], ['path.join from __dirname', "require(path.join(__dirname, '..', 'lib', 'x.js'));", ['./../lib/x.js']], + ['whitespace before the call paren', "require ('./spaced'); require\n('./newline'); import ('./spaced-import');", + ['./spaced', './newline', './spaced-import']], + ['whitespace inside path.join from __dirname', "require (path.join (__dirname, 'lib', 'y.js'));", ['./lib/y.js']], + ['unicode escape in a specifier', "require('\\u002e./escaped');", ['../escaped']], + ['hex and braced escapes in a specifier', "require('\\x2e/a'); require(`\\u{2e}/b`);", ['./a', './b']], + ['escaped quote and backslash', "require('./it\\'s'); require('.\\\\win');", ["./it's", '.\\win']], ]; for (const [label, source, expected] of SCANNER_CASES) { From 0d3c85bd543baf25a5519cf74ebd7eada09982df Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 16:48:33 +0000 Subject: [PATCH 22/39] fix(carriers): match spaced calls, cook escapes, treat Windows and file URLs as absolute --- scripts/ci/validate-carrier-closure.js | 39 ++++++++++++++++++++------ 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/scripts/ci/validate-carrier-closure.js b/scripts/ci/validate-carrier-closure.js index 8738db360..13f898427 100644 --- a/scripts/ci/validate-carrier-closure.js +++ b/scripts/ci/validate-carrier-closure.js @@ -26,16 +26,37 @@ const BUILTINS = new Set(builtinModules); const SCRIPT_EXTENSIONS = new Set(['.js', '.cjs', '.mjs']); const MARK = '\u0000'; const SLOT = `${MARK}(\\d+)${MARK}`; -const CALL_PATTERN = new RegExp(`\\b(?:require|import)\\(\\s*${SLOT}\\s*\\)`, 'g'); +const CALL_PATTERN = new RegExp(`\\b(?:require|import)\\s*\\(\\s*${SLOT}\\s*\\)`, 'g'); const FROM_PATTERN = new RegExp(`\\bfrom\\s*${SLOT}`, 'g'); const SIDE_EFFECT_PATTERN = new RegExp(`\\bimport\\s*${SLOT}`, 'g'); const DIRNAME_JOIN_PATTERN = new RegExp( - `\\brequire\\(\\s*path\\.join\\(\\s*__dirname\\s*((?:,\\s*${SLOT}\\s*)+)\\)\\s*\\)`, 'g'); -const DYNAMIC_PATTERN = /\b(?:require|import)\(\s*((?:[^()]|\([^()]*\))+)\)/g; -const STATIC_JOIN_PATTERN = new RegExp(`^path\\.join\\(\\s*__dirname\\s*(?:,\\s*${SLOT}\\s*)+\\)$`); + `\\brequire\\s*\\(\\s*path\\.join\\s*\\(\\s*__dirname\\s*((?:,\\s*${SLOT}\\s*)+)\\)\\s*\\)`, 'g'); +const DYNAMIC_PATTERN = /\b(?:require|import)\s*\(\s*((?:[^()]|\([^()]*\))+)\)/g; +const STATIC_JOIN_PATTERN = new RegExp(`^path\\.join\\s*\\(\\s*__dirname\\s*(?:,\\s*${SLOT}\\s*)+\\)$`); const SLOT_PATTERN = new RegExp(SLOT, 'g'); +const ESCAPE_PATTERN = /\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2})|(\r\n|[\s\S]))/g; +const SIMPLE_ESCAPES = Object.freeze({ n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v', 0: '\0' }); +const LINE_CONTINUATIONS = new Set(['\n', '\r', '\r\n', '\u2028', '\u2029']); const REGEX_PRECEDES = /(?:[([{,;:=!&|?+\-*/%~^<>]|\b(?:return|typeof|case|in|of|new|delete|void|instanceof|do|else|yield|await))$/; +/** Cook a raw string or template chunk the way the JavaScript parser would. */ +function decodeEscapes(raw) { + return raw.replace(ESCAPE_PATTERN, (whole, braced, unicode, hex, other) => { + if (braced !== undefined) { + const codePoint = parseInt(braced, 16); + return codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : whole; + } + if (unicode !== undefined) return String.fromCharCode(parseInt(unicode, 16)); + if (hex !== undefined) return String.fromCharCode(parseInt(hex, 16)); + if (LINE_CONTINUATIONS.has(other)) return ''; + return SIMPLE_ESCAPES[other] ?? other; + }); +} + +function isAbsoluteSpecifier(value) { + return value.startsWith('/') || value.startsWith('\\\\') || /^[A-Za-z]:[\\/]/.test(value) || /^file:/i.test(value); +} + function readRegexLiteral(source, start) { let index = start + 1; let inClass = false; @@ -63,7 +84,7 @@ function scanSource(rawSource) { const stack = []; let code = ''; let index = 0; - const slot = value => { code += `${MARK}${literals.push(value) - 1}${MARK}`; }; + const slot = value => { code += `${MARK}${literals.push(decodeEscapes(value)) - 1}${MARK}`; }; const frame = () => (stack.length ? stack[stack.length - 1] : null); while (index < source.length) { @@ -154,7 +175,7 @@ function extractReferences(rawSource) { for (const pattern of [CALL_PATTERN, FROM_PATTERN, SIDE_EFFECT_PATTERN]) { collect(pattern, code, match => { const value = literalAt(match); - if (value.startsWith('.') || value.startsWith('/')) specifiers.add(value); + if (value.startsWith('.') || isAbsoluteSpecifier(value)) specifiers.add(value); else if (!value.startsWith('node:') && !BUILTINS.has(value.split('/')[0])) { packages.add(value.startsWith('@') ? value.split('/').slice(0, 2).join('/') : value.split('/')[0]); } @@ -174,12 +195,12 @@ function extractReferences(rawSource) { } function candidateDestinations(destinationPath, specifier) { - if (specifier.startsWith('/')) return { base: specifier, candidates: null }; - const base = path.posix.join(path.posix.dirname(destinationPath), specifier); + if (isAbsoluteSpecifier(specifier)) return { base: specifier, candidates: null }; + const base = path.posix.join(path.posix.dirname(destinationPath), specifier.replace(/\\/g, '/')); if (base === '..' || base.startsWith('../')) return { base, candidates: null }; const candidates = destinationPath.endsWith('.mjs') ? [base] - : [base, `${base}.js`, `${base}.cjs`, `${base}.mjs`, `${base}.json`, `${base}/index.js`]; + : [base, `${base}.js`, `${base}.cjs`, `${base}.mjs`, `${base}.json`, `${base}/index.js`, `${base}/index.json`]; return { base, candidates }; } From e85c529eaff159b23f826442dcd6d5997ce43808 Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 16:48:47 +0000 Subject: [PATCH 23/39] test(carriers): an interpolated from identifier is not an import --- tests/scripts/validate-carrier-closure.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/scripts/validate-carrier-closure.test.js b/tests/scripts/validate-carrier-closure.test.js index a0ce842aa..5b660914b 100644 --- a/tests/scripts/validate-carrier-closure.test.js +++ b/tests/scripts/validate-carrier-closure.test.js @@ -152,6 +152,11 @@ for (const [label, source, expected] of SCANNER_CASES) { }); } +test('scanner: an identifier named from before a template chunk is not an import', () => { + assert.deepEqual(validator().extractReferences('const s = `${from} is shipped as ${to}`;'), + { specifiers: [], packages: [], dynamic: [] }); +}); + test('unknown flags are rejected', () => { const result = childProcess.spawnSync(process.execPath, [SCRIPT_PATH, '--write'], { cwd: REPO_ROOT, encoding: 'utf8', timeout: 120_000, From d00b379f702264b4efd6887cf64f0dd00dab41b2 Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 16:49:08 +0000 Subject: [PATCH 24/39] fix(carriers): keep keyword patterns from spanning an interpolation --- scripts/ci/validate-carrier-closure.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/ci/validate-carrier-closure.js b/scripts/ci/validate-carrier-closure.js index 13f898427..6005aed05 100644 --- a/scripts/ci/validate-carrier-closure.js +++ b/scripts/ci/validate-carrier-closure.js @@ -34,6 +34,8 @@ const DIRNAME_JOIN_PATTERN = new RegExp( const DYNAMIC_PATTERN = /\b(?:require|import)\s*\(\s*((?:[^()]|\([^()]*\))+)\)/g; const STATIC_JOIN_PATTERN = new RegExp(`^path\\.join\\s*\\(\\s*__dirname\\s*(?:,\\s*${SLOT}\\s*)+\\)$`); const SLOT_PATTERN = new RegExp(SLOT, 'g'); +// Closes an interpolation; no keyword pattern can match across it. +const CLOSE = `${MARK}${MARK}`; const ESCAPE_PATTERN = /\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2})|(\r\n|[\s\S]))/g; const SIMPLE_ESCAPES = Object.freeze({ n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v', 0: '\0' }); const LINE_CONTINUATIONS = new Set(['\n', '\r', '\r\n', '\u2028', '\u2029']); @@ -136,7 +138,7 @@ function scanSource(rawSource) { if (open && open.type === 'interpolation') { if (char === '{') open.depth += 1; else if (char === '}') { - if (open.depth === 0) { stack.pop(); index += 1; code += ' '; continue; } + if (open.depth === 0) { stack.pop(); index += 1; code += CLOSE; continue; } open.depth -= 1; } } @@ -157,7 +159,7 @@ function collect(pattern, code, handler) { } function renderExpression(text, literals) { - return text.replace(SLOT_PATTERN, (_whole, slotIndex) => `'${literals[Number(slotIndex)]}'`).trim(); + return text.replace(SLOT_PATTERN, (_whole, slotIndex) => `'${literals[Number(slotIndex)]}'`).split(CLOSE).join(' ').trim(); } /** From aec97150d815ee4886b0c1a0dbb850c5b2b4829d Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 16:49:30 +0000 Subject: [PATCH 25/39] docs(carriers): record the closure review follow-up --- docs/design/context-carriers.tdd.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/design/context-carriers.tdd.md b/docs/design/context-carriers.tdd.md index 1f0d9d675..e3df49d81 100644 --- a/docs/design/context-carriers.tdd.md +++ b/docs/design/context-carriers.tdd.md @@ -93,4 +93,6 @@ Limits. Nothing is executed; the check reads bytes and resolves paths. A package The scanner is pinned by twelve edge cases (regex versus division, regex classes and quotes, template interpolations with object braces, block comments, multi-line and re-exported `from`, and `path.join(__dirname, …)`). Across all 655 JavaScript files in the repository it found every relative specifier a plain pattern match finds outside comments and strings, with no scanner errors. The suite validates the real registry once, through the CLI, and shares that receipt between its real-registry cases: 4.8 s per run instead of 9.1 s, which matters because the test job runs in about 33 matrix cells. -Focused verification: 22 carrier cases, 12 compiler cases, 22 closure cases, 3 CI cases and 2 publish-surface cases pass with zero failures or skips. ESLint passes on the validator and the new test. The validator is CI-only and stays out of the packed surface, matching `scripts/ci/validate-context-profiles.js`. +Review follow-up. A third RED added 10 cases from review: spaced calls (`require ('./x')`, `require\n(...)`, `import (...)`, and `path.join (__dirname, ...)`), escaped specifiers (`\u002e`, `\x2e`, `\u{2e}`, escaped quotes and backslashes), Windows drive, UNC and `file:` URL specifiers, and a directory resolving through `index.json`. All 10 failed. GREEN cooks string and template escapes as the parser does, allows whitespace before call parentheses, treats drive, UNC and `file:` specifiers as absolute, normalizes backslash separators in relative specifiers and adds the `index.json` candidate. A fourth RED/GREEN stops `from` and `import` patterns matching across a closed `${...}` interpolation. Rescanning all 802 repository JavaScript files against the previous scanner changed only three results, each a false package warning removed (`${from}` and similar identifiers before a template chunk). The real registry still reports 35/35 resolved, zero escapes, warnings or package imports, in 3.5 s. + +Focused verification: 22 carrier cases, 12 compiler cases, 33 closure cases, 3 CI cases and 2 publish-surface cases pass with zero failures or skips. ESLint passes on the validator and the new test. The validator is CI-only and stays out of the packed surface, matching `scripts/ci/validate-context-profiles.js`. From 6d15a1272eb77867d034a9be6c326e3c666f9e6d Mon Sep 17 00:00:00 2001 From: taljeri <318237806+taljeri@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:50:04 +0300 Subject: [PATCH 26/39] docs(i18n): add Arabic localization scout Add Arabic (ar) localization guide and glossary, register the locale:ar component and docs-ar module across install manifests, and wire test coverage. Contributed-by: @taljeri (taljeri) Signed-off-by: taljeri <318237806+taljeri@users.noreply.github.com> --- README.md | 1 + README.zh-CN.md | 2 +- docs/ar/GLOSSARY.md | 63 +++++++++ docs/ar/README.md | 174 +++++++++++++++++++++++++ docs/de-DE/README.md | 4 +- docs/es/README.md | 2 +- docs/ja-JP/README.md | 1 + docs/ko-KR/README.md | 2 +- docs/pl/README.md | 2 +- docs/pt-BR/README.md | 2 +- docs/ru/README.md | 2 +- docs/th/README.md | 4 +- docs/tr/README.md | 2 +- docs/uk-UA/README.md | 1 + docs/ur/README.md | 4 +- docs/vi-VN/README.md | 4 +- docs/zh-CN/README.md | 2 +- docs/zh-TW/README.md | 2 +- manifests/install-components.json | 10 +- manifests/install-modules.json | 18 ++- package.json | 1 + scripts/lib/install-manifests.js | 6 +- tests/docs/install-identifiers.test.js | 1 + tests/lib/locale-install.test.js | 86 ++++++++++++ 24 files changed, 375 insertions(+), 21 deletions(-) create mode 100644 docs/ar/GLOSSARY.md create mode 100644 docs/ar/README.md diff --git a/README.md b/README.md index 95673de4b..98b5f41ab 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@

Language: English | + العربية | Português (Brasil) | 简体中文 | 繁體中文 | diff --git a/README.zh-CN.md b/README.zh-CN.md index c098f5f41..594f6210e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -23,7 +23,7 @@ **Language / 语言 / 語言 / Dil / Язык / Ngôn ngữ** -[**English**](README.md) | [Português (Brasil)](docs/pt-BR/README.md) | [简体中文](README.zh-CN.md) | [繁體中文](docs/zh-TW/README.md) | [日本語](docs/ja-JP/README.md) | [한국어](docs/ko-KR/README.md) | [Türkçe](docs/tr/README.md) | [Русский](docs/ru/README.md) | [Tiếng Việt](docs/vi-VN/README.md) | [ไทย](docs/th/README.md) | [Deutsch](docs/de-DE/README.md) | [Polski](docs/pl/README.md) +[**English**](README.md) | [العربية](docs/ar/README.md) | [Português (Brasil)](docs/pt-BR/README.md) | [简体中文](README.zh-CN.md) | [繁體中文](docs/zh-TW/README.md) | [日本語](docs/ja-JP/README.md) | [한국어](docs/ko-KR/README.md) | [Türkçe](docs/tr/README.md) | [Русский](docs/ru/README.md) | [Tiếng Việt](docs/vi-VN/README.md) | [ไทย](docs/th/README.md) | [Deutsch](docs/de-DE/README.md) | [Polski](docs/pl/README.md) diff --git a/docs/ar/GLOSSARY.md b/docs/ar/GLOSSARY.md new file mode 100644 index 000000000..9a7916e89 --- /dev/null +++ b/docs/ar/GLOSSARY.md @@ -0,0 +1,63 @@ +# مسرد المصطلحات / Glossary + +مسرد المصطلحات الموحد للترجمة العربية لمشروع ECC. + +المبدأ التوجيهي: تظل أسماء واجهات ECC الأساسية والمصطلحات التقنية المعتمدة باللغة الإنجليزية كما هي (`agents/`، `skills/`، `hooks/`، `rules/`، `commands/`) عندما يكون تعريبها سبباً في صعوبة ربط النص باسم المجلد أو الأمر البرمجي أو واجهة الاستخدام في بيئة العمل. في السياق الوصفي العام، يمكن استخدام المقابل العربي الموضح أدناه. + +| المصطلح بالإنجليزية (English) | المقابل بالعربية (Arabic) | ملاحظات وسياق الاستخدام | +|---|---|---| +| Agent | Agent / وكيل | اسم واجهة `agents/` البرمجية؛ الجمع: وكلاء | +| Skill | Skill / مهارة | اسم واجهة `skills/`؛ الجمع: مهارات | +| Hook | Hook / خُطاف أو معالج أحداث | اسم واجهة `hooks/`؛ معالجات أحداث بيئة العمل | +| Command | Command / أمر | اسم واجهة `commands/`؛ عامةً: "أمر برمجيات" | +| Rule | Rule / قاعدة | اسم واجهة `rules/`؛ عامةً: "قاعدة توجيهية" | +| Harness | بيئة تشغيل الوكيل (Harness) | يُبقى على Harness كمصطلح تقني أصيل لنظام التشغيل | +| Instinct | Instinct / غريزة برمجية | مصطلح مستخدم في ميزة Continuous Learning | +| Plugin | إضافة (Plugin) | حزمة برمجية متكاملة لبيئة العمل | +| Marketplace | سوق الإضافات (Marketplace) | مستودع الإضافات المعتمد | +| Worktree | شجرة عمل (Worktree) | مصطلح Git لبيئات العمل المتوازية | +| Subagent | وكيل فرعي (Subagent) | وكيل منبثق لمهمة محددة | +| Frontmatter | الترويسة التعريفية (Frontmatter) | بيانات YAML الوصفية أعلى الملفات | +| Continuous Learning | التعلّم المستمر | ميزة استخلاص التجارب وتطوير الأنماط | +| Memory | الذاكرة / نظام الذاكرة | كمفهوم بنيوي في ECC: مستودع الذاكرة | +| Context window | نافذة السياق | سعة الرموز المتاحة للنموذج في الجلسة | +| Token | رمز / وحدة معالجة (Token) | أصغر وحدة نصية يفهمها النموذج | +| Coverage | نسبة التغطية (Test Coverage) | تغطية الاختبارات البرمجية للكود | +| Test-Driven Development | التطوير الموجه باختبارات | يُحافظ على الاختصار TDD | +| Code review | مراجعة الكود البرمجي | فحص جودة وشروط الكود | +| Refactoring | إعادة هيكلة الكود (Refactoring) | تحسين بنية الكود دون تغيير سلوكه | +| Pull request | طلب السحب (PR) | طلب دمج التعديلات في المستودع | +| Commit | إيداع / تثبيت (Commit) | تسجيل التغييرات في سجل Git | +| Branch | فرع (Branch) | مسار تطوير مستقل | +| Merge | دمج (Merge) | دمج فرع في آخر | +| Build | بناء (Build) | إنتاج الحزمة البرمجية النهائية | +| Deploy | نشر (Deploy) | إطلاق البرمجية في بيئة التشغيل | +| Pipeline | خط أنابيب المعالجة (Pipeline) | مسار أتمتة العمليات البرمجية | +| Orchestration | التنسيق التلقائي (Orchestration) | إدارة تفاعل الوكلاء والمهام | +| Repository | مستودع (Repository) | اختصاراً: مستودع (Repo) | +| Dependency | تبعية برمجية (Dependency) | الجمع: تبعيات | +| Edge case | حالة طرفية / حدّية | سيناريو غير تقليدي في عمل البرنامج | +| Best practice | أفضل الممارسات | المعايير الهندسية الفضلى الموصى بها | +| Anti-pattern | نمط مضاد (Anti-pattern) | ممارسة برمجية تبدو حلاً ولكنها تخلق مشاكل | +| Middleware | برمجية وسيطة (Middleware) | طبقة اعتراض ومعالجة الطلبات | +| Endpoint | نقطة نهاية (Endpoint) | رابط واجهة برمجة التطبيقات (API) | +| Schema | مخطط هيكلي (Schema) | توصيف بنية البيانات وقواعدها | +| Payload | حمولة البيانات (Payload) | البيانات المرسلة في الطلب البرمجي | +| Callback | رد نداء (Callback) | دالة تُنفذ كرد على حدث معين | +| Checkpoint | نقطة فحص / استعادة | نقطة حفظ مرحلية لحالة العمل | +| Linter | أداة فحص الكود (Linter) | مدقق المعايير الصياغية للكود | +| Formatter | أداة تنسيق الكود (Formatter) | منظم شكل وترتيب الكود البرمجي | +| Staging | بيئة الاختبار والتجهيز (Staging) | بيئة تحاكي بيئة الإنتاج قبل الإطلاق | +| Production | بيئة الإنتاج (Production) | البيئة الحية المستخدمة فعلياً | +| Debugging | تنقيح الأخطاء (Debugging) | تشخيص وإصلاح العلل البرمجية | +| Logging | تسجيل السجلات (Logging) | تدوين أحداث وسجلات النظام | +| Monitoring | المراقبة والمتابعة (Monitoring) | رصد أداء واستقرار الخدمات البرمجية | +| Rate limit | حد معدل الطلبات (Rate Limit) | السقف الزمني المسموح به للطلبات | +| Retry | إعادة المحاولة (Retry) | تكرار تنفيذ عملية فشلت مؤقتاً | +| Fallback | خيار بديل / مسار تراجعي | آلية عمل بديلة عند تعذر المسار الأساسي | +| Sandboxing | العزل في بيئة آمنة (Sandboxing) | تشغيل الكود في بيئة محكمة معزولة | +| Sanitization | تنقية وتطهير البيانات | تنظيف المدخلات من الشوائب والرموز غير الآمنة | +| Selective install | التثبيت الانتقائي | تثبيت مكونات محددة وفق الاحتياج | +| Profile | ملف تعريف التثبيت (Profile) | إعداد مسبق لحزمة التثبيت | +| Component | مكوّن (Component) | عنصر معرف ضمن كتالوج المثبت | +| Module | وحدة نمطية (Module) | وحدة ملفات قابلة للتثبيت والنقل | diff --git a/docs/ar/README.md b/docs/ar/README.md new file mode 100644 index 000000000..818ba29d9 --- /dev/null +++ b/docs/ar/README.md @@ -0,0 +1,174 @@ +**اللغة:** [English](../../README.md) | **العربية** | [Deutsch](../de-DE/README.md) | [Español](../es/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) + +# ECC + +![ECC — نظام تشغيل مهام الوكلاء الأصلي لبيئات العمل](../../assets/hero.png) + +> يغطي هذا التوثيق دليل البدء السريع والواجهات والمفاهيم الأساسية لنظام ECC. +> مصدر المحتوى الإنجليزي المعتمد هو الفرع main في المستودع الرسمي. +> يظل الكتالوج الكامل والمحدّث باستمرار متوفراً في [README باللغة الإنجليزية](../../README.md)؛ +> وتتم متابعة وتحديث الأقسام دورياً لضمان الدقة والتوافق. + +--- + +**نظام تشغيل مهام الوكلاء الأصلي لبيئات العمل (Harness-Native Agent Operating System).** + +مشروع ECC ليس مجرد مجموعة من ملفات الإعداد الثابتة. بل يدمج وكلاء متخصصين (Agents)، +ومهارات معيارية (Skills)، ومعالجات أحداث تلقائية (Hooks)، وقواعد توجيهية (Rules)، +وتكوينات بروتوكول سياق النموذج (MCP)، وطبقة توافق للأوامر السابقة (Commands). +صُمم النظام بناءً على ممارسات عمل حقيقية ويمتد ليعمل بتناسق عبر بيئات متعددة: +**Claude Code**، **Codex**، **Cursor**، **OpenCode**، **Gemini**، **Zed**، **GitHub Copilot** وغيرها الكثير. + +## المصادر الرسمية + +ثبّت ECC حصرياً عبر القنوات الرسمية المعتمدة: + +- مستودع المشروع الرسمي: [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) +- تطبيق [ECC Tools على GitHub](https://github.com/apps/ecc-tools) +- معرّف الإضافة المعتمد: `ecc@ecc` +- الموقع الرسمي: [ecc.tools](https://ecc.tools) + +يُرجى الحذر من النسخ غير الرسمية أو المستودعات البديلة غير المعتمدة حيث لا تخضع للمراجعة الأمنية أو الصيانة من قبل فريق المشروع. + +## البدء السريع + +اختر **طريقة تثبيت واحدة فقط**. الجمع بين تثبيت الإضافة والتثبيت اليدوي الكامل هو السبب الأكثر شيوعاً لتكرار الوكلاء والمهارات والخطافات. + +### الإعداد التفاعلي الموجه (الموصى به) + +```bash +npx ecc-universal@2.2.1 setup +``` + +يمكنك أيضاً استخدام مدير الحزم المفضل لديك: + +```bash +pnpm dlx ecc-universal@2.2.1 setup +yarn dlx ecc-universal@2.2.1 setup +bunx ecc-universal@2.2.1 setup +``` + +قبل تشغيل كود الحزمة، تحقق من مصدر الإصدار وسلامة السجل المعتمد. + +### Claude Code + +في Claude Code، أضف سوق الإضافات ثم ثبّت الإضافة المعتمدة: + +```text +/plugin marketplace add https://github.com/affaan-m/ECC +/plugin install ecc@ecc +``` + +بعد ذلك، ابدأ بـ `rules/common` بالإضافة إلى حزم اللغات وأطر العمل التي تستخدمها بالفعل في مشروعك. +تحذير مهم: بعد إتمام تثبيت الإضافة عبر `/plugin install`، لا تقم بتشغيل أمر التثبيت الكامل `./install.sh --profile full` إطلاقاً لتجنب تكرار الوكلاء والمهارات. + +### Codex + +```bash +codex plugin marketplace add affaan-m/ECC +codex plugin add ecc@ecc +``` + +في Codex، استخدم `$configure-ecc` لإجراء التكوين المخصص المناسب لمزود الخدمة في بيئتك. + +### البيئات الأخرى (Other Harnesses) + +شغّل الأوامر التالية من المجلد الرئيسي للمستودع المحلي بعد تنزيله. يختار سطر OpenCode الملف التعريفي الكامل ويفعّل الخطافات التلقائية صراحة. + +| بيئة العمل (Harness) | أمر التثبيت من المستودع | +|---|---| +| Cursor | `./install.sh --profile minimal --target cursor` | +| Gemini CLI | `./install.sh --profile minimal --target gemini` | +| Zed | `./install.sh --profile minimal --target zed` | +| OpenCode | `npm install && npm run build:opencode && ./install.sh --profile full --target opencode --enable-hooks` | +| Hermes | `./install.sh --profile minimal --target hermes` | +| OpenClaw | `./install.sh --profile minimal --target openclaw` | +| Kimi Code CLI | `./install.sh --profile minimal --target kimi` | + +للاطلاع على مصفوفة الدعم الكاملة لكافة المنصات والمتطلبات التفصيلية، راجع [قسم Platform Support في README الأساسي](../../README.md#platform-support). + +## تثبيت التوثيق العربي + +تستخدم اللغة العربية الرمز `ar`، ويُقبل أيضاً الاسم المستعار `ar-SA`. استخدم نسخة محلية من المستودع تحتوي على الدليل `docs/ar/` والمكوّن `locale:ar`. نفّذ الأوامر التالية من المجلد الرئيسي للمستودع بعد تثبيت تبعيات المشروع (يتطلب Node.js 18 أو أحدث). الإصدار الحالي من حزمة `ecc-universal` قد لا يتضمن التوثيق العربي بعد. + +أولاً، عاين خطة التثبيت دون كتابة الملفات (Dry Run): + +```bash +node scripts/install-apply.js --target claude --locale ar --dry-run +``` + +ثم ثبّت التوثيق العربي في المسار `~/.claude/docs/ar/`: + +```bash +node scripts/install-apply.js --target claude --locale ar +``` + +يثبت هذا الخيار وحدة التوثيق `docs-ar` بشكل مستقل؛ ولا يثبت الملف التعريفي الكامل لـ ECC ولا الخطافات التلقائية. + +## مكونات وواجهات ECC + +| الواجهة | الدور البرمجي والوظيفي | +|---|---| +| `agents/` | وكلاء متخصصون للتخطيط والتنفيذ والمراجعة والتشخيص | +| `skills/` | إجراءات ومعارف نمطية متخصصة تُحمّل عند الحاجة إليها في المهمة | +| `hooks/` | أتمتة ومعالجات أحداث تُنفّذ عند وقوع أحداث محددة في بيئة العمل | +| `rules/` | قواعد توجيهية دائمة ومعايير خاصة باللغات وأطر العمل | +| `commands/` | طبقة توافق مع الأوامر السابقة للأوامر المسبوقة بشرطة مائلة | +| `mcp-configs/` | تكوينات خوادم بروتوكول سياق النموذج (Model Context Protocol) | +| `manifests/` | بيانات تعريفية معيارية للوحدات والمكونات وملفات التثبيت | + +يعتمد مسار المشروع منهجية **المهارات أولاً (skills-first)**: حيث تُضاف مهام سير العمل الجديدة أولاً إلى `skills/`؛ وتبقى `commands/` كواجهة توافق حيثما دعت الحاجة. + +## المفاهيم الأساسية + +### الوكلاء (Agents) + +يمتلك كل وكيل دوراً محدداً، ومجموعة أدوات مخصصة، وأسلوب عمل واضحاً؛ مثل وكيل التخطيط، ومراجعي الكود للغات البرمجية المختلفة، ومتخصصي تشخيص وحل أخطاء البناء والترجمة. + +### المهارات (Skills) + +تحتوي المهارة على إجراء عمل مركز أو معرفة بمجال تقني محدد. بفضل ذلك، يُحمّل السياق فقط عندما يلائم المهمة الحالية، بدلاً من إثقال كل موجّه نظام (System Prompt) بسياق عام غير ضروري. + +### الخطافات (Hooks) + +تستجيب الخطافات للأحداث البرمجية مثل بدء الجلسة أو استدعاء الأدوات. صُممت الخطافات لتكون آمنة وقابلة للنقل؛ ولا يلزم نسخها يدوياً إلى ملف الإعدادات بعد تثبيت الإضافة، حيث تقوم الإصدارات الحديثة من بيئات العمل بتحميل `hooks/hooks.json` تلقائياً. + +### القواعد (Rules) + +تحدد القواعد الأعراف والاصطلاحات البرمجية الواجب اتباعها دائماً. ثبّت النواة المشتركة والحزم المطابقة لاحتياجاتك فقط، لتجنب استهلاك نافذة السياق بقواعد لا صلة لها بالمشروع. + +للاطلاع على تفاصيل تعريب المفاهيم والمصطلحات، راجع [مسرد المصطلحات العربي](GLOSSARY.md). + +## الأمان والخصوصية + +- لا تقم أبداً بتخزين مفاتيح واجهات برمجة التطبيقات (API Keys) أو كلمات المرور أو الرموز السرية داخل المستودع. +- راجع السكربتات ومصادر الحزم قبل تشغيلها في بيئتك. +- تجنب الجمع بين عدة طرق تثبيت لتفادي تضارب وتكرار الملفات. +- لا تنسخ ملف `hooks/hooks.json` الخام مباشرة إلى `~/.claude/settings.json` بعد تثبيت الإضافة. +- التزم بمبدأ الحد الأدنى من الصلاحيات وتحقق من سلامة المدخلات عند حدود النظام. + +للمزيد من الإرشادات الأمنية، راجع [قسم Security في README الأساسي](../../README.md#security). + +## تحديث التعريب والمساهمة + +تتم صيانة التوثيق المترجم وفق أفضل الممارسات المستمرة. يُستحسن في كل طلب سحب (PR) توضيح ما يلي: + +1. رقم إيداع المصدر الإنجليزي (Commit SHA) المعتمد. +2. النطاق الدقيق للمحتوى المترجم أو المحدّث. +3. أي تعديلات أو إضافات على [مسرد المصطلحات](GLOSSARY.md). +4. نتائج فحوصات الروابط وسلامة التنسيق ومخططات التثبيت (Manifests). + +يُفضل أن تكون طلبات السحب مركزة ومقسمة حسب المجال (مثل `docs/`، `agents/`، `skills/`) لتسهيل المراجعة والمزامنة مع المستودع الأساسي. + +## المساهمة في المشروع + +قبل البدء في ترجمات موسعة أو ميزات جديدة، راجع المهام وقوائم النقاش المفتوحة لتنسيق الجهود وتجنب العمل المزدوج. للاطلاع على الإرشادات الكاملة للمساهمة، راجع [CONTRIBUTING.md](../../CONTRIBUTING.md). + +## الترخيص + +مرخص بموجب رخصة MIT — يمنحك ذلك حرية الاستخدام والتعديل ومشاركة التحسينات مع مجتمع المطورين. + +--- + +**إذا كان ECC يقدم لك قيمة في عملك، شارك بنجمة للمستودع، واقرأ الأدلة، وابنِ برمجيات استثنائية.** diff --git a/docs/de-DE/README.md b/docs/de-DE/README.md index c520a611c..561608611 100644 --- a/docs/de-DE/README.md +++ b/docs/de-DE/README.md @@ -1,4 +1,4 @@ -**Sprache:** [English](../../README.md) | [Deutsch](README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) +**Sprache:** [English](../../README.md) | [العربية](../ar/README.md) | [Deutsch](README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) # ECC @@ -27,7 +27,7 @@ **Language / 语言 / 語言 / Dil / Язык / Ngôn ngữ** -[English](../../README.md) | [**Deutsch**](README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) +[English](../../README.md) | [العربية](../ar/README.md) | [**Deutsch**](README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) diff --git a/docs/es/README.md b/docs/es/README.md index 954adaa86..f3fd565f8 100644 --- a/docs/es/README.md +++ b/docs/es/README.md @@ -1,4 +1,4 @@ -**Idioma:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | **Español** | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) +**Idioma:** [English](../../README.md) | [العربية](../ar/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | **Español** | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) # ECC diff --git a/docs/ja-JP/README.md b/docs/ja-JP/README.md index 3c772b7bd..89de43bd1 100644 --- a/docs/ja-JP/README.md +++ b/docs/ja-JP/README.md @@ -20,6 +20,7 @@

Language: English | + العربية | Português (Brasil) | 简体中文 | 繁體中文 | diff --git a/docs/ko-KR/README.md b/docs/ko-KR/README.md index 25be75d36..9f0c29822 100644 --- a/docs/ko-KR/README.md +++ b/docs/ko-KR/README.md @@ -1,4 +1,4 @@ -**언어:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Polski](../pl/README.md) +**언어:** [English](../../README.md) | [العربية](../ar/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Polski](../pl/README.md) # ECC diff --git a/docs/pl/README.md b/docs/pl/README.md index 582549f6b..7881c6362 100644 --- a/docs/pl/README.md +++ b/docs/pl/README.md @@ -1,4 +1,4 @@ -**Język:** [English](../../README.md) | **Polski** | [Deutsch](../de-DE/README.md) | [Español](../es/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) +**Język:** [English](../../README.md) | [العربية](../ar/README.md) | **Polski** | [Deutsch](../de-DE/README.md) | [Español](../es/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) # ECC diff --git a/docs/pt-BR/README.md b/docs/pt-BR/README.md index 5cd268864..8fed5e513 100644 --- a/docs/pt-BR/README.md +++ b/docs/pt-BR/README.md @@ -1,4 +1,4 @@ -**Idioma:** [English](../../README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | Português (Brasil) | [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) | [Polski](../pl/README.md) +**Idioma:** [English](../../README.md) | [العربية](../ar/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | Português (Brasil) | [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) | [Polski](../pl/README.md) # ECC diff --git a/docs/ru/README.md b/docs/ru/README.md index beeb4e59e..09a9794c7 100644 --- a/docs/ru/README.md +++ b/docs/ru/README.md @@ -1,4 +1,4 @@ -**Язык:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | **Русский** | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) +**Язык:** [English](../../README.md) | [العربية](../ar/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | **Русский** | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) # ECC diff --git a/docs/th/README.md b/docs/th/README.md index a74671910..5e7b5c972 100644 --- a/docs/th/README.md +++ b/docs/th/README.md @@ -1,4 +1,4 @@ -**ภาษา:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | **ไทย** | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) +**ภาษา:** [English](../../README.md) | [العربية](../ar/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | **ไทย** | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) # ECC @@ -18,7 +18,7 @@ **ภาษา / Language / 语言 / 語言 / Dil / Язык / Ngôn ngữ** -[English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | **ไทย** | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) +[English](../../README.md) | [العربية](../ar/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | **ไทย** | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) diff --git a/docs/tr/README.md b/docs/tr/README.md index 3f480127b..0b795a322 100644 --- a/docs/tr/README.md +++ b/docs/tr/README.md @@ -23,7 +23,7 @@ **Dil / Language / 语言 / 語言 / Язык / Ngôn ngữ** -[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [**Türkçe**](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) | [Polski](../pl/README.md) +[**English**](../../README.md) | [العربية](../ar/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [**Türkçe**](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) | [Polski](../pl/README.md) diff --git a/docs/uk-UA/README.md b/docs/uk-UA/README.md index f76959dc6..d5e452c88 100644 --- a/docs/uk-UA/README.md +++ b/docs/uk-UA/README.md @@ -5,6 +5,7 @@

Мова: English | + العربية | Português (Brasil) | 简体中文 | 繁體中文 | diff --git a/docs/ur/README.md b/docs/ur/README.md index d98a0dc4c..7f4765336 100644 --- a/docs/ur/README.md +++ b/docs/ur/README.md @@ -1,4 +1,4 @@ -**زبان:** [English](../../README.md) | [اردو](README.md) | [Deutsch](../de-DE/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) +**زبان:** [English](../../README.md) | [العربية](../ar/README.md) | [اردو](README.md) | [Deutsch](../de-DE/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) # ECC @@ -27,7 +27,7 @@ **زبان / Language / 语言** -[English](../../README.md) | [**اردو**](README.md) | [Deutsch](../de-DE/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) +[English](../../README.md) | [العربية](../ar/README.md) | [**اردو**](README.md) | [Deutsch](../de-DE/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) diff --git a/docs/vi-VN/README.md b/docs/vi-VN/README.md index 8fa868509..2a68b8ab2 100644 --- a/docs/vi-VN/README.md +++ b/docs/vi-VN/README.md @@ -1,4 +1,4 @@ -**Ngôn ngữ:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | **Tiếng Việt** | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) +**Ngôn ngữ:** [English](../../README.md) | [العربية](../ar/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | **Tiếng Việt** | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) # ECC @@ -18,7 +18,7 @@ **Ngôn ngữ / Language / 语言 / 語言 / Dil / Язык** -[English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | **Tiếng Việt** | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) +[English](../../README.md) | [العربية](../ar/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | **Tiếng Việt** | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 4e64b2389..9347cc940 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -1,4 +1,4 @@ -**语言:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) +**语言:** [English](../../README.md) | [العربية](../ar/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) # ECC diff --git a/docs/zh-TW/README.md b/docs/zh-TW/README.md index 6f1654c66..71cac112d 100644 --- a/docs/zh-TW/README.md +++ b/docs/zh-TW/README.md @@ -13,7 +13,7 @@ **Language / 语言 / 語言 / Dil / Язык / Ngôn ngữ** -[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | **繁體中文** | [日本語](../ja-JP/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) | [Polski](../pl/README.md) +[**English**](../../README.md) | [العربية](../ar/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | **繁體中文** | [日本語](../ja-JP/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) | [Polski](../pl/README.md) diff --git a/manifests/install-components.json b/manifests/install-components.json index 5f8970c75..55ac377b2 100644 --- a/manifests/install-components.json +++ b/manifests/install-components.json @@ -205,7 +205,7 @@ { "id": "capability:ito-compute", "family": "capability", - "description": "Authenticated It\u00f4 GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", + "description": "Authenticated Itô GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", "modules": [ "ito-compute" ] @@ -685,6 +685,14 @@ "modules": [ "docs-pl" ] + }, + { + "id": "locale:ar", + "family": "locale", + "description": "Arabic (ar) translated reference docs installed to ~/.claude/docs/ar/.", + "modules": [ + "docs-ar" + ] } ] } diff --git a/manifests/install-modules.json b/manifests/install-modules.json index b61d4e20f..a8b3a06d1 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -642,7 +642,7 @@ { "id": "ito-compute", "kind": "skills", - "description": "Authenticated It\u00f4 GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", + "description": "Authenticated Itô GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", "paths": [ "skills/ito-compute", "skills/ito-inference", @@ -1194,6 +1194,22 @@ "defaultInstall": false, "cost": "heavy", "stability": "stable" + }, + { + "id": "docs-ar", + "kind": "docs", + "description": "Arabic (ar) getting-started and core-concepts guide with a terminology glossary.", + "paths": [ + "docs/ar" + ], + "targets": [ + "claude", + "claude-project" + ], + "dependencies": [], + "defaultInstall": false, + "cost": "heavy", + "stability": "stable" } ] } diff --git a/package.json b/package.json index 983370b3a..a326b98eb 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "assets/images/sponsors/", "agents/", "commands/", + "docs/ar/", "docs/de-DE/", "docs/CODEX-NAVIGATION-GUIDE.md", "docs/COMMAND-AGENT-MAP.md", diff --git a/scripts/lib/install-manifests.js b/scripts/lib/install-manifests.js index 6d0fac5cf..d64c627f2 100644 --- a/scripts/lib/install-manifests.js +++ b/scripts/lib/install-manifests.js @@ -15,7 +15,7 @@ const COMPONENT_FAMILY_PREFIXES = { skill: 'skill:', locale: 'locale:', }; -const SUPPORTED_LOCALES = Object.freeze(['ja', 'zh-CN', 'ko-KR', 'pt-BR', 'ru', 'tr', 'vi-VN', 'zh-TW', 'de-DE', 'uk-UA', 'pl']); +const SUPPORTED_LOCALES = Object.freeze(['ja', 'zh-CN', 'ko-KR', 'pt-BR', 'ru', 'tr', 'vi-VN', 'zh-TW', 'de-DE', 'uk-UA', 'pl', 'ar']); const LOCALE_ALIAS_TO_COMPONENT_ID = Object.freeze({ 'ja': 'locale:ja', 'ja-JP': 'locale:ja', @@ -35,7 +35,9 @@ const LOCALE_ALIAS_TO_COMPONENT_ID = Object.freeze({ 'uk-UA': 'locale:uk-ua', 'uk': 'locale:uk-ua', 'pl': 'locale:pl', - 'pl-PL': 'locale:pl' + 'pl-PL': 'locale:pl', + 'ar': 'locale:ar', + 'ar-SA': 'locale:ar' }); function listSupportedLocales() { diff --git a/tests/docs/install-identifiers.test.js b/tests/docs/install-identifiers.test.js index 184e1b861..fdfe6b23c 100644 --- a/tests/docs/install-identifiers.test.js +++ b/tests/docs/install-identifiers.test.js @@ -26,6 +26,7 @@ const publicInstallDocs = [ 'README.zh-CN.md', 'docs/pt-BR/README.md', 'docs/zh-CN/README.md', + 'docs/ar/README.md', 'docs/ja-JP/skills/configure-ecc/SKILL.md', 'docs/zh-CN/skills/configure-ecc/SKILL.md', ]; diff --git a/tests/lib/locale-install.test.js b/tests/lib/locale-install.test.js index 373427224..8420417ba 100644 --- a/tests/lib/locale-install.test.js +++ b/tests/lib/locale-install.test.js @@ -53,9 +53,63 @@ function runTests() { assert.ok(components.some(component => component.id === 'locale:de-de')); assert.ok(components.some(component => component.id === 'locale:uk-ua')); assert.ok(components.some(component => component.id === 'locale:pl')); + assert.ok(components.some(component => component.id === 'locale:ar')); assert.ok(components.every(component => component.family === 'locale')); })) passed++; else failed++; + if (test('locale:ar resolves to the Arabic translated docs module', () => { + const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'locale-plan-ar-')); + try { + const plan = resolveInstallPlan({ + includeComponentIds: ['locale:ar'], + target: 'claude', + homeDir, + }); + + assert.deepStrictEqual(plan.selectedModuleIds, ['docs-ar']); + assert.ok( + plan.operations.some(operation => ( + normalizePlanPath(operation.sourceRelativePath) === 'docs/ar' + && normalizePlanPath(operation.destinationPath).endsWith('/.claude/docs/ar') + )), + 'Should map docs/ar to ~/.claude/docs/ar' + ); + } finally { + fs.rmSync(homeDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + + if (test('end-to-end: --locale ar-SA dry-run includes docs-ar operations', () => { + const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'locale-dry-run-ar-')); + const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'locale-dry-run-ar-project-')); + + try { + const output = runInstallApply([ + '--locale', 'ar-SA', + '--dry-run', + '--json', + ], { + cwd: projectDir, + env: { HOME: homeDir }, + }); + const json = JSON.parse(output); + + assert.strictEqual(json.plan.mode, 'manifest'); + assert.deepStrictEqual(json.plan.includedComponentIds, ['locale:ar']); + assert.deepStrictEqual(json.plan.selectedModuleIds, ['docs-ar']); + assert.ok( + json.plan.operations.some(operation => ( + normalizePlanPath(operation.sourceRelativePath) === 'docs/ar/README.md' + && normalizePlanPath(operation.destinationPath).endsWith('/.claude/docs/ar/README.md') + )), + 'Should copy translated README into ~/.claude/docs/ar' + ); + } finally { + fs.rmSync(homeDir, { recursive: true, force: true }); + fs.rmSync(projectDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + if (test('locale:pl resolves to the Polish translated docs module', () => { const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'locale-plan-pl-')); try { @@ -327,6 +381,38 @@ function runTests() { } })) passed++; else failed++; + if (test('end-to-end: --locale ar installs translated docs cleanly', () => { + const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'locale-install-ar-')); + const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'locale-install-ar-project-')); + + try { + runInstallApply([ + '--locale', 'ar', + ], { + cwd: projectDir, + env: { HOME: homeDir }, + }); + + const claudeRoot = path.join(homeDir, '.claude'); + assert.ok( + fs.existsSync(path.join(claudeRoot, 'docs', 'ar', 'README.md')), + 'Should install Arabic README under docs/ar' + ); + assert.ok( + !fs.existsSync(path.join(claudeRoot, 'skills', 'configure-ecc', 'SKILL.md')), + 'Locale-only install should not install English skills' + ); + + const statePath = path.join(claudeRoot, 'ecc', 'install-state.json'); + const state = JSON.parse(fs.readFileSync(statePath, 'utf8')); + assert.deepStrictEqual(state.request.includeComponents, ['locale:ar']); + assert.deepStrictEqual(state.resolution.selectedModules, ['docs-ar']); + } finally { + fs.rmSync(homeDir, { recursive: true, force: true }); + fs.rmSync(projectDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + if (test('end-to-end: --locale uk-UA installs translated docs cleanly', () => { const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'locale-install-uk-')); const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'locale-install-uk-project-')); From 849456d57e1cb2a6bc9633b06b544d609f84499d Mon Sep 17 00:00:00 2001 From: dusseau-dev Date: Thu, 8 Oct 2026 22:59:42 -0400 Subject: [PATCH 27/39] feat(skills): add agent runtime gateway smoke test --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .gemini/GEMINI.md | 2 +- AGENTS.md | 4 +- README.md | 4 +- README.zh-CN.md | 2 +- SOUL.md | 2 +- docs/tr/AGENTS.md | 4 +- docs/zh-CN/AGENTS.md | 4 +- docs/zh-CN/README.md | 6 +- manifests/context-packs/skill-triggers@1.json | 2 +- manifests/install-modules.json | 1 + package.json | 1 + .../agent-runtime-gateway-smoke-test/SKILL.md | 77 +++++++++++++++++++ 14 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 skills/agent-runtime-gateway-smoke-test/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 0d83b4a00..266ad2574 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ { "name": "ecc", "source": "./", - "description": "Harness-native ECC operator layer - 68 agents, 293 skills, 94 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses", + "description": "Harness-native ECC operator layer - 68 agents, 294 skills, 94 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses", "version": "2.2.3", "author": { "name": "Affaan Mustafa", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 64d1f8ede..d1eb21374 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ecc", "version": "2.2.3", - "description": "Harness-native ECC plugin for engineering teams - 68 agents, 293 skills, 94 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses", + "description": "Harness-native ECC plugin for engineering teams - 68 agents, 294 skills, 94 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/.gemini/GEMINI.md b/.gemini/GEMINI.md index 42ada430b..544993ad6 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -4,7 +4,7 @@ This file provides Gemini CLI with the baseline ECC workflow, review standards, ## Overview -ECC is a cross-harness coding system with 68 specialized agents, 293 skills, and 94 commands. +ECC is a cross-harness coding system with 68 specialized agents, 294 skills, and 94 commands. Gemini support is currently focused on a strong project-local instruction layer via `.gemini/GEMINI.md`, plus the shared MCP catalog and package-manager setup assets shipped by the installer. diff --git a/AGENTS.md b/AGENTS.md index e2e6847c1..f4a0f616d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # ECC — Agent Instructions -This is a **production-ready AI coding plugin** providing 68 specialized agents, 293 skills, 94 commands, and automated hook workflows for software development. +This is a **production-ready AI coding plugin** providing 68 specialized agents, 294 skills, 94 commands, and automated hook workflows for software development. **Version:** 2.2.3 @@ -201,7 +201,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat ``` agents/ — 68 specialized subagents -skills/ — 293 workflow skills and domain knowledge +skills/ — 294 workflow skills and domain knowledge commands/ — 94 slash commands hooks/ — Trigger-based automations rules/ — Always-follow guidelines (common + per-language) diff --git a/README.md b/README.md index 95673de4b..403910d82 100644 --- a/README.md +++ b/README.md @@ -139,12 +139,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, 293 skills, and 94 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, 294 skills, and 94 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 | 293 skills | TDD, research, security, docs, frontend, data, ML, operations, and more | +| Skills | 294 skills | TDD, research, security, docs, frontend, data, ML, operations, and more | | Commands | 94 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 | diff --git a/README.zh-CN.md b/README.zh-CN.md index c098f5f41..a896430b3 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -200,7 +200,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**完成!** 你现在可以使用 68 个代理、293 个技能和 94 个命令。 +**完成!** 你现在可以使用 68 个代理、294 个技能和 94 个命令。 ### multi-* 命令需要额外配置 diff --git a/SOUL.md b/SOUL.md index ccec20351..92688e1b1 100644 --- a/SOUL.md +++ b/SOUL.md @@ -1,7 +1,7 @@ # Soul ## Core Identity -ECC is a production-ready AI coding plugin with 68 specialized agents, 293 skills, 94 commands, and automated hook workflows for software development. +ECC is a production-ready AI coding plugin with 68 specialized agents, 294 skills, 94 commands, 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/docs/tr/AGENTS.md b/docs/tr/AGENTS.md index 9df43a466..68490cfdb 100644 --- a/docs/tr/AGENTS.md +++ b/docs/tr/AGENTS.md @@ -1,6 +1,6 @@ # ECC — Agent Talimatları -Bu, yazılım geliştirme için 68 özel agent, 293 skill, 94 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, 294 skill, 94 command ve otomatik hook iş akışları sağlayan **üretime hazır bir AI kodlama eklentisidir**. **Sürüm:** 2.2.3 @@ -142,7 +142,7 @@ Başarısızlık sorunlarını giderin: test izolasyonunu kontrol edin → mockl ``` agents/ — 68 özel subagent -skills/ — 293 iş akışı skillleri ve alan bilgisi +skills/ — 294 iş akışı skillleri ve alan bilgisi commands/ — 94 slash command hooks/ — Tetikleyici tabanlı otomasyonlar rules/ — Her zaman uyulması gereken kurallar (ortak + dile özel) diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md index 781079a19..8bcad7767 100644 --- a/docs/zh-CN/AGENTS.md +++ b/docs/zh-CN/AGENTS.md @@ -1,6 +1,6 @@ # ECC — 智能体指令 -这是一个**生产就绪的 AI 编码插件**,提供 68 个专业代理、293 项技能、94 条命令以及自动化钩子工作流,用于软件开发。 +这是一个**生产就绪的 AI 编码插件**,提供 68 个专业代理、294 项技能、94 条命令以及自动化钩子工作流,用于软件开发。 **版本:** 2.2.3 @@ -147,7 +147,7 @@ ``` agents/ — 68 个专业子代理 -skills/ — 293 个工作流技能和领域知识 +skills/ — 294 个工作流技能和领域知识 commands/ — 94 个斜杠命令 hooks/ — 基于触发的自动化 rules/ — 始终遵循的指导方针(通用 + 每种语言) diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 4e64b2389..d9c221e57 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -264,7 +264,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**搞定!** 你现在可以使用 68 个智能体、293 项技能和 94 个命令了。 +**搞定!** 你现在可以使用 68 个智能体、294 项技能和 94 个命令了。 *** @@ -1178,7 +1178,7 @@ opencode |---------|---------------|----------|--------| | 智能体 | PASS: 68 个 | PASS: 12 个 | **Claude Code 领先** | | 命令 | PASS: 94 个 | PASS: 35 个 | **Claude Code 领先** | -| 技能 | PASS: 293 项 | PASS: 37 项 | **Claude Code 领先** | +| 技能 | PASS: 294 项 | PASS: 37 项 | **Claude Code 领先** | | 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** | | 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** | | MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** | @@ -1286,7 +1286,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以 |---------|-----------------------|------------|-----------|----------| | **智能体** | 68 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | | **命令** | 94 | 共享 | 基于指令 | 35 | -| **技能** | 293 | 共享 | 10 (原生格式) | 37 | +| **技能** | 294 | 共享 | 10 (原生格式) | 37 | | **钩子事件** | 8 种类型 | 15 种类型 | SessionStart(1 种类型) | 11 种类型 | | **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | 1 个 SessionStart 引导脚本 | 插件钩子 | | **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 | diff --git a/manifests/context-packs/skill-triggers@1.json b/manifests/context-packs/skill-triggers@1.json index b478a347e..bdd3d2271 100644 --- a/manifests/context-packs/skill-triggers@1.json +++ b/manifests/context-packs/skill-triggers@1.json @@ -1 +1 @@ -{"coverage":{"skills":293,"withTriggers":32},"generatedAt":"2026-09-24T23:51:22.784Z","id":"skill-triggers@1","model":{"effort":null,"id":"hand-seeded","source":"manual-curation-pending-regeneration"},"registryDigest":"a4abb961bbb5032e40d14e9aeef7e8c652d3517458b7ab72086b768c2e8b5a8b","schemaVersion":1,"triggers":{"skill:api-connector-builder":["add api integration","new provider connector","match existing integration pattern"],"skill:api-design":["rest endpoint design","pagination api","status codes","api versioning","rate limiting api","resource naming","filtering api","api error responses","offset pagination","limit query parameter","pagination defaults"],"skill:backend-patterns":["express api","node backend architecture","nextjs api routes","server side patterns","data access layer","static file server","url path handling","file server"],"skill:browser-qa":["deployed feature test","visual regression screenshots","core web vitals check","axe accessibility audit","ship do not ship","staging verification"],"skill:canary-watch":["post deploy monitoring","smoke test url","production url check","console errors production","sse stream check","after deploy verification"],"skill:code-tour":["onboarding walkthrough","explain subsystem","architecture tour","pr walkthrough","rca tour"],"skill:coding-standards":["code review standards","naming conventions","readability review","immutability conventions","fix naming typo","export naming","consistent exports"],"skill:content-hash-cache-pattern":["cache file processing","content addressed cache","sha256 hash cache"],"skill:database-migrations":["zero downtime migration","schema change production","add column large table","backfill data","expand contract","concurrent index","migration rollback","prisma migration","django migration"],"skill:deployment-patterns":["ci cd setup","dockerize app","health checks","rollback strategy","production readiness","deploy pipeline","containerize application"],"skill:design-system":["design tokens","visual consistency audit","css custom properties","ui audit","design system bootstrap"],"skill:django-patterns":["django orm","drf api","django rest framework","django caching","django signals","django middleware"],"skill:django-security":["django authentication","csrf protection","sql injection prevention","xss prevention","django deployment security","role based access control","authorization middleware","permissions checks"],"skill:docker-patterns":["dockerfile review","docker compose setup","container security","multi service orchestration"],"skill:error-handling":["error types","retry logic","circuit breaker","user facing errors","exception handling patterns","typed errors","error boundaries","go error handling","custom error class","error codes","config validation"],"skill:evm-token-decimals":["token decimals","wei conversion","erc20 balance off","bridge token precision"],"skill:frontend-a11y":["aria attributes","screen reader support","focus management","semantic html","form labeling","keyboard navigation react","a11y lint errors"],"skill:git-workflow":["merge vs rebase","commit conventions","resolve merge conflict","branching strategy","clean up commits","pull request cleanup","git history tidy"],"skill:hexagonal-architecture":["ports and adapters","dependency injection boundaries","decouple domain from io"],"skill:kubernetes-patterns":["kubernetes manifests","kubectl debugging","pod probes","k8s rbac","autoscaling config","configmap secrets"],"skill:orch-fix-defect":["fix a bug","broken behavior","regression fix","reproduce bug","defect repair"],"skill:postgres-patterns":["slow postgres query","query optimization","index design","rls policies","supabase schema","postgres indexing","database performance","schema design postgres","postgres driver","node postgres","query planner"],"skill:python-patterns":["pythonic code","pep 8","type hints python","python code review","idiomatic python"],"skill:python-testing":["pytest fixtures","mocking python","parametrized tests","coverage python","tdd python"],"skill:redis-patterns":["cache aside pattern","distributed lock","redis rate limiting","cache invalidation"],"skill:regex-vs-llm-structured-text":["parse invoice","extract receipt data","text extraction pipeline","parse form fields","cheap document parser","extract table data","parse log lines","parse access logs","common log format","log line parsing"],"skill:rust-patterns":["rust ownership","borrow checker","rust error handling","traits rust","rust concurrency","idiomatic rust"],"skill:search-first":["find existing library","npm package research","before writing custom code","evaluate existing tools","add dependency research"],"skill:security-review":["security audit","authentication review","sanitize user input","secrets handling","payment security checklist","prevent injection attacks","secure api endpoints","authn authz review","vulnerability checklist","input validation security","parameterized queries","sql injection"],"skill:security-scan":["audit claude config","claudemd security","mcp server audit","agentshield scan","hook configuration audit","settings json security"],"skill:tdd-workflow":["write test first","failing test","red green refactor","test driven development","regression test first","write a regression test"],"skill:verification-loop":["pre pr checks","verification report","quality gates","build lint test coverage","before creating a pr"]},"triggersDigest":"25b97a9e06fc336c7cf95ab854ed1a41033a54bcd6e1fb1cf69dc906332462aa"} +{"coverage":{"skills":294,"withTriggers":33},"generatedAt":"2026-10-09T02:41:30.398Z","id":"skill-triggers@1","model":{"effort":null,"id":"hand-seeded","source":"manual-curation-pending-regeneration"},"registryDigest":"c2f1d0c8b46faed2714526ff69c598556d3ea1da0d9dbbb770107098bc41be3d","schemaVersion":1,"triggers":{"skill:agent-runtime-gateway-smoke-test":["test local agent gateway","remote sandbox callback smoke test","agent task stuck after dispatch","verify gateway tunnel","restore web api after agent test"],"skill:api-connector-builder":["add api integration","new provider connector","match existing integration pattern"],"skill:api-design":["rest endpoint design","pagination api","status codes","api versioning","rate limiting api","resource naming","filtering api","api error responses","offset pagination","limit query parameter","pagination defaults"],"skill:backend-patterns":["express api","node backend architecture","nextjs api routes","server side patterns","data access layer","static file server","url path handling","file server"],"skill:browser-qa":["deployed feature test","visual regression screenshots","core web vitals check","axe accessibility audit","ship do not ship","staging verification"],"skill:canary-watch":["post deploy monitoring","smoke test url","production url check","console errors production","sse stream check","after deploy verification"],"skill:code-tour":["onboarding walkthrough","explain subsystem","architecture tour","pr walkthrough","rca tour"],"skill:coding-standards":["code review standards","naming conventions","readability review","immutability conventions","fix naming typo","export naming","consistent exports"],"skill:content-hash-cache-pattern":["cache file processing","content addressed cache","sha256 hash cache"],"skill:database-migrations":["zero downtime migration","schema change production","add column large table","backfill data","expand contract","concurrent index","migration rollback","prisma migration","django migration"],"skill:deployment-patterns":["ci cd setup","dockerize app","health checks","rollback strategy","production readiness","deploy pipeline","containerize application"],"skill:design-system":["design tokens","visual consistency audit","css custom properties","ui audit","design system bootstrap"],"skill:django-patterns":["django orm","drf api","django rest framework","django caching","django signals","django middleware"],"skill:django-security":["django authentication","csrf protection","sql injection prevention","xss prevention","django deployment security","role based access control","authorization middleware","permissions checks"],"skill:docker-patterns":["dockerfile review","docker compose setup","container security","multi service orchestration"],"skill:error-handling":["error types","retry logic","circuit breaker","user facing errors","exception handling patterns","typed errors","error boundaries","go error handling","custom error class","error codes","config validation"],"skill:evm-token-decimals":["token decimals","wei conversion","erc20 balance off","bridge token precision"],"skill:frontend-a11y":["aria attributes","screen reader support","focus management","semantic html","form labeling","keyboard navigation react","a11y lint errors"],"skill:git-workflow":["merge vs rebase","commit conventions","resolve merge conflict","branching strategy","clean up commits","pull request cleanup","git history tidy"],"skill:hexagonal-architecture":["ports and adapters","dependency injection boundaries","decouple domain from io"],"skill:kubernetes-patterns":["kubernetes manifests","kubectl debugging","pod probes","k8s rbac","autoscaling config","configmap secrets"],"skill:orch-fix-defect":["fix a bug","broken behavior","regression fix","reproduce bug","defect repair"],"skill:postgres-patterns":["slow postgres query","query optimization","index design","rls policies","supabase schema","postgres indexing","database performance","schema design postgres","postgres driver","node postgres","query planner"],"skill:python-patterns":["pythonic code","pep 8","type hints python","python code review","idiomatic python"],"skill:python-testing":["pytest fixtures","mocking python","parametrized tests","coverage python","tdd python"],"skill:redis-patterns":["cache aside pattern","distributed lock","redis rate limiting","cache invalidation"],"skill:regex-vs-llm-structured-text":["parse invoice","extract receipt data","text extraction pipeline","parse form fields","cheap document parser","extract table data","parse log lines","parse access logs","common log format","log line parsing"],"skill:rust-patterns":["rust ownership","borrow checker","rust error handling","traits rust","rust concurrency","idiomatic rust"],"skill:search-first":["find existing library","npm package research","before writing custom code","evaluate existing tools","add dependency research"],"skill:security-review":["security audit","authentication review","sanitize user input","secrets handling","payment security checklist","prevent injection attacks","secure api endpoints","authn authz review","vulnerability checklist","input validation security","parameterized queries","sql injection"],"skill:security-scan":["audit claude config","claudemd security","mcp server audit","agentshield scan","hook configuration audit","settings json security"],"skill:tdd-workflow":["write test first","failing test","red green refactor","test driven development","regression test first","write a regression test"],"skill:verification-loop":["pre pr checks","verification report","quality gates","build lint test coverage","before creating a pr"]},"triggersDigest":"054898acb769893c46e5bd7037141f7be83fb775515ea1dbb52fb1c18aca19e6"} diff --git a/manifests/install-modules.json b/manifests/install-modules.json index b61d4e20f..47c5eeff8 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -855,6 +855,7 @@ "skills/autonomous-agent-harness", "skills/gan-style-harness", "skills/hermes-imports", + "skills/agent-runtime-gateway-smoke-test", "skills/openclaw-persona-forge", "skills/opensource-pipeline", "skills/orch-add-feature", diff --git a/package.json b/package.json index 983370b3a..d60604ca0 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,7 @@ "skills/agent-architecture-audit/", "skills/agent-harness-construction/", "skills/agent-introspection-debugging/", + "skills/agent-runtime-gateway-smoke-test/", "skills/agent-sort/", "skills/agentic-engineering/", "skills/agentic-os/", diff --git a/skills/agent-runtime-gateway-smoke-test/SKILL.md b/skills/agent-runtime-gateway-smoke-test/SKILL.md new file mode 100644 index 000000000..65231cbb6 --- /dev/null +++ b/skills/agent-runtime-gateway-smoke-test/SKILL.md @@ -0,0 +1,77 @@ +--- +name: agent-runtime-gateway-smoke-test +description: Verify a local agent API, temporary gateway tunnel, and remote sandbox callback with a tool-free task, then restore the original app connection. +metadata: + origin: ECC +--- + +# Agent Runtime Gateway Smoke Test + +Use this workflow when an agent task runs in a remote sandbox that calls back to a local API through a gateway. It verifies the whole path without relying on connected tools or private production data. + +## When to Activate + +- A local agent API needs an end-to-end runtime check. +- A task stalls after dispatch and the gateway or tunnel may be unreachable. +- A web app is temporarily pointed at a local API for agent testing. +- A previous test left the web app pointing at a stopped API. + +## Required Inputs + +- Repository-relative instructions for starting the API and web app. +- The gateway route path and its expected anonymous response. +- An isolated test agent, a tool-free prompt, and its exact expected answer. +- A way to inspect task status and confirm a callback reached the local API. +- Provider credentials, supplied through the project's normal secret store. Name the providers in shared documentation; never copy credential values into the skill or test report. + +If these inputs are unavailable, report the missing input instead of guessing an endpoint or using a real account. + +## Smoke Test + +1. **Record and check the original route.** Note the web app's current API target and verify it is healthy. Keep this value locally for restoration. Confirm the test agent and data are isolated from real users. +2. **Start the local API and tunnel.** Use a process manager that will keep both alive for the full test. Verify API health. Probe the gateway route anonymously and confirm the documented authentication response, usually `401` or `403`. Confirm the probe appears in the local API log; an edge response alone does not prove the API was reached. +3. **Switch the web app only after both checks pass.** Verify a request from the web app reaches the local API. Check the route again after any web server restart or configuration reload. +4. **Run one tool-free task.** Use a short prompt with an exact expected answer. Confirm the remote sandbox called the local gateway, the task reached a terminal state, and the returned answer matches. A queued or running task is not a pass. +5. **Restore the original route.** Set the web app back to its recorded API target, verify that target and a representative page load, then stop the test API and tunnel. If restoration fails, report the broken route immediately. + +Example checks, with values supplied by the project: + +```sh +curl --silent --show-error --output /dev/null --write-out '%{http_code}\n' "$LOCAL_API_HEALTH_URL" +curl --silent --show-error --output /dev/null --write-out '%{http_code}\n' "$PUBLIC_GATEWAY_PROBE_URL" +``` + +The first command should return the project's healthy status. The second should return the documented anonymous authentication status **and** appear in the local API log. Do not put a credential in either URL. + +## Connected Tool Follow-up + +Run a separate test only after the tool-free task passes. Use an explicit test connection and grants. Report connection presence and actions granted as different facts. Do not infer that an empty action search means no app is connected. Do not execute a write action against a real account as a smoke test. + +## Failure Handling + +| Observation | Next check | +| --- | --- | +| Web page loads but API requests fail | Compare the configured API target with the listening process and restore the original route. | +| Gateway probe gets a connection error or `404` | Check tunnel lifetime, route path, and local API health before dispatching a task. | +| Gateway probe gets `401` or `403`, but no local log entry | The response may come from the tunnel edge; verify the callback path. | +| Task stays queued or running | Check whether another dispatcher claimed it and whether the sandbox reached the local gateway. | +| Task finishes with the wrong answer | Capture the task status and redacted error; do not call the smoke test successful. | + +## Output Contract + +Return a short report with: + +- Local API healthy: yes or no +- Gateway probe status and local log confirmation +- Remote callback observed: yes or no +- Tool-free task terminal status and whether the exact answer matched +- Original web API route restored and healthy: yes or no +- One unresolved blocker or next action, if any + +Keep account details, credential values, personal paths, public tunnel URLs, and raw private logs out of the report. Keep the source operator notes local. + +## Related Skills + +- `hermes-imports` for sanitizing a local workflow before sharing it. +- `agent-introspection-debugging` when an agent run fails repeatedly. +- `verification-loop` after changing runtime code. From 11a970e11e5fb9a03254a013b4138c619eb9a1ed Mon Sep 17 00:00:00 2001 From: Beksultan Pirmatov Date: Fri, 9 Oct 2026 11:16:44 +0600 Subject: [PATCH 28/39] fix: detect Dart/Flutter projects in project-detect.js LANGUAGE_RULES had no entry for Dart, and FRAMEWORK_RULES had no way to detect Flutter, so any Flutter app (pubspec.yaml, lib/*.dart, android/, ios/) was reported as primary: "unknown" by the SessionStart project-type banner, despite ECC shipping a dart-flutter-patterns skill and a dart-build-resolver agent for this exact stack. Adds a 'dart' language rule (pubspec.yaml/pubspec.lock markers, .dart extension) and a 'flutter' framework rule that checks pubspec.yaml for the standard `flutter: / sdk: flutter` dependency shape, via a new customCheck hook on FRAMEWORK_RULES entries. This distinguishes Flutter apps from plain Dart packages, which don't depend on the Flutter SDK. Closes #3469 --- scripts/lib/project-detect.js | 35 ++++++++++++++++++++-- tests/lib/project-detect.test.js | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/scripts/lib/project-detect.js b/scripts/lib/project-detect.js index 98308fa0a..a7f625847 100644 --- a/scripts/lib/project-detect.js +++ b/scripts/lib/project-detect.js @@ -84,6 +84,11 @@ const LANGUAGE_RULES = [ type: 'php', markers: ['composer.json', 'composer.lock'], extensions: ['.php'] + }, + { + type: 'dart', + markers: ['pubspec.yaml', 'pubspec.lock'], + extensions: ['.dart'] } ]; @@ -129,7 +134,10 @@ const FRAMEWORK_RULES = [ { framework: 'symfony', language: 'php', markers: ['symfony.lock'], packageKeys: ['symfony/framework-bundle'] }, // Elixir frameworks - { framework: 'phoenix', language: 'elixir', markers: [], packageKeys: ['phoenix'] } + { framework: 'phoenix', language: 'elixir', markers: [], packageKeys: ['phoenix'] }, + + // Dart frameworks + { framework: 'flutter', language: 'dart', markers: [], packageKeys: [], customCheck: hasFlutterSdkDependency } ]; /** @@ -330,6 +338,27 @@ function getElixirDeps(projectDir) { } } +/** + * Check whether pubspec.yaml declares the Flutter SDK as a dependency, + * which distinguishes a Flutter app from a plain Dart package. + * @param {string} projectDir - Project root directory + * @returns {boolean} + */ +function hasFlutterSdkDependency(projectDir) { + try { + const pubspecPath = path.join(projectDir, 'pubspec.yaml'); + if (!fs.existsSync(pubspecPath)) return false; + const content = fs.readFileSync(pubspecPath, 'utf8'); + // Matches the standard Flutter pubspec shape: + // dependencies: + // flutter: + // sdk: flutter + return /^\s*flutter:\s*$/m.test(content) && /sdk:\s*flutter/.test(content); + } catch { + return false; + } +} + /** * Detect project languages and frameworks * @param {string} [projectDir] - Project directory (defaults to cwd) @@ -409,7 +438,9 @@ function detectProjectType(projectDir) { }); } - if (hasMarker || hasDep) { + const hasCustom = typeof rule.customCheck === 'function' ? rule.customCheck(projectDir) : false; + + if (hasMarker || hasDep || hasCustom) { frameworks.push(rule.framework); } } diff --git a/tests/lib/project-detect.test.js b/tests/lib/project-detect.test.js index 30727f360..100e1c6c3 100644 --- a/tests/lib/project-detect.test.js +++ b/tests/lib/project-detect.test.js @@ -493,6 +493,57 @@ function runTests() { } })) passed++; else failed++; + // Dart/Flutter detection + console.log('\nDart/Flutter Detection:'); + + if (test('detects dart from pubspec.yaml and .dart files', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'pubspec.yaml', 'name: test_pkg\nversion: 1.0.0\n'); + writeTestFile(dir, 'main.dart', 'void main() {}\n'); + const result = detectProjectType(dir); + assert.ok(result.languages.includes('dart')); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + + if (test('detects flutter framework when pubspec.yaml depends on the Flutter SDK', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'pubspec.yaml', [ + 'name: halal_cosmetics_app', + 'dependencies:', + ' flutter:', + ' sdk: flutter', + ' cupertino_icons: ^1.0.2' + ].join('\n')); + const result = detectProjectType(dir); + assert.ok(result.languages.includes('dart')); + assert.ok(result.frameworks.includes('flutter'), `flutter not detected: ${JSON.stringify(result.frameworks)}`); + assert.strictEqual(result.primary, 'flutter'); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + + if (test('does not misclassify a plain Dart package as flutter', () => { + const dir = createTempDir(); + try { + writeTestFile(dir, 'pubspec.yaml', [ + 'name: plain_dart_pkg', + 'dependencies:', + ' http: ^1.0.0' + ].join('\n')); + const result = detectProjectType(dir); + assert.ok(result.languages.includes('dart')); + assert.ok(!result.frameworks.includes('flutter'), `plain Dart package wrongly detected as flutter: ${JSON.stringify(result.frameworks)}`); + assert.strictEqual(result.primary, 'dart'); + } finally { + cleanupDir(dir); + } + })) passed++; else failed++; + // Edge cases console.log('\nEdge Cases:'); From aba3c241af4da77d22dd56e5d018107b5242cbcf Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Fri, 9 Oct 2026 16:12:32 -0400 Subject: [PATCH 29/39] test(carriers): pin exact import resolution, portable separators, and regex after a condition Review on aec97150 found four ways the closure gate passes a carrier that fails at run time. Each is reproduced here against the current head: - import('./helper') with only helper.js planned is accepted, but Node's ESM resolver does no extension search. - import('./data') with only data/index.json (or index.js) planned is accepted, but Node rejects directory imports even from CommonJS. - require('./lib\helper.js') with lib/helper.js planned is accepted because backslashes were normalized, but Linux and macOS read the backslash as part of the filename. - In `if (ok) /['"]/.test(text); require('./missing')` the regex after the condition's closing paren is read as division, its quote opens a fake string, and the require disappears. Controls that must keep passing: an exact import() of a planned file, an extensionless require through the CommonJS search, and division after a method named `if` or a call nested inside a condition. extractReferences gains an `imports` list so the resolver can tell the two forms apart. --- .../scripts/validate-carrier-closure.test.js | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/scripts/validate-carrier-closure.test.js b/tests/scripts/validate-carrier-closure.test.js index 5b660914b..22430c0f0 100644 --- a/tests/scripts/validate-carrier-closure.test.js +++ b/tests/scripts/validate-carrier-closure.test.js @@ -87,6 +87,46 @@ test('a directory require resolves through index.json', () => withFixture(root = assert.equal(result.status, 'success', JSON.stringify(result.failures)); })); +// Node resolves import() and static imports by exact path: no extension +// search and no directory index, even when the calling file is CommonJS. +for (const [label, planned, source] of [ + ['an extensionless import()', 'helper.js', "module.exports = import('./helper');\n"], + ['a directory import() through index.json', 'data/index.json', "module.exports = import('./data');\n"], + ['a directory import() through index.js', 'data/index.js', "module.exports = import('./data');\n"], + ['an extensionless static import', 'helper.js', "import helper from './helper';\nexport default helper;\n"], +]) { + test(`${label} fails even when the require form would resolve`, () => withFixture(root => { + write(root, `skills/ecc-guide/scripts/${planned}`, planned.endsWith('.json') ? '{}\n' : 'module.exports = 1;\n'); + plantSkillScript(root, source); + const result = validator().validate(root); + assert.equal(result.status, 'failure'); + assert.ok(result.failures.every(failure => failure.reason === 'unplanned-target')); + })); +} + +test('an import() naming the planned file exactly resolves', () => withFixture(root => { + write(root, 'skills/ecc-guide/scripts/helper.js', 'module.exports = 1;\n'); + plantSkillScript(root, "module.exports = import('./helper.js');\n"); + const result = validator().validate(root); + assert.equal(result.status, 'success', JSON.stringify(result.failures)); +})); + +test('an extensionless require still resolves through the CommonJS extension search', () => withFixture(root => { + write(root, 'skills/ecc-guide/scripts/helper.js', 'module.exports = 1;\n'); + plantSkillScript(root, "module.exports = require('./helper');\n"); + const result = validator().validate(root); + assert.equal(result.status, 'success', JSON.stringify(result.failures)); +})); + +test('a backslash-separated relative require fails: Linux and macOS read it as one filename', () => withFixture(root => { + write(root, 'skills/ecc-guide/scripts/lib/helper.js', 'module.exports = 1;\n'); + plantSkillScript(root, "module.exports = require('./lib\\\\helper.js');\n"); + const result = validator().validate(root); + assert.equal(result.status, 'failure'); + assert.ok(specifiersOf(result.failures).includes('./lib\\helper.js')); + assert.ok(result.failures.every(failure => failure.reason === 'non-portable-separator')); +})); + test('a package import warns because carriers ship no node_modules; builtins do not', () => withFixture(root => { plantSkillScript(root, "'use strict';\n\nconst yaml = require('js-yaml');\nconst { x } = require('@scope/pkg/deep');\n" + "const fs = require('node:fs');\nconst path = require('path');\nmodule.exports = { yaml, x, fs, path };\n"); @@ -144,6 +184,10 @@ const SCANNER_CASES = [ ['unicode escape in a specifier', "require('\\u002e./escaped');", ['../escaped']], ['hex and braced escapes in a specifier', "require('\\x2e/a'); require(`\\u{2e}/b`);", ['./a', './b']], ['escaped quote and backslash', "require('./it\\'s'); require('.\\\\win');", ["./it's", '.\\win']], + ['regex after an if condition', "if (ok) /['\"]/.test(text); require('./missing');", ['./missing']], + ['regex after a while condition', "while (more) /\"/.test(s); require('./after-while');", ['./after-while']], + ['division after a method named if', "const v = a.if(x) / 2, q = \"/\"; require('./after-method');", ['./after-method']], + ['division after a call inside a condition', "if (f(a) / 2 > 1) { q = \"/\"; } require('./after-nested');", ['./after-nested']], ]; for (const [label, source, expected] of SCANNER_CASES) { @@ -154,7 +198,14 @@ for (const [label, source, expected] of SCANNER_CASES) { test('scanner: an identifier named from before a template chunk is not an import', () => { assert.deepEqual(validator().extractReferences('const s = `${from} is shipped as ${to}`;'), - { specifiers: [], packages: [], dynamic: [] }); + { specifiers: [], imports: [], packages: [], dynamic: [] }); +}); + +test('scanner: import() and static imports are reported apart from require', () => { + const { specifiers, imports } = validator().extractReferences( + "require('./a'); import('./b'); import c from './c'; import './d'; require(path.join(__dirname, 'e.js'));"); + assert.deepEqual(specifiers.sort(), ['./a', './b', './c', './d', './e.js']); + assert.deepEqual(imports.sort(), ['./b', './c', './d']); }); test('unknown flags are rejected', () => { From fa3f459a93297e205eb18f9f80643987d31fc56f Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Fri, 9 Oct 2026 16:15:55 -0400 Subject: [PATCH 30/39] fix(carriers): resolve imports exactly, reject backslash separators, see regex after a condition - Track whether each relative specifier arrives through require or through import()/static import, and resolve the import forms by exact path. Node's ESM resolver adds no extension and rejects directory imports even from a CommonJS file, so import('./helper') and import('./data') no longer pass on helper.js or data/index.json alone. require keeps the CommonJS extension and index candidates. - Stop normalizing backslashes in relative specifiers. Linux and macOS read them as filename characters, so require('./lib\helper.js') now fails as non-portable-separator instead of resolving to lib/helper.js. - Record whether each closing paren ends an if/while/for/with condition, so a `/` right after one starts a regex. A method named `if` and calls nested inside a condition still read as division. extractReferences gains an `imports` list. Rescanning all 802 repository JavaScript files against the previous scanner changes no result, and the real registry still reports 35/35 resolved with zero failures. --- scripts/ci/validate-carrier-closure.js | 67 +++++++++++++++++--------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/scripts/ci/validate-carrier-closure.js b/scripts/ci/validate-carrier-closure.js index 6005aed05..08114284c 100644 --- a/scripts/ci/validate-carrier-closure.js +++ b/scripts/ci/validate-carrier-closure.js @@ -26,7 +26,7 @@ const BUILTINS = new Set(builtinModules); const SCRIPT_EXTENSIONS = new Set(['.js', '.cjs', '.mjs']); const MARK = '\u0000'; const SLOT = `${MARK}(\\d+)${MARK}`; -const CALL_PATTERN = new RegExp(`\\b(?:require|import)\\s*\\(\\s*${SLOT}\\s*\\)`, 'g'); +const CALL_PATTERN = new RegExp(`\\b(require|import)\\s*\\(\\s*${SLOT}\\s*\\)`, 'g'); const FROM_PATTERN = new RegExp(`\\bfrom\\s*${SLOT}`, 'g'); const SIDE_EFFECT_PATTERN = new RegExp(`\\bimport\\s*${SLOT}`, 'g'); const DIRNAME_JOIN_PATTERN = new RegExp( @@ -40,6 +40,8 @@ const ESCAPE_PATTERN = /\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F] const SIMPLE_ESCAPES = Object.freeze({ n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v', 0: '\0' }); const LINE_CONTINUATIONS = new Set(['\n', '\r', '\r\n', '\u2028', '\u2029']); const REGEX_PRECEDES = /(?:[([{,;:=!&|?+\-*/%~^<>]|\b(?:return|typeof|case|in|of|new|delete|void|instanceof|do|else|yield|await))$/; +// A `(` opening a statement condition; `a.if(` is a method call, not one. +const CONTROL_OPENS = /(?:^|[^.\w$])(?:if|while|for|with)$/; /** Cook a raw string or template chunk the way the JavaScript parser would. */ function decodeEscapes(raw) { @@ -84,6 +86,10 @@ function scanSource(rawSource) { const source = String(rawSource || ''); const literals = []; const stack = []; + // Whether each open paren began a statement condition, and whether the + // last one closed did: `/` after `if (...)` starts a regex, not division. + const parens = []; + let closedCondition = false; let code = ''; let index = 0; const slot = value => { code += `${MARK}${literals.push(decodeEscapes(value)) - 1}${MARK}`; }; @@ -117,7 +123,7 @@ function scanSource(rawSource) { index = index === -1 ? source.length : index + 2; continue; } - if (char === '/' && REGEX_PRECEDES.test(code.trimEnd())) { + if (char === '/' && (REGEX_PRECEDES.test(code.trimEnd()) || (closedCondition && code.trimEnd().endsWith(')')))) { index = readRegexLiteral(source, index); code += ' '; continue; @@ -142,6 +148,8 @@ function scanSource(rawSource) { open.depth -= 1; } } + if (char === '(') parens.push(CONTROL_OPENS.test(code.trimEnd())); + else if (char === ')') closedCondition = parens.pop() === true; code += char; index += 1; } @@ -165,23 +173,27 @@ function renderExpression(text, literals) { /** * Extract the relative module specifiers a script declares, plus the * non-literal `require`/`import` arguments that cannot be resolved without - * running the code. + * running the code. `imports` lists the specifiers reached through `import()` + * or static `import`/`from`, which Node resolves by exact path. */ function extractReferences(rawSource) { const { code, literals } = scanSource(rawSource); const specifiers = new Set(); + const imports = new Set(); const packages = new Set(); const dynamic = new Set(); - const literalAt = match => literals[Number(match[1])]; - for (const pattern of [CALL_PATTERN, FROM_PATTERN, SIDE_EFFECT_PATTERN]) { - collect(pattern, code, match => { - const value = literalAt(match); - if (value.startsWith('.') || isAbsoluteSpecifier(value)) specifiers.add(value); - else if (!value.startsWith('node:') && !BUILTINS.has(value.split('/')[0])) { - packages.add(value.startsWith('@') ? value.split('/').slice(0, 2).join('/') : value.split('/')[0]); - } - }); + const record = (value, viaImport) => { + if (value.startsWith('.') || isAbsoluteSpecifier(value)) { + specifiers.add(value); + if (viaImport) imports.add(value); + } else if (!value.startsWith('node:') && !BUILTINS.has(value.split('/')[0])) { + packages.add(value.startsWith('@') ? value.split('/').slice(0, 2).join('/') : value.split('/')[0]); + } + }; + collect(CALL_PATTERN, code, match => record(literals[Number(match[2])], match[1] === 'import')); + for (const pattern of [FROM_PATTERN, SIDE_EFFECT_PATTERN]) { + collect(pattern, code, match => record(literals[Number(match[1])], true)); } collect(DIRNAME_JOIN_PATTERN, code, match => { const segments = []; @@ -193,17 +205,25 @@ function extractReferences(rawSource) { if (new RegExp(`^${SLOT}$`).test(argument) || STATIC_JOIN_PATTERN.test(argument)) return; dynamic.add(renderExpression(argument, literals)); }); - return { specifiers: [...specifiers], packages: [...packages], dynamic: [...dynamic] }; + return { specifiers: [...specifiers], imports: [...imports], packages: [...packages], dynamic: [...dynamic] }; } -function candidateDestinations(destinationPath, specifier) { - if (isAbsoluteSpecifier(specifier)) return { base: specifier, candidates: null }; - const base = path.posix.join(path.posix.dirname(destinationPath), specifier.replace(/\\/g, '/')); - if (base === '..' || base.startsWith('../')) return { base, candidates: null }; - const candidates = destinationPath.endsWith('.mjs') +/** + * The planned paths that would satisfy a specifier, or the failure reason + * when none can. CommonJS `require` searches extensions and directory + * indexes; ESM (`import`, or any `.mjs` file) resolves only the exact path. + * A relative backslash is not a separator on Linux or macOS, so it is + * rejected rather than normalized. + */ +function candidateDestinations(destinationPath, specifier, exact) { + if (isAbsoluteSpecifier(specifier)) return { base: specifier, candidates: null, reason: 'outside-carrier-tree' }; + if (specifier.includes('\\')) return { base: specifier, candidates: null, reason: 'non-portable-separator' }; + const base = path.posix.join(path.posix.dirname(destinationPath), specifier); + if (base === '..' || base.startsWith('../')) return { base, candidates: null, reason: 'outside-carrier-tree' }; + const candidates = exact ? [base] : [base, `${base}.js`, `${base}.cjs`, `${base}.mjs`, `${base}.json`, `${base}/index.js`, `${base}/index.json`]; - return { base, candidates }; + return { base, candidates, reason: null }; } function sourceReader(repoRoot) { @@ -221,16 +241,19 @@ function inspectProjection(carrier, profileId, read, report) { const planned = new Set(carrier.files.map(file => file.destinationPath)); const scripts = carrier.files.filter(file => SCRIPT_EXTENSIONS.has(path.posix.extname(file.destinationPath))); for (const file of scripts) { - const { specifiers, packages, dynamic } = extractReferences(read(file)); + const { specifiers, imports, packages, dynamic } = extractReferences(read(file)); + const exactOnly = new Set(imports); + const isModule = file.destinationPath.endsWith('.mjs'); const location = { target: carrier.target, layout: carrier.layout.id, profileId, file: file.destinationPath, source: file.sourcePath || null, }; for (const specifier of specifiers) { report.specifierCount += 1; - const { base, candidates } = candidateDestinations(file.destinationPath, specifier); + const exact = isModule || exactOnly.has(specifier); + const { base, candidates, reason } = candidateDestinations(file.destinationPath, specifier, exact); if (!candidates) { - report.failures.push({ ...location, specifier, resolved: base, reason: 'outside-carrier-tree' }); + report.failures.push({ ...location, specifier, resolved: base, reason }); } else if (candidates.some(candidate => planned.has(candidate))) { report.resolvedCount += 1; } else { From dd14493e988522b54d9b3be2259931fe30db851e Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Fri, 9 Oct 2026 16:15:55 -0400 Subject: [PATCH 31/39] docs(carriers): record the second closure review follow-up --- docs/design/context-carriers.tdd.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/design/context-carriers.tdd.md b/docs/design/context-carriers.tdd.md index e3df49d81..f9ef8273b 100644 --- a/docs/design/context-carriers.tdd.md +++ b/docs/design/context-carriers.tdd.md @@ -89,10 +89,12 @@ Zero escapes is the expected result and not evidence the check is idle. Carriers Absolute specifiers fail as `outside-carrier-tree`. A package import warns rather than fails, because carriers ship no `node_modules`; Node builtins, bare or `node:`-prefixed, are exempt. No skill script imports a package today. -Limits. Nothing is executed; the check reads bytes and resolves paths. A package warning names the dependency but cannot tell whether the host environment provides it. Resolution assumes CommonJS extension candidates for `.js` and `.cjs` and exact specifiers for `.mjs`; it does not model conditional exports, a nested `package.json` type field, or import maps. A non-literal `require` is reported, not resolved, so a carrier can still fail at load time through a path computed at runtime. Native discovery, executable mode and actual loading remain unobserved, exactly as before. +Limits. Nothing is executed; the check reads bytes and resolves paths. A package warning names the dependency but cannot tell whether the host environment provides it. Resolution assumes CommonJS extension and directory-index candidates for `require` in `.js` and `.cjs`, and the exact specifier for `import()`, static `import`/`from`, and anything in `.mjs`; it does not model conditional exports, a nested `package.json` type field, or import maps. A non-literal `require` is reported, not resolved, so a carrier can still fail at load time through a path computed at runtime. Native discovery, executable mode and actual loading remain unobserved, exactly as before. The scanner is pinned by twelve edge cases (regex versus division, regex classes and quotes, template interpolations with object braces, block comments, multi-line and re-exported `from`, and `path.join(__dirname, …)`). Across all 655 JavaScript files in the repository it found every relative specifier a plain pattern match finds outside comments and strings, with no scanner errors. The suite validates the real registry once, through the CLI, and shares that receipt between its real-registry cases: 4.8 s per run instead of 9.1 s, which matters because the test job runs in about 33 matrix cells. Review follow-up. A third RED added 10 cases from review: spaced calls (`require ('./x')`, `require\n(...)`, `import (...)`, and `path.join (__dirname, ...)`), escaped specifiers (`\u002e`, `\x2e`, `\u{2e}`, escaped quotes and backslashes), Windows drive, UNC and `file:` URL specifiers, and a directory resolving through `index.json`. All 10 failed. GREEN cooks string and template escapes as the parser does, allows whitespace before call parentheses, treats drive, UNC and `file:` specifiers as absolute, normalizes backslash separators in relative specifiers and adds the `index.json` candidate. A fourth RED/GREEN stops `from` and `import` patterns matching across a closed `${...}` interpolation. Rescanning all 802 repository JavaScript files against the previous scanner changed only three results, each a false package warning removed (`${from}` and similar identifiers before a template chunk). The real registry still reports 35/35 resolved, zero escapes, warnings or package imports, in 3.5 s. -Focused verification: 22 carrier cases, 12 compiler cases, 33 closure cases, 3 CI cases and 2 publish-surface cases pass with zero failures or skips. ESLint passes on the validator and the new test. The validator is CI-only and stays out of the packed surface, matching `scripts/ci/validate-context-profiles.js`. +Second review follow-up. Review of that head found four ways the gate still passed a carrier that fails at run time, each reproduced before any change: an extensionless `import('./helper')` and a directory `import('./data')` resolved through the CommonJS candidates, although Node's ESM resolver does neither even from a CommonJS file; `require('./lib\\helper.js')` resolved to `lib/helper.js`, although Linux and macOS read the backslash as part of the filename; and in `if (ok) /['"]/.test(text); require('./missing')` the regex after the condition was read as division, so its quote opened a fake string that swallowed the `require`. A fifth RED added 12 cases, 9 failing: the four findings, the two regex conditions, and the `imports` shape. The three controls already passed: an exact `import()`, an extensionless `require`, and division after `a.if(x)` or a call inside a condition. GREEN records which specifiers arrive through `import` and resolves those by exact path. It reverses the earlier backslash normalization: a relative specifier containing a backslash now fails as `non-portable-separator`. It also tracks whether each closing paren ends an `if`, `while`, `for` or `with` condition. Rescanning all 802 repository JavaScript files against the previous scanner changed no result; 18 of them declare `import` specifiers. The real registry still reports 35/35 resolved and zero escapes, warnings or package imports, unchanged on every receipt count. + +Focused verification: 22 carrier cases, 12 compiler cases, 45 closure cases, 3 CI cases and 2 publish-surface cases pass with zero failures or skips. ESLint passes on the validator and the new test. The validator is CI-only and stays out of the packed surface, matching `scripts/ci/validate-context-profiles.js`. From dec9774791ac148500d8a7eabacd28f09b64cdf6 Mon Sep 17 00:00:00 2001 From: Eneko Ruiz <196690101+eneekoruiz@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:24:12 +0200 Subject: [PATCH 32/39] docs(csharp): recommend AwesomeAssertions for readable assertions --- rules/csharp/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/csharp/testing.md b/rules/csharp/testing.md index a00f0127d..f0242fe7d 100644 --- a/rules/csharp/testing.md +++ b/rules/csharp/testing.md @@ -11,7 +11,7 @@ paths: ## Test Framework - Prefer **xUnit** for unit and integration tests -- Use **FluentAssertions** for readable assertions +- Use **AwesomeAssertions** for readable assertions - Use **Moq** or **NSubstitute** for mocking dependencies - Use **Testcontainers** when integration tests need real infrastructure From 253c051dbfefc42d427f659eac48674f05e477ac Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Fri, 9 Oct 2026 17:46:02 -0400 Subject: [PATCH 33/39] test(carriers): pin CommonJS require candidates, regex after a block, and non-import from Review on dd14493e found four more ways the closure gate misjudges a carrier, each reproduced against that head: - require('./helper') resolves when only helper.cjs or helper.mjs is planned, but Node's CommonJS search tries only .js, .json and .node. - In `if (ok) {} /"/.test(text); require(...)` the regex after the block is read as division and its quote swallows the require. - `obj.\nif(x) / 2` is a method call, but the condition check only saw the character immediately before `if`, so the division read as a regex and swallowed a later require. Introduced by the previous fix. - A tagged template named `from` was recorded as an import; with exact import resolution it now fails a carrier that never loads that path. Introduced by the previous fix. Controls: an extensionless require still resolves to a .json file, and a template literal passed to import() is still a dependency. --- .../scripts/validate-carrier-closure.test.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/scripts/validate-carrier-closure.test.js b/tests/scripts/validate-carrier-closure.test.js index 22430c0f0..5c3c25a04 100644 --- a/tests/scripts/validate-carrier-closure.test.js +++ b/tests/scripts/validate-carrier-closure.test.js @@ -118,6 +118,32 @@ test('an extensionless require still resolves through the CommonJS extension sea assert.equal(result.status, 'success', JSON.stringify(result.failures)); })); +// An extensionless require searches only .js, .json and .node, then a +// directory's index; .cjs and .mjs must be named explicitly. +for (const extension of ['cjs', 'mjs']) { + test(`an extensionless require does not resolve to a .${extension} file`, () => withFixture(root => { + write(root, `skills/ecc-guide/scripts/helper.${extension}`, 'module.exports = 1;\n'); + plantSkillScript(root, "module.exports = require('./helper');\n"); + const result = validator().validate(root); + assert.equal(result.status, 'failure'); + assert.ok(result.failures.every(failure => failure.reason === 'unplanned-target')); + })); +} + +test('an extensionless require resolves to a .json file', () => withFixture(root => { + write(root, 'skills/ecc-guide/scripts/helper.json', '{}\n'); + plantSkillScript(root, "module.exports = require('./helper');\n"); + const result = validator().validate(root); + assert.equal(result.status, 'success', JSON.stringify(result.failures)); +})); + +test('a tagged template named from is not an import', () => withFixture(root => { + plantSkillScript(root, 'const from = parts => parts[0];\nmodule.exports = from`./helper`;\n'); + const result = validator().validate(root); + assert.equal(result.status, 'success', JSON.stringify(result.failures)); + assert.deepEqual(result.warnings, []); +})); + test('a backslash-separated relative require fails: Linux and macOS read it as one filename', () => withFixture(root => { write(root, 'skills/ecc-guide/scripts/lib/helper.js', 'module.exports = 1;\n'); plantSkillScript(root, "module.exports = require('./lib\\\\helper.js');\n"); @@ -188,6 +214,13 @@ const SCANNER_CASES = [ ['regex after a while condition', "while (more) /\"/.test(s); require('./after-while');", ['./after-while']], ['division after a method named if', "const v = a.if(x) / 2, q = \"/\"; require('./after-method');", ['./after-method']], ['division after a call inside a condition', "if (f(a) / 2 > 1) { q = \"/\"; } require('./after-nested');", ['./after-nested']], + ['regex after a block', "if (ok) {} /\"/.test(text); require('./after-block');", ['./after-block']], + ['division after a method named if across a line break', + "const n = obj.\nif(x) / 2, q = \"/\"; require('./after-spaced-method');", ['./after-spaced-method']], + ['division after an optional-chained method named while', + "const n = obj?. while(x) / 2, q = \"/\"; require('./after-optional');", ['./after-optional']], + ['a tagged template named from', 'const from = p => p[0]; const s = from`./helper`;', []], + ['a template import() is still a dependency', 'import(`./templated`);', ['./templated']], ]; for (const [label, source, expected] of SCANNER_CASES) { From bdb2ceb230cd0dde9de22360d01476d410f0faae Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Fri, 9 Oct 2026 17:48:27 -0400 Subject: [PATCH 34/39] fix(carriers): follow Node's CommonJS candidates, see regex after a block, ignore non-import from - An extensionless require now tries only the path, .js, .json, and a directory's index.js/index.json, as Node does; .cjs and .mjs must be named, so require('./helper') no longer passes on helper.cjs alone. - Treat `}` as a statement boundary, so a regex after a block is read as a regex and cannot swallow a later require. - Decide whether a paren opens a condition from the token before the keyword, ignoring whitespace, so `obj.\nif(x) / 2` and `obj?. while(x) / 2` stay method calls followed by division. - Record which slots came from template literals and skip them for static `from` and side-effect `import`, which accept only a quoted string; a tagged template named `from` is no longer an import. A template passed to import() is still a dependency. Rescanning all 802 repository JavaScript files against dd14493e changes no result, and the real registry still reports 35/35 resolved with zero failures or warnings. --- scripts/ci/validate-carrier-closure.js | 49 ++++++++++++++++++-------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/scripts/ci/validate-carrier-closure.js b/scripts/ci/validate-carrier-closure.js index 08114284c..cea037909 100644 --- a/scripts/ci/validate-carrier-closure.js +++ b/scripts/ci/validate-carrier-closure.js @@ -39,9 +39,9 @@ const CLOSE = `${MARK}${MARK}`; const ESCAPE_PATTERN = /\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2})|(\r\n|[\s\S]))/g; const SIMPLE_ESCAPES = Object.freeze({ n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v', 0: '\0' }); const LINE_CONTINUATIONS = new Set(['\n', '\r', '\r\n', '\u2028', '\u2029']); -const REGEX_PRECEDES = /(?:[([{,;:=!&|?+\-*/%~^<>]|\b(?:return|typeof|case|in|of|new|delete|void|instanceof|do|else|yield|await))$/; -// A `(` opening a statement condition; `a.if(` is a method call, not one. -const CONTROL_OPENS = /(?:^|[^.\w$])(?:if|while|for|with)$/; +// `}` ends a block, so a slash after it starts a statement: a regex. +const REGEX_PRECEDES = /(?:[([{},;:=!&|?+\-*/%~^<>]|\b(?:return|typeof|case|in|of|new|delete|void|instanceof|do|else|yield|await))$/; +const CONDITION_KEYWORD = /(?:^|[^\w$])(if|while|for|with)$/; /** Cook a raw string or template chunk the way the JavaScript parser would. */ function decodeEscapes(raw) { @@ -61,6 +61,16 @@ function isAbsoluteSpecifier(value) { return value.startsWith('/') || value.startsWith('\\\\') || /^[A-Za-z]:[\\/]/.test(value) || /^file:/i.test(value); } +/** + * Whether code ends with a keyword whose `(` opens a statement condition. + * `obj.if(`, `obj?. while(` and `obj.` + newline + `if(` are method calls, + * so the token before the keyword, ignoring whitespace, must not be a dot. + */ +function opensCondition(code) { + const match = CONDITION_KEYWORD.exec(code); + return Boolean(match) && !code.slice(0, code.length - match[1].length).trimEnd().endsWith('.'); +} + function readRegexLiteral(source, start) { let index = start + 1; let inClass = false; @@ -92,7 +102,13 @@ function scanSource(rawSource) { let closedCondition = false; let code = ''; let index = 0; - const slot = value => { code += `${MARK}${literals.push(decodeEscapes(value)) - 1}${MARK}`; }; + // Slots cut from template literals; `from` and `import` need a quoted string. + const templates = new Set(); + const slot = (value, fromTemplate = false) => { + const slotIndex = literals.push(decodeEscapes(value)) - 1; + if (fromTemplate) templates.add(slotIndex); + code += `${MARK}${slotIndex}${MARK}`; + }; const frame = () => (stack.length ? stack[stack.length - 1] : null); while (index < source.length) { @@ -102,9 +118,9 @@ function scanSource(rawSource) { if (open && open.type === 'template') { if (char === '\\') { open.text += source.substr(index, 2); index += 2; continue; } - if (char === '`') { stack.pop(); slot(open.text); index += 1; continue; } + if (char === '`') { stack.pop(); slot(open.text, true); index += 1; continue; } if (char === '$' && next === '{') { - slot(open.text); + slot(open.text, true); open.text = ''; stack.push({ type: 'interpolation', depth: 0 }); index += 2; @@ -148,13 +164,13 @@ function scanSource(rawSource) { open.depth -= 1; } } - if (char === '(') parens.push(CONTROL_OPENS.test(code.trimEnd())); + if (char === '(') parens.push(opensCondition(code.trimEnd())); else if (char === ')') closedCondition = parens.pop() === true; code += char; index += 1; } - if (frame() && frame().type === 'template') slot(frame().text); - return { code, literals }; + if (frame() && frame().type === 'template') slot(frame().text, true); + return { code, literals, templates }; } function collect(pattern, code, handler) { @@ -177,7 +193,7 @@ function renderExpression(text, literals) { * or static `import`/`from`, which Node resolves by exact path. */ function extractReferences(rawSource) { - const { code, literals } = scanSource(rawSource); + const { code, literals, templates } = scanSource(rawSource); const specifiers = new Set(); const imports = new Set(); const packages = new Set(); @@ -192,8 +208,12 @@ function extractReferences(rawSource) { } }; collect(CALL_PATTERN, code, match => record(literals[Number(match[2])], match[1] === 'import')); + // Static `from` and side-effect `import` take only a quoted string, so a + // template there (a tag named `from`, say) is not an import. for (const pattern of [FROM_PATTERN, SIDE_EFFECT_PATTERN]) { - collect(pattern, code, match => record(literals[Number(match[1])], true)); + collect(pattern, code, match => { + if (!templates.has(Number(match[1]))) record(literals[Number(match[1])], true); + }); } collect(DIRNAME_JOIN_PATTERN, code, match => { const segments = []; @@ -210,8 +230,9 @@ function extractReferences(rawSource) { /** * The planned paths that would satisfy a specifier, or the failure reason - * when none can. CommonJS `require` searches extensions and directory - * indexes; ESM (`import`, or any `.mjs` file) resolves only the exact path. + * when none can. CommonJS `require` tries the path, then `.js` and `.json`, + * then a directory's `index.js` and `index.json` (`.cjs` and `.mjs` must be + * named); ESM (`import`, or any `.mjs` file) resolves only the exact path. * A relative backslash is not a separator on Linux or macOS, so it is * rejected rather than normalized. */ @@ -222,7 +243,7 @@ function candidateDestinations(destinationPath, specifier, exact) { if (base === '..' || base.startsWith('../')) return { base, candidates: null, reason: 'outside-carrier-tree' }; const candidates = exact ? [base] - : [base, `${base}.js`, `${base}.cjs`, `${base}.mjs`, `${base}.json`, `${base}/index.js`, `${base}/index.json`]; + : [base, `${base}.js`, `${base}.json`, `${base}/index.js`, `${base}/index.json`]; return { base, candidates, reason: null }; } From 0ca7864ad7aa59bb52ac12db6fc0fa34f1f85feb Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Fri, 9 Oct 2026 17:48:50 -0400 Subject: [PATCH 35/39] docs(carriers): record the third closure review follow-up --- docs/design/context-carriers.tdd.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/design/context-carriers.tdd.md b/docs/design/context-carriers.tdd.md index f9ef8273b..405cb1905 100644 --- a/docs/design/context-carriers.tdd.md +++ b/docs/design/context-carriers.tdd.md @@ -89,7 +89,7 @@ Zero escapes is the expected result and not evidence the check is idle. Carriers Absolute specifiers fail as `outside-carrier-tree`. A package import warns rather than fails, because carriers ship no `node_modules`; Node builtins, bare or `node:`-prefixed, are exempt. No skill script imports a package today. -Limits. Nothing is executed; the check reads bytes and resolves paths. A package warning names the dependency but cannot tell whether the host environment provides it. Resolution assumes CommonJS extension and directory-index candidates for `require` in `.js` and `.cjs`, and the exact specifier for `import()`, static `import`/`from`, and anything in `.mjs`; it does not model conditional exports, a nested `package.json` type field, or import maps. A non-literal `require` is reported, not resolved, so a carrier can still fail at load time through a path computed at runtime. Native discovery, executable mode and actual loading remain unobserved, exactly as before. +Limits. Nothing is executed; the check reads bytes and resolves paths. A package warning names the dependency but cannot tell whether the host environment provides it. Resolution assumes Node's CommonJS candidates for `require` in `.js` and `.cjs` (the path, `.js`, `.json`, then a directory's `index.js` or `index.json`), and the exact specifier for `import()`, static `import`/`from`, and anything in `.mjs`; it does not model conditional exports, a nested `package.json` type field, or import maps. A non-literal `require` is reported, not resolved, so a carrier can still fail at load time through a path computed at runtime. Native discovery, executable mode and actual loading remain unobserved, exactly as before. The scanner is pinned by twelve edge cases (regex versus division, regex classes and quotes, template interpolations with object braces, block comments, multi-line and re-exported `from`, and `path.join(__dirname, …)`). Across all 655 JavaScript files in the repository it found every relative specifier a plain pattern match finds outside comments and strings, with no scanner errors. The suite validates the real registry once, through the CLI, and shares that receipt between its real-registry cases: 4.8 s per run instead of 9.1 s, which matters because the test job runs in about 33 matrix cells. @@ -97,4 +97,6 @@ Review follow-up. A third RED added 10 cases from review: spaced calls (`require Second review follow-up. Review of that head found four ways the gate still passed a carrier that fails at run time, each reproduced before any change: an extensionless `import('./helper')` and a directory `import('./data')` resolved through the CommonJS candidates, although Node's ESM resolver does neither even from a CommonJS file; `require('./lib\\helper.js')` resolved to `lib/helper.js`, although Linux and macOS read the backslash as part of the filename; and in `if (ok) /['"]/.test(text); require('./missing')` the regex after the condition was read as division, so its quote opened a fake string that swallowed the `require`. A fifth RED added 12 cases, 9 failing: the four findings, the two regex conditions, and the `imports` shape. The three controls already passed: an exact `import()`, an extensionless `require`, and division after `a.if(x)` or a call inside a condition. GREEN records which specifiers arrive through `import` and resolves those by exact path. It reverses the earlier backslash normalization: a relative specifier containing a backslash now fails as `non-portable-separator`. It also tracks whether each closing paren ends an `if`, `while`, `for` or `with` condition. Rescanning all 802 repository JavaScript files against the previous scanner changed no result; 18 of them declare `import` specifiers. The real registry still reports 35/35 resolved and zero escapes, warnings or package imports, unchanged on every receipt count. -Focused verification: 22 carrier cases, 12 compiler cases, 45 closure cases, 3 CI cases and 2 publish-surface cases pass with zero failures or skips. ESLint passes on the validator and the new test. The validator is CI-only and stays out of the packed surface, matching `scripts/ci/validate-context-profiles.js`. +Third review follow-up. Review of that head found four more problems, each reproduced first. Two were older: an extensionless `require` still offered `.cjs` and `.mjs` candidates, which Node's CommonJS search never tries; and a regex directly after a block (`if (ok) {} /"/`) was read as division, so its quote swallowed a later `require`. Two had been introduced by the previous fix. The condition check looked only at the character before `if`, so `obj.` + newline + `if(x) / 2` read as a condition and its division as a regex. Exact import resolution also turned a tagged template named `from` into a failing import. A sixth RED added 9 cases, 7 failing. Its two controls passed: `require` resolving to `.json`, and a template passed to `import()` still counting as a dependency. GREEN limits CommonJS candidates to the path, `.js`, `.json` and a directory's `index.js`/`index.json`. It treats `}` as a statement boundary before a regex, and decides a condition from the token before the keyword, ignoring whitespace. Static `from` and side-effect `import` now accept only quoted strings, never template slots. Rescanning all 802 repository JavaScript files against the previous head changed no result. The real registry is unchanged on every receipt count: 35/35 resolved, zero failures or warnings. + +Focused verification: 22 carrier cases, 12 compiler cases, 54 closure cases, 3 CI cases and 2 publish-surface cases pass with zero failures or skips. ESLint passes on the validator and the new test. The validator is CI-only and stays out of the packed surface, matching `scripts/ci/validate-context-profiles.js`. From 64ac61e6a372d356b54b1fad7a056aa4dbae7508 Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Fri, 9 Oct 2026 17:50:38 -0400 Subject: [PATCH 36/39] test(carriers): pin member calls and deeply nested computed arguments Two first-round review findings were never addressed, and both reproduce on the current head: - records.require('./label'), obj?.import('./x') and obj . require('./y') are method calls, but the call pattern's word boundary is satisfied by the dot, so a self-contained object method fails every projection for a file the script never loads. - The dynamic-argument pattern allows one level of nested parentheses, so require(path.join(__dirname, getName())) produces no dynamic warning while require(getName()) does. Control: a genuine require beside the member calls is still a dependency. --- tests/scripts/validate-carrier-closure.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/scripts/validate-carrier-closure.test.js b/tests/scripts/validate-carrier-closure.test.js index 5c3c25a04..568bd342e 100644 --- a/tests/scripts/validate-carrier-closure.test.js +++ b/tests/scripts/validate-carrier-closure.test.js @@ -137,6 +137,13 @@ test('an extensionless require resolves to a .json file', () => withFixture(root assert.equal(result.status, 'success', JSON.stringify(result.failures)); })); +test('an object method named require is not a module load', () => withFixture(root => { + plantSkillScript(root, "const records = { require: name => name };\nmodule.exports = records.require('./label');\n"); + const result = validator().validate(root); + assert.equal(result.status, 'success', JSON.stringify(result.failures)); + assert.deepEqual(result.warnings, []); +})); + test('a tagged template named from is not an import', () => withFixture(root => { plantSkillScript(root, 'const from = parts => parts[0];\nmodule.exports = from`./helper`;\n'); const result = validator().validate(root); @@ -221,8 +228,16 @@ const SCANNER_CASES = [ "const n = obj?. while(x) / 2, q = \"/\"; require('./after-optional');", ['./after-optional']], ['a tagged template named from', 'const from = p => p[0]; const s = from`./helper`;', []], ['a template import() is still a dependency', 'import(`./templated`);', ['./templated']], + ['member calls named require or import', + "r.require('./label'); obj?.import('./x'); obj . require('./y'); obj.\nimport('./z'); require('./real');", ['./real']], ]; +test('scanner: computed arguments nested more than one level deep still warn', () => { + const { dynamic } = validator().extractReferences( + "require(path.join(__dirname, getName())); require(a(b(c()))); obj.require(hidden());"); + assert.deepEqual(dynamic.sort(), ['a(b(c()))', 'path.join(__dirname, getName())']); +}); + for (const [label, source, expected] of SCANNER_CASES) { test(`scanner: ${label}`, () => { assert.deepEqual(validator().extractReferences(source).specifiers.sort(), [...expected].sort()); From 5ce12fb9f93e442afeaed57c2febc03ca8e2c654 Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Fri, 9 Oct 2026 17:52:55 -0400 Subject: [PATCH 37/39] fix(carriers): ignore member calls named require or import, and warn on deeply nested arguments - Call patterns now skip a require/import preceded by a dot, with or without whitespace or optional chaining, so records.require('./label') and obj?.import('./x') are method calls rather than module loads. - Replace the one-level dynamic-argument pattern with a balanced-paren scan over the string-free skeleton, so require(path.join(__dirname, getName())) and require(a(b(c()))) are reported as dynamic. Rescanning all 802 repository JavaScript files against the previous head changes 6 results, each a newly reported dynamic argument nested deeper than one level (docker eval checks and one test); no specifier or package result changes. The real registry is unchanged: 35/35 resolved, zero failures or warnings. --- scripts/ci/validate-carrier-closure.js | 36 +++++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/scripts/ci/validate-carrier-closure.js b/scripts/ci/validate-carrier-closure.js index cea037909..b6e4682db 100644 --- a/scripts/ci/validate-carrier-closure.js +++ b/scripts/ci/validate-carrier-closure.js @@ -26,12 +26,14 @@ const BUILTINS = new Set(builtinModules); const SCRIPT_EXTENSIONS = new Set(['.js', '.cjs', '.mjs']); const MARK = '\u0000'; const SLOT = `${MARK}(\\d+)${MARK}`; -const CALL_PATTERN = new RegExp(`\\b(require|import)\\s*\\(\\s*${SLOT}\\s*\\)`, 'g'); +// `obj.require(`, `obj?.import(` and `obj . require(` are method calls, not module loads. +const NOT_MEMBER = '(? { + const start = match.index + match[0].length; + let depth = 1; + let cursor = start; + while (cursor < code.length && depth > 0) { + if (code[cursor] === '(') depth += 1; + else if (code[cursor] === ')') depth -= 1; + cursor += 1; + } + if (depth === 0) found.push(code.slice(start, cursor - 1).trim()); + }); + return found; +} + function renderExpression(text, literals) { return text.replace(SLOT_PATTERN, (_whole, slotIndex) => `'${literals[Number(slotIndex)]}'`).split(CLOSE).join(' ').trim(); } @@ -220,11 +243,10 @@ function extractReferences(rawSource) { collect(SLOT_PATTERN, match[1], slotMatch => segments.push(literals[Number(slotMatch[1])])); specifiers.add(`./${segments.join('/')}`); }); - collect(DYNAMIC_PATTERN, code, match => { - const argument = match[1].trim(); - if (new RegExp(`^${SLOT}$`).test(argument) || STATIC_JOIN_PATTERN.test(argument)) return; + for (const argument of callArguments(code)) { + if (!argument || new RegExp(`^${SLOT}$`).test(argument) || STATIC_JOIN_PATTERN.test(argument)) continue; dynamic.add(renderExpression(argument, literals)); - }); + } return { specifiers: [...specifiers], imports: [...imports], packages: [...packages], dynamic: [...dynamic] }; } From 0f8a302627bcfd21db04c51d8b8279cf738719bd Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Fri, 9 Oct 2026 17:53:06 -0400 Subject: [PATCH 38/39] docs(carriers): record the member-call and nested-argument fixes --- docs/design/context-carriers.tdd.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/design/context-carriers.tdd.md b/docs/design/context-carriers.tdd.md index 405cb1905..76ee4c878 100644 --- a/docs/design/context-carriers.tdd.md +++ b/docs/design/context-carriers.tdd.md @@ -99,4 +99,6 @@ Second review follow-up. Review of that head found four ways the gate still pass Third review follow-up. Review of that head found four more problems, each reproduced first. Two were older: an extensionless `require` still offered `.cjs` and `.mjs` candidates, which Node's CommonJS search never tries; and a regex directly after a block (`if (ok) {} /"/`) was read as division, so its quote swallowed a later `require`. Two had been introduced by the previous fix. The condition check looked only at the character before `if`, so `obj.` + newline + `if(x) / 2` read as a condition and its division as a regex. Exact import resolution also turned a tagged template named `from` into a failing import. A sixth RED added 9 cases, 7 failing. Its two controls passed: `require` resolving to `.json`, and a template passed to `import()` still counting as a dependency. GREEN limits CommonJS candidates to the path, `.js`, `.json` and a directory's `index.js`/`index.json`. It treats `}` as a statement boundary before a regex, and decides a condition from the token before the keyword, ignoring whitespace. Static `from` and side-effect `import` now accept only quoted strings, never template slots. Rescanning all 802 repository JavaScript files against the previous head changed no result. The real registry is unchanged on every receipt count: 35/35 resolved, zero failures or warnings. -Focused verification: 22 carrier cases, 12 compiler cases, 54 closure cases, 3 CI cases and 2 publish-surface cases pass with zero failures or skips. ESLint passes on the validator and the new test. The validator is CI-only and stays out of the packed surface, matching `scripts/ci/validate-context-profiles.js`. +Two first-round findings had stayed open and still reproduced. A member call such as `records.require('./label')` matched the call pattern, because the dot satisfies a word boundary, so a self-contained object method failed every projection. And the dynamic-argument pattern allowed only one level of nested parentheses, so `require(path.join(__dirname, getName()))` produced no warning while `require(getName())` did. A seventh RED added 3 cases, all failing. GREEN skips calls preceded by a dot, including after whitespace or optional chaining, and finds each call's argument by balanced-paren scanning over the string-free skeleton. Rescanning the 802 files changed 6 results, each a newly reported nested dynamic argument in a docker eval check or a test, none of them in a carrier. The real registry is unchanged. + +Focused verification: 22 carrier cases, 12 compiler cases, 57 closure cases, 3 CI cases and 2 publish-surface cases pass with zero failures or skips. ESLint passes on the validator and the new test. The validator is CI-only and stays out of the packed surface, matching `scripts/ci/validate-context-profiles.js`. From 56a8235ea612a828e6e41f326d316ddbec0b5be8 Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:43:08 -0400 Subject: [PATCH 39/39] fix(catalog): preserve locale navigation and export new skill metadata --- agent.yaml | 1 + docs/ar/README.md | 2 +- docs/bn/README.md | 2 +- docs/tr/AGENTS.md | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/agent.yaml b/agent.yaml index cb504d707..e3ec4592b 100644 --- a/agent.yaml +++ b/agent.yaml @@ -13,6 +13,7 @@ skills: - agent-eval - agent-harness-construction - agent-payment-x402 + - agent-runtime-gateway-smoke-test - agentic-engineering - agentic-os - ai-first-engineering diff --git a/docs/ar/README.md b/docs/ar/README.md index 7f991d9ab..234045478 100644 --- a/docs/ar/README.md +++ b/docs/ar/README.md @@ -1,4 +1,4 @@ -**اللغة:** [English](../../README.md) | **العربية** | [Deutsch](../de-DE/README.md) | [Español](../es/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) +**اللغة:** [English](../../README.md) | **العربية** | [বাংলা](../bn/README.md) | [Deutsch](../de-DE/README.md) | [Español](../es/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md) # ECC diff --git a/docs/bn/README.md b/docs/bn/README.md index 93418c0b3..ea1e8c0b9 100644 --- a/docs/bn/README.md +++ b/docs/bn/README.md @@ -1,4 +1,4 @@ -**ভাষা:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Español](../es/README.md) | [Українська](../uk-UA/README.md) | [اردو](../ur/README.md) | [Polski](../pl/README.md) | বাংলা +**ভাষা:** [English](../../README.md) | [العربية](../ar/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/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) | [Español](../es/README.md) | [Українська](../uk-UA/README.md) | [اردو](../ur/README.md) | [Polski](../pl/README.md) | বাংলা # ECC diff --git a/docs/tr/AGENTS.md b/docs/tr/AGENTS.md index 4ae6972ab..c68352e90 100644 --- a/docs/tr/AGENTS.md +++ b/docs/tr/AGENTS.md @@ -1,6 +1,6 @@ # ECC — Agent Talimatları -Bu, yazılım geliştirme için 71 özel agent, 302 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 71 özel agent, 304 skill, 95 command ve otomatik hook iş akışları sağlayan **üretime hazır bir AI kodlama eklentisidir**. **Sürüm:** 2.2.3 @@ -142,7 +142,7 @@ Başarısızlık sorunlarını giderin: test izolasyonunu kontrol edin → mockl ``` agents/ — 71 özel subagent -skills/ — 302 iş akışı skillleri ve alan bilgisi +skills/ — 304 iş akışı skillleri ve alan bilgisi commands/ — 95 slash command hooks/ — Tetikleyici tabanlı otomasyonlar rules/ — Her zaman uyulması gereken kurallar (ortak + dile özel)