From e1bc08fa6ee44365ce757011c3ea497113c21d69 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 31 Mar 2026 22:57:48 -0700 Subject: [PATCH] fix: harden install planning and sync tracked catalogs --- README.md | 6 +- README.zh-CN.md | 2 +- WORKING-CONTEXT.md | 5 +- agents/gan-generator.md | 26 +- commands/code-review.md | 8 +- commands/prp-implement.md | 32 +- docs/zh-CN/AGENTS.md | 8 +- docs/zh-CN/README.md | 14 +- docs/zh-CN/skills/browser-qa/SKILL.md | 81 ++++ package.json | 4 +- scripts/ci/catalog.js | 419 +++++++++++++++++- scripts/gan-harness.sh | 14 +- scripts/lib/install-manifests.js | 73 ++- .../install-targets/antigravity-project.js | 22 +- scripts/lib/install-targets/helpers.js | 7 + skills/gan-style-harness/SKILL.md | 4 +- tests/ci/validators.test.js | 193 +++++++- tests/lib/changed-files-store.test.js | 2 +- tests/lib/install-manifests.test.js | 168 +++++-- 19 files changed, 970 insertions(+), 118 deletions(-) create mode 100644 docs/zh-CN/skills/browser-qa/SKILL.md diff --git a/README.md b/README.md index 1b2a057f..3f3f548c 100644 --- a/README.md +++ b/README.md @@ -1221,9 +1221,9 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e | Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode | |---------|------------|------------|-----------|----------| -| **Agents** | 21 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | -| **Commands** | 52 | Shared | Instruction-based | 31 | -| **Skills** | 102 | Shared | 10 (native format) | 37 | +| **Agents** | 36 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | +| **Commands** | 68 | Shared | Instruction-based | 31 | +| **Skills** | 142 | Shared | 10 (native format) | 37 | | **Hook Events** | 8 types | 15 types | None yet | 11 types | | **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | | **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions | diff --git a/README.zh-CN.md b/README.zh-CN.md index 4ccf1cda..6d7047b7 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -106,7 +106,7 @@ cp -r everything-claude-code/rules/perl ~/.claude/rules/ /plugin list everything-claude-code@everything-claude-code ``` -**完成!** 你现在可以使用 13 个代理、43 个技能和 31 个命令。 +**完成!** 你现在可以使用 36 个代理、142 个技能和 68 个命令。 ### multi-* 命令需要额外配置 diff --git a/WORKING-CONTEXT.md b/WORKING-CONTEXT.md index f529337b..4d7fef00 100644 --- a/WORKING-CONTEXT.md +++ b/WORKING-CONTEXT.md @@ -48,7 +48,6 @@ Public ECC plugin repo for agents, skills, commands, hooks, rules, install surfa - Native-support candidates to fully diff-audit next: - `#1055` Dart / Flutter support - `#1043` C# reviewer and .NET skills - - `#834` localized catalog sync and antigravity target filtering - Port or rebuild inside ECC after full audit: - `#894` Jira integration - `#844` ui-demo skill @@ -75,3 +74,7 @@ Keep this file detailed for only the current sprint, blockers, and next actions. - 2026-04-01: Notification PRs `#808` and `#814` were identified as overlapping and should be rebuilt as one unified feature instead of landing as parallel branches. - 2026-04-01: External-source skill PRs `#640`, `#851`, and `#852` were closed under the new ingestion policy; copy ideas from audited source later rather than merging branded/source-import PRs directly. - 2026-04-01: The remaining low GitHub advisory on `ecc2/Cargo.lock` was addressed by moving `ratatui` to `0.30` with `crossterm_0_28`, which updated transitive `lru` from `0.12.5` to `0.16.3`. `cargo build --manifest-path ecc2/Cargo.toml` still passes. +- 2026-04-01: Safe core of `#834` was ported directly into `main` instead of merging the PR wholesale. This included stricter install-plan validation, antigravity target filtering that skips unsupported module trees, tracked catalog sync for English plus zh-CN docs, and a dedicated `catalog:sync` write mode. +- 2026-04-01: Repo catalog truth is now synced at `36` agents, `68` commands, and `142` skills across the tracked English and zh-CN docs. +- 2026-04-01: Legacy emoji and non-essential symbol usage in docs, scripts, and tests was normalized to keep the unicode-safety lane green without weakening the check itself. +- 2026-04-01: The remaining self-contained piece of `#834`, `docs/zh-CN/skills/browser-qa/SKILL.md`, was ported directly into the repo. After commit, `#834` should be closed as superseded-by-direct-port. diff --git a/agents/gan-generator.md b/agents/gan-generator.md index b8bd5045..8622edc4 100644 --- a/agents/gan-generator.md +++ b/agents/gan-generator.md @@ -98,21 +98,21 @@ Write to `gan-harness/generator-state.md` after each iteration: The Evaluator will specifically penalize these patterns. **Avoid them:** -- ❌ Generic gradient backgrounds (#667eea → #764ba2 is an instant tell) -- ❌ Excessive rounded corners on everything -- ❌ Stock hero sections with "Welcome to [App Name]" -- ❌ Default Material UI / Shadcn themes without customization -- ❌ Placeholder images from unsplash/placeholder services -- ❌ Generic card grids with identical layouts -- ❌ "AI-generated" decorative SVG patterns +- Avoid generic gradient backgrounds (#667eea -> #764ba2 is an instant tell) +- Avoid excessive rounded corners on everything +- Avoid stock hero sections with "Welcome to [App Name]" +- Avoid default Material UI / Shadcn themes without customization +- Avoid placeholder images from unsplash/placeholder services +- Avoid generic card grids with identical layouts +- Avoid "AI-generated" decorative SVG patterns **Instead, aim for:** -- ✅ A specific, opinionated color palette (follow the spec) -- ✅ Thoughtful typography hierarchy (different weights, sizes for different content) -- ✅ Custom layouts that match the content (not generic grids) -- ✅ Meaningful animations tied to user actions (not decoration) -- ✅ Real empty states with personality -- ✅ Error states that help the user (not just "Something went wrong") +- Use a specific, opinionated color palette (follow the spec) +- Use thoughtful typography hierarchy (different weights, sizes for different content) +- Use custom layouts that match the content (not generic grids) +- Use meaningful animations tied to user actions (not decoration) +- Use real empty states with personality +- Use error states that help the user (not just "Something went wrong") ## Interaction with Evaluator diff --git a/commands/code-review.md b/commands/code-review.md index e68d8e7e..8189f951 100644 --- a/commands/code-review.md +++ b/commands/code-review.md @@ -219,10 +219,10 @@ Create review artifact at `.claude/PRPs/reviews/pr--review.md`: | Check | Result | |---|---| -| Type check | ✅ Pass / ❌ Fail / ⏭️ Skipped | -| Lint | ✅ / ❌ / ⏭️ | -| Tests | ✅ / ❌ / ⏭️ | -| Build | ✅ / ❌ / ⏭️ | +| Type check | Pass / Fail / Skipped | +| Lint | Pass / Fail / Skipped | +| Tests | Pass / Fail / Skipped | +| Build | Pass / Fail / Skipped | ## Files Reviewed diff --git a/commands/prp-implement.md b/commands/prp-implement.md index 67777c66..9a729cf2 100644 --- a/commands/prp-implement.md +++ b/commands/prp-implement.md @@ -115,7 +115,7 @@ For each task in **Step-by-Step Tasks**: ``` If type-check fails → fix the error before moving to the next file. -4. **Track progress** — Log: `✅ Task N: [task name] — complete` +4. **Track progress** — Log: `[done] Task N: [task name] — complete` ### Handling Deviations @@ -234,18 +234,18 @@ Write report to `.claude/PRPs/reports/{plan-name}-report.md`: | # | Task | Status | Notes | |---|---|---|---| -| 1 | [task name] | ✅ Complete | | -| 2 | [task name] | ✅ Complete | Deviated — [reason] | +| 1 | [task name] | [done] Complete | | +| 2 | [task name] | [done] Complete | Deviated — [reason] | ## Validation Results | Level | Status | Notes | |---|---|---| -| Static Analysis | ✅ Pass | | -| Unit Tests | ✅ Pass | N tests written | -| Build | ✅ Pass | | -| Integration | ✅ Pass | or N/A | -| Edge Cases | ✅ Pass | | +| Static Analysis | [done] Pass | | +| Unit Tests | [done] Pass | N tests written | +| Build | [done] Pass | | +| Integration | [done] Pass | or N/A | +| Edge Cases | [done] Pass | | ## Files Changed @@ -297,17 +297,17 @@ Report to user: - **Plan**: [plan file path] → archived to completed/ - **Branch**: [current branch name] -- **Status**: ✅ All tasks complete +- **Status**: [done] All tasks complete ### Validation Summary | Check | Status | |---|---| -| Type Check | ✅ | -| Lint | ✅ | -| Tests | ✅ (N written) | -| Build | ✅ | -| Integration | ✅ or N/A | +| Type Check | [done] | +| Lint | [done] | +| Tests | [done] (N written) | +| Build | [done] | +| Integration | [done] or N/A | ### Files Changed - [N] files created, [M] files updated @@ -322,8 +322,8 @@ Report to user: ### PRD Progress (if applicable) | Phase | Status | |---|---| -| Phase 1 | ✅ Complete | -| Phase 2 | ⏳ Next | +| Phase 1 | [done] Complete | +| Phase 2 | [next] | | ... | ... | > Next step: Run `/prp-pr` to create a pull request, or `/code-review` to review changes first. diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md index db4edacf..cdc2c3a1 100644 --- a/docs/zh-CN/AGENTS.md +++ b/docs/zh-CN/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — 智能体指令 -这是一个**生产就绪的 AI 编码插件**,提供 28 个专业代理、116 项技能、59 条命令以及自动化钩子工作流,用于软件开发。 +这是一个**生产就绪的 AI 编码插件**,提供 36 个专业代理、142 项技能、68 条命令以及自动化钩子工作流,用于软件开发。 **版本:** 1.9.0 @@ -146,9 +146,9 @@ ## 项目结构 ``` -agents/ — 28 个专业子代理 -skills/ — 115 个工作流技能和领域知识 -commands/ — 59 个斜杠命令 +agents/ — 36 个专业子代理 +skills/ — 142 个工作流技能和领域知识 +commands/ — 68 个斜杠命令 hooks/ — 基于触发的自动化 rules/ — 始终遵循的指导方针(通用 + 每种语言) scripts/ — 跨平台 Node.js 实用工具 diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index e363001e..019352cc 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -209,7 +209,7 @@ npx ecc-install typescript /plugin list everything-claude-code@everything-claude-code ``` -**搞定!** 你现在可以使用 28 个智能体、116 项技能和 59 个命令了。 +**搞定!** 你现在可以使用 36 个智能体、142 项技能和 68 个命令了。 *** @@ -1094,9 +1094,9 @@ opencode | 功能特性 | Claude Code | OpenCode | 状态 | |---------|-------------|----------|--------| -| 智能体 | PASS: 28 个 | PASS: 12 个 | **Claude Code 领先** | -| 命令 | PASS: 59 个 | PASS: 31 个 | **Claude Code 领先** | -| 技能 | PASS: 116 项 | PASS: 37 项 | **Claude Code 领先** | +| 智能体 | PASS: 36 个 | PASS: 12 个 | **Claude Code 领先** | +| 命令 | PASS: 68 个 | PASS: 31 个 | **Claude Code 领先** | +| 技能 | PASS: 142 项 | PASS: 37 项 | **Claude Code 领先** | | 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** | | 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** | | MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** | @@ -1206,9 +1206,9 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以 | 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode | |---------|------------|------------|-----------|----------| -| **智能体** | 21 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | -| **命令** | 52 | 共享 | 基于指令 | 31 | -| **技能** | 102 | 共享 | 10 (原生格式) | 37 | +| **智能体** | 36 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | +| **命令** | 68 | 共享 | 基于指令 | 31 | +| **技能** | 142 | 共享 | 10 (原生格式) | 37 | | **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 | | **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 | | **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 | diff --git a/docs/zh-CN/skills/browser-qa/SKILL.md b/docs/zh-CN/skills/browser-qa/SKILL.md new file mode 100644 index 00000000..52f0958b --- /dev/null +++ b/docs/zh-CN/skills/browser-qa/SKILL.md @@ -0,0 +1,81 @@ +# Browser QA — 自动化视觉测试与交互验证 + +## When to use + +- 功能部署到 staging / preview 之后 +- 需要验证跨页面的 UI 行为时 +- 发布前确认布局、表单和交互是否真的可用 +- 审查涉及前端改动的 PR 时 +- 做可访问性审计和响应式测试时 + +## How it works + +使用浏览器自动化 MCP(claude-in-chrome、Playwright 或 Puppeteer),像真实用户一样与线上页面交互。 + +### 阶段 1:冒烟测试 +``` +1. 打开目标 URL +2. 检查控制台错误(过滤噪声:分析脚本、第三方库) +3. 验证网络请求中没有 4xx / 5xx +4. 在桌面和移动端视口截图首屏内容 +5. 检查 Core Web Vitals:LCP < 2.5s,CLS < 0.1,INP < 200ms +``` + +### 阶段 2:交互测试 +``` +1. 点击所有导航链接,验证没有死链 +2. 使用有效数据提交表单,验证成功态 +3. 使用无效数据提交表单,验证错误态 +4. 测试认证流程:登录 → 受保护页面 → 登出 +5. 测试关键用户路径(结账、引导、搜索) +``` + +### 阶段 3:视觉回归 +``` +1. 在 3 个断点(375px、768px、1440px)对关键页面截图 +2. 与基线截图对比(如果已保存) +3. 标记 > 5px 的布局偏移、缺失元素、内容溢出 +4. 如适用,检查暗色模式 +``` + +### 阶段 4:可访问性 +``` +1. 在每个页面运行 axe-core 或等价工具 +2. 标记 WCAG AA 违规(对比度、标签、焦点顺序) +3. 验证键盘导航可以端到端工作 +4. 检查屏幕阅读器地标 +``` + +## Examples + +```markdown +## QA 报告 — [URL] — [timestamp] + +### 冒烟测试 +- 控制台错误:0 个严重错误,2 个警告(分析脚本噪声) +- 网络:全部 200/304,无失败请求 +- Core Web Vitals:LCP 1.2s,CLS 0.02,INP 89ms + +### 交互 +- [done] 导航链接:12/12 正常 +- [issue] 联系表单:无效邮箱缺少错误态 +- [done] 认证流程:登录 / 登出正常 + +### 视觉 +- [issue] Hero 区域在 375px 视口下溢出 +- [done] 暗色模式:所有页面一致 + +### 可访问性 +- 2 个 AA 级违规:Hero 图片缺少 alt 文本,页脚链接对比度过低 + +### 结论:修复后可发布(2 个问题,0 个阻塞项) +``` + +## 集成 + +可与任意浏览器 MCP 配合: +- `mChild__claude-in-chrome__*` 工具(推荐,直接使用你的真实 Chrome) +- 通过 `mcp__browserbase__*` 使用 Playwright +- 直接运行 Puppeteer 脚本 + +可与 `/canary-watch` 搭配用于发布后的持续监控。 diff --git a/package.json b/package.json index b6a81ab2..9150536b 100644 --- a/package.json +++ b/package.json @@ -102,13 +102,15 @@ }, "scripts": { "postinstall": "echo '\\n ecc-universal installed!\\n Run: npx ecc typescript\\n Compat: npx ecc-install typescript\\n Docs: https://github.com/affaan-m/everything-claude-code\\n'", + "catalog:check": "node scripts/ci/catalog.js --text", + "catalog:sync": "node scripts/ci/catalog.js --write --text", "lint": "eslint . && markdownlint '**/*.md' --ignore node_modules", "harness:audit": "node scripts/harness-audit.js", "claw": "node scripts/claw.js", "orchestrate:status": "node scripts/orchestration-status.js", "orchestrate:worker": "bash scripts/orchestrate-codex-worker.sh", "orchestrate:tmux": "node scripts/orchestrate-worktrees.js", - "test": "node scripts/ci/check-unicode-safety.js && node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-no-personal-paths.js && node scripts/ci/catalog.js --text && 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/validate-install-manifests.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && node tests/run-all.js", "coverage": "c8 --all --include=\"scripts/**/*.js\" --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js" }, "dependencies": { diff --git a/scripts/ci/catalog.js b/scripts/ci/catalog.js index 8ef3dc92..46d62976 100644 --- a/scripts/ci/catalog.js +++ b/scripts/ci/catalog.js @@ -1,12 +1,13 @@ #!/usr/bin/env node /** - * Verify repo catalog counts against README.md and AGENTS.md. + * Verify repo catalog counts against tracked documentation files. * * Usage: * node scripts/ci/catalog.js * node scripts/ci/catalog.js --json * node scripts/ci/catalog.js --md * node scripts/ci/catalog.js --text + * node scripts/ci/catalog.js --write --text */ 'use strict'; @@ -17,6 +18,10 @@ const path = require('path'); const ROOT = path.join(__dirname, '../..'); const README_PATH = path.join(ROOT, 'README.md'); const AGENTS_PATH = path.join(ROOT, 'AGENTS.md'); +const README_ZH_CN_PATH = path.join(ROOT, 'README.zh-CN.md'); +const DOCS_ZH_CN_README_PATH = path.join(ROOT, 'docs', 'zh-CN', 'README.md'); +const DOCS_ZH_CN_AGENTS_PATH = path.join(ROOT, 'docs', 'zh-CN', 'AGENTS.md'); +const WRITE_MODE = process.argv.includes('--write'); const OUTPUT_MODE = process.argv.includes('--md') ? 'md' @@ -43,8 +48,9 @@ function listMatchingFiles(relativeDir, matcher) { function buildCatalog() { const agents = listMatchingFiles('agents', entry => entry.isFile() && entry.name.endsWith('.md')); const commands = listMatchingFiles('commands', entry => entry.isFile() && entry.name.endsWith('.md')); - const skills = listMatchingFiles('skills', entry => entry.isDirectory() && fs.existsSync(path.join(ROOT, 'skills', entry.name, 'SKILL.md'))) - .map(skillDir => `${skillDir}/SKILL.md`); + const skills = listMatchingFiles('skills', entry => ( + entry.isDirectory() && fs.existsSync(path.join(ROOT, 'skills', entry.name, 'SKILL.md')) + )).map(skillDir => `${skillDir}/SKILL.md`); return { agents: { count: agents.length, files: agents, glob: 'agents/*.md' }, @@ -61,6 +67,22 @@ function readFileOrThrow(filePath) { } } +function writeFileOrThrow(filePath, content) { + try { + fs.writeFileSync(filePath, content, 'utf8'); + } catch (error) { + throw new Error(`Failed to write ${path.basename(filePath)}: ${error.message}`); + } +} + +function replaceOrThrow(content, regex, replacer, source) { + if (!regex.test(content)) { + throw new Error(`${source} is missing the expected catalog marker`); + } + + return content.replace(regex, replacer); +} + function parseReadmeExpectations(readmeContent) { const expectations = []; @@ -95,6 +117,120 @@ function parseReadmeExpectations(readmeContent) { }); } + const parityPatterns = [ + { + category: 'agents', + regex: /^\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*Shared\s*\(AGENTS\.md\)\s*\|\s*Shared\s*\(AGENTS\.md\)\s*\|\s*12\s*\|$/im, + source: 'README.md parity table' + }, + { + category: 'commands', + regex: /^\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*Shared\s*\|\s*Instruction-based\s*\|\s*31\s*\|$/im, + source: 'README.md parity table' + }, + { + category: 'skills', + regex: /^\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*Shared\s*\|\s*10\s*\(native format\)\s*\|\s*37\s*\|$/im, + source: 'README.md parity table' + } + ]; + + for (const pattern of parityPatterns) { + const match = readmeContent.match(pattern.regex); + if (!match) { + throw new Error(`${pattern.source} is missing the ${pattern.category} row`); + } + + expectations.push({ + category: pattern.category, + mode: 'exact', + expected: Number(match[1]), + source: `${pattern.source} (${pattern.category})` + }); + } + + return expectations; +} + +function parseZhRootReadmeExpectations(readmeContent) { + const match = readmeContent.match(/你现在可以使用\s+(\d+)\s+个代理、\s*(\d+)\s*个技能和\s*(\d+)\s*个命令/i); + if (!match) { + throw new Error('README.zh-CN.md is missing the quick-start catalog summary'); + } + + return [ + { category: 'agents', mode: 'exact', expected: Number(match[1]), source: 'README.zh-CN.md quick-start summary' }, + { category: 'skills', mode: 'exact', expected: Number(match[2]), source: 'README.zh-CN.md quick-start summary' }, + { category: 'commands', mode: 'exact', expected: Number(match[3]), source: 'README.zh-CN.md quick-start summary' } + ]; +} + +function parseZhDocsReadmeExpectations(readmeContent) { + const expectations = []; + + const quickStartMatch = readmeContent.match(/你现在可以使用\s+(\d+)\s+个智能体、\s*(\d+)\s*项技能和\s*(\d+)\s*个命令了/i); + if (!quickStartMatch) { + throw new Error('docs/zh-CN/README.md is missing the quick-start catalog summary'); + } + + expectations.push( + { category: 'agents', mode: 'exact', expected: Number(quickStartMatch[1]), source: 'docs/zh-CN/README.md quick-start summary' }, + { category: 'skills', mode: 'exact', expected: Number(quickStartMatch[2]), source: 'docs/zh-CN/README.md quick-start summary' }, + { category: 'commands', mode: 'exact', expected: Number(quickStartMatch[3]), source: 'docs/zh-CN/README.md quick-start summary' } + ); + + const tablePatterns = [ + { category: 'agents', regex: /\|\s*智能体\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s*个\s*\|/i, source: 'docs/zh-CN/README.md comparison table' }, + { category: 'commands', regex: /\|\s*命令\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s*个\s*\|/i, source: 'docs/zh-CN/README.md comparison table' }, + { category: 'skills', regex: /\|\s*技能\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s*项\s*\|/i, source: 'docs/zh-CN/README.md comparison table' } + ]; + + for (const pattern of tablePatterns) { + const match = readmeContent.match(pattern.regex); + if (!match) { + throw new Error(`${pattern.source} is missing the ${pattern.category} row`); + } + + expectations.push({ + category: pattern.category, + mode: 'exact', + expected: Number(match[1]), + source: `${pattern.source} (${pattern.category})` + }); + } + + const parityPatterns = [ + { + category: 'agents', + regex: /^\|\s*(?:\*\*)?智能体(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*共享\s*\(AGENTS\.md\)\s*\|\s*共享\s*\(AGENTS\.md\)\s*\|\s*12\s*\|$/im, + source: 'docs/zh-CN/README.md parity table' + }, + { + category: 'commands', + regex: /^\|\s*(?:\*\*)?命令(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*共享\s*\|\s*基于指令\s*\|\s*31\s*\|$/im, + source: 'docs/zh-CN/README.md parity table' + }, + { + category: 'skills', + regex: /^\|\s*(?:\*\*)?技能(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*共享\s*\|\s*10\s*\(原生格式\)\s*\|\s*37\s*\|$/im, + source: 'docs/zh-CN/README.md parity table' + } + ]; + + for (const pattern of parityPatterns) { + const match = readmeContent.match(pattern.regex); + if (!match) { + throw new Error(`${pattern.source} is missing the ${pattern.category} row`); + } + + expectations.push({ + category: pattern.category, + mode: 'exact', + expected: Number(match[1]), + source: `${pattern.source} (${pattern.category})` + }); + } + return expectations; } @@ -153,6 +289,61 @@ function parseAgentsDocExpectations(agentsContent) { return expectations; } +function parseZhAgentsDocExpectations(agentsContent) { + const summaryMatch = agentsContent.match(/提供\s+(\d+)\s+个专业代理、\s*(\d+)(\+)?\s*项技能、\s*(\d+)\s+条命令/i); + if (!summaryMatch) { + throw new Error('docs/zh-CN/AGENTS.md is missing the catalog summary line'); + } + + const expectations = [ + { category: 'agents', mode: 'exact', expected: Number(summaryMatch[1]), source: 'docs/zh-CN/AGENTS.md summary' }, + { + category: 'skills', + mode: summaryMatch[3] ? 'minimum' : 'exact', + expected: Number(summaryMatch[2]), + source: 'docs/zh-CN/AGENTS.md summary' + }, + { category: 'commands', mode: 'exact', expected: Number(summaryMatch[4]), source: 'docs/zh-CN/AGENTS.md summary' } + ]; + + const structurePatterns = [ + { + category: 'agents', + mode: 'exact', + regex: /^\s*agents\/\s*[—–-]\s*(\d+)\s+个专业子代理\s*$/im, + source: 'docs/zh-CN/AGENTS.md project structure' + }, + { + category: 'skills', + mode: 'minimum', + regex: /^\s*skills\/\s*[—–-]\s*(\d+)(\+)?\s+个工作流技能和领域知识\s*$/im, + source: 'docs/zh-CN/AGENTS.md project structure' + }, + { + category: 'commands', + mode: 'exact', + regex: /^\s*commands\/\s*[—–-]\s*(\d+)\s+个斜杠命令\s*$/im, + source: 'docs/zh-CN/AGENTS.md project structure' + } + ]; + + for (const pattern of structurePatterns) { + const match = agentsContent.match(pattern.regex); + if (!match) { + throw new Error(`${pattern.source} is missing the ${pattern.category} entry`); + } + + expectations.push({ + category: pattern.category, + mode: pattern.mode === 'minimum' && match[2] ? 'minimum' : pattern.mode, + expected: Number(match[1]), + source: `${pattern.source} (${pattern.category})` + }); + } + + return expectations; +} + function evaluateExpectations(catalog, expectations) { return expectations.map(expectation => { const actual = catalog[expectation.category].count; @@ -173,6 +364,208 @@ function formatExpectation(expectation) { return `${expectation.source}: ${expectation.category} documented ${comparator} ${expectation.expected}, actual ${expectation.actual}`; } +function syncEnglishReadme(content, catalog) { + let nextContent = content; + + nextContent = replaceOrThrow( + nextContent, + /(access to\s+)(\d+)(\s+agents,\s+)(\d+)(\s+skills,\s+and\s+)(\d+)(\s+commands)/i, + (_, prefix, __, agentsSuffix, ___, skillsSuffix, ____, commandsSuffix) => + `${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsSuffix}${catalog.commands.count}${commandsSuffix}`, + 'README.md quick-start summary' + ); + nextContent = replaceOrThrow( + nextContent, + /(\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s+agents\s*\|)/i, + (_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`, + 'README.md comparison table (agents)' + ); + nextContent = replaceOrThrow( + nextContent, + /(\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s+commands\s*\|)/i, + (_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`, + 'README.md comparison table (commands)' + ); + nextContent = replaceOrThrow( + nextContent, + /(\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s+skills\s*\|)/i, + (_, prefix, __, suffix) => `${prefix}${catalog.skills.count}${suffix}`, + 'README.md comparison table (skills)' + ); + nextContent = replaceOrThrow( + nextContent, + /^(\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*Shared\s*\(AGENTS\.md\)\s*\|\s*Shared\s*\(AGENTS\.md\)\s*\|\s*12\s*\|)$/im, + (_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`, + 'README.md parity table (agents)' + ); + nextContent = replaceOrThrow( + nextContent, + /^(\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*Shared\s*\|\s*Instruction-based\s*\|\s*31\s*\|)$/im, + (_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`, + 'README.md parity table (commands)' + ); + nextContent = replaceOrThrow( + nextContent, + /^(\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*Shared\s*\|\s*10\s*\(native format\)\s*\|\s*37\s*\|)$/im, + (_, prefix, __, suffix) => `${prefix}${catalog.skills.count}${suffix}`, + 'README.md parity table (skills)' + ); + + return nextContent; +} + +function syncEnglishAgents(content, catalog) { + let nextContent = content; + + nextContent = replaceOrThrow( + nextContent, + /(providing\s+)(\d+)(\s+specialized agents,\s+)(\d+)(\+?)(\s+skills,\s+)(\d+)(\s+commands)/i, + (_, prefix, __, agentsSuffix, ___, skillsPlus, skillsSuffix, ____, commandsSuffix) => + `${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsPlus}${skillsSuffix}${catalog.commands.count}${commandsSuffix}`, + 'AGENTS.md summary' + ); + nextContent = replaceOrThrow( + nextContent, + /^(\s*agents\/\s*[—–-]\s*)(\d+)(\s+specialized subagents\s*)$/im, + (_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`, + 'AGENTS.md project structure (agents)' + ); + nextContent = replaceOrThrow( + nextContent, + /^(\s*skills\/\s*[—–-]\s*)(\d+)(\+?)(\s+workflow skills and domain knowledge\s*)$/im, + (_, prefix, __, plus, suffix) => `${prefix}${catalog.skills.count}${plus}${suffix}`, + 'AGENTS.md project structure (skills)' + ); + nextContent = replaceOrThrow( + nextContent, + /^(\s*commands\/\s*[—–-]\s*)(\d+)(\s+slash commands\s*)$/im, + (_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`, + 'AGENTS.md project structure (commands)' + ); + + return nextContent; +} + +function syncZhRootReadme(content, catalog) { + return replaceOrThrow( + content, + /(你现在可以使用\s+)(\d+)(\s+个代理、\s*)(\d+)(\s*个技能和\s*)(\d+)(\s*个命令[。.!!]?)/i, + (_, prefix, __, agentsSuffix, ___, skillsSuffix, ____, commandsSuffix) => + `${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsSuffix}${catalog.commands.count}${commandsSuffix}`, + 'README.zh-CN.md quick-start summary' + ); +} + +function syncZhDocsReadme(content, catalog) { + let nextContent = content; + + nextContent = replaceOrThrow( + nextContent, + /(你现在可以使用\s+)(\d+)(\s+个智能体、\s*)(\d+)(\s*项技能和\s*)(\d+)(\s*个命令了[。.!!]?)/i, + (_, prefix, __, agentsSuffix, ___, skillsSuffix, ____, commandsSuffix) => + `${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsSuffix}${catalog.commands.count}${commandsSuffix}`, + 'docs/zh-CN/README.md quick-start summary' + ); + nextContent = replaceOrThrow( + nextContent, + /(\|\s*智能体\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s*个\s*\|)/i, + (_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`, + 'docs/zh-CN/README.md comparison table (agents)' + ); + nextContent = replaceOrThrow( + nextContent, + /(\|\s*命令\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s*个\s*\|)/i, + (_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`, + 'docs/zh-CN/README.md comparison table (commands)' + ); + nextContent = replaceOrThrow( + nextContent, + /(\|\s*技能\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s*项\s*\|)/i, + (_, prefix, __, suffix) => `${prefix}${catalog.skills.count}${suffix}`, + 'docs/zh-CN/README.md comparison table (skills)' + ); + nextContent = replaceOrThrow( + nextContent, + /^(\|\s*(?:\*\*)?智能体(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*共享\s*\(AGENTS\.md\)\s*\|\s*共享\s*\(AGENTS\.md\)\s*\|\s*12\s*\|)$/im, + (_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`, + 'docs/zh-CN/README.md parity table (agents)' + ); + nextContent = replaceOrThrow( + nextContent, + /^(\|\s*(?:\*\*)?命令(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*共享\s*\|\s*基于指令\s*\|\s*31\s*\|)$/im, + (_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`, + 'docs/zh-CN/README.md parity table (commands)' + ); + nextContent = replaceOrThrow( + nextContent, + /^(\|\s*(?:\*\*)?技能(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*共享\s*\|\s*10\s*\(原生格式\)\s*\|\s*37\s*\|)$/im, + (_, prefix, __, suffix) => `${prefix}${catalog.skills.count}${suffix}`, + 'docs/zh-CN/README.md parity table (skills)' + ); + + return nextContent; +} + +function syncZhAgents(content, catalog) { + let nextContent = content; + + nextContent = replaceOrThrow( + nextContent, + /(提供\s+)(\d+)(\s+个专业代理、\s*)(\d+)(\+?)(\s*项技能、\s*)(\d+)(\s+条命令)/i, + (_, prefix, __, agentsSuffix, ___, skillsPlus, skillsSuffix, ____, commandsSuffix) => + `${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsPlus}${skillsSuffix}${catalog.commands.count}${commandsSuffix}`, + 'docs/zh-CN/AGENTS.md summary' + ); + nextContent = replaceOrThrow( + nextContent, + /^(\s*agents\/\s*[—–-]\s*)(\d+)(\s+个专业子代理\s*)$/im, + (_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`, + 'docs/zh-CN/AGENTS.md project structure (agents)' + ); + nextContent = replaceOrThrow( + nextContent, + /^(\s*skills\/\s*[—–-]\s*)(\d+)(\+?)(\s+个工作流技能和领域知识\s*)$/im, + (_, prefix, __, plus, suffix) => `${prefix}${catalog.skills.count}${plus}${suffix}`, + 'docs/zh-CN/AGENTS.md project structure (skills)' + ); + nextContent = replaceOrThrow( + nextContent, + /^(\s*commands\/\s*[—–-]\s*)(\d+)(\s+个斜杠命令\s*)$/im, + (_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`, + 'docs/zh-CN/AGENTS.md project structure (commands)' + ); + + return nextContent; +} + +const DOCUMENT_SPECS = [ + { + filePath: README_PATH, + parseExpectations: parseReadmeExpectations, + syncContent: syncEnglishReadme, + }, + { + filePath: AGENTS_PATH, + parseExpectations: parseAgentsDocExpectations, + syncContent: syncEnglishAgents, + }, + { + filePath: README_ZH_CN_PATH, + parseExpectations: parseZhRootReadmeExpectations, + syncContent: syncZhRootReadme, + }, + { + filePath: DOCS_ZH_CN_README_PATH, + parseExpectations: parseZhDocsReadmeExpectations, + syncContent: syncZhDocsReadme, + }, + { + filePath: DOCS_ZH_CN_AGENTS_PATH, + parseExpectations: parseZhAgentsDocExpectations, + syncContent: syncZhAgents, + }, +]; + function renderText(result) { console.log('Catalog counts:'); console.log(`- agents: ${result.catalog.agents.count}`); @@ -215,12 +608,20 @@ function renderMarkdown(result) { function main() { const catalog = buildCatalog(); - const readmeContent = readFileOrThrow(README_PATH); - const agentsContent = readFileOrThrow(AGENTS_PATH); - const expectations = [ - ...parseReadmeExpectations(readmeContent), - ...parseAgentsDocExpectations(agentsContent) - ]; + + if (WRITE_MODE) { + for (const spec of DOCUMENT_SPECS) { + const currentContent = readFileOrThrow(spec.filePath); + const nextContent = spec.syncContent(currentContent, catalog); + if (nextContent !== currentContent) { + writeFileOrThrow(spec.filePath, nextContent); + } + } + } + + const expectations = DOCUMENT_SPECS.flatMap(spec => ( + spec.parseExpectations(readFileOrThrow(spec.filePath)) + )); const checks = evaluateExpectations(catalog, expectations); const result = { catalog, checks }; diff --git a/scripts/gan-harness.sh b/scripts/gan-harness.sh index c3865809..f720135e 100755 --- a/scripts/gan-harness.sh +++ b/scripts/gan-harness.sh @@ -54,7 +54,7 @@ NC='\033[0m' log() { echo -e "${BLUE}[GAN-HARNESS]${NC} $*"; } ok() { echo -e "${GREEN}[✓]${NC} $*"; } -warn() { echo -e "${YELLOW}[⚠]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } fail() { echo -e "${RED}[✗]${NC} $*"; } phase() { echo -e "\n${PURPLE}═══════════════════════════════════════════════${NC}"; echo -e "${PURPLE} $*${NC}"; echo -e "${PURPLE}═══════════════════════════════════════════════${NC}\n"; } @@ -159,7 +159,7 @@ for (( i=1; i<=MAX_ITERATIONS; i++ )); do log "━━━ Iteration $i / $MAX_ITERATIONS ━━━" # ── GENERATE ── - echo -e "${GREEN}▶ GENERATOR (iteration $i)${NC}" + echo -e "${GREEN}>> GENERATOR (iteration $i)${NC}" FEEDBACK_CONTEXT="" if [ $i -gt 1 ] && [ -f "${FEEDBACK_DIR}/feedback-$(printf '%03d' $((i-1))).md" ]; then @@ -181,7 +181,7 @@ Update gan-harness/generator-state.md." \ ok "Generator completed iteration $i" # ── EVALUATE ── - echo -e "${RED}▶ EVALUATOR (iteration $i)${NC}" + echo -e "${RED}>> EVALUATOR (iteration $i)${NC}" claude -p --model "$EVALUATOR_MODEL" \ --allowedTools "Read,Write,Bash,Grep,Glob" \ @@ -217,7 +217,7 @@ Include the weighted TOTAL score in the format: | **TOTAL** | | | **X.X** |" \ # ── CHECK PASS ── if score_passes "$SCORE" "$PASS_THRESHOLD"; then echo "" - ok "🎉 PASSED at iteration $i with score $SCORE (threshold: $PASS_THRESHOLD)" + ok "PASSED at iteration $i with score $SCORE (threshold: $PASS_THRESHOLD)" break fi @@ -256,7 +256,7 @@ cat > "${HARNESS_DIR}/build-report.md" << EOF # GAN Harness Build Report **Brief:** $BRIEF -**Result:** $(score_passes "$FINAL_SCORE" "$PASS_THRESHOLD" && echo "✅ PASS" || echo "❌ FAIL") +**Result:** $(score_passes "$FINAL_SCORE" "$PASS_THRESHOLD" && echo "PASS" || echo "FAIL") **Iterations:** $NUM_ITERATIONS / $MAX_ITERATIONS **Final Score:** $FINAL_SCORE / 10.0 (threshold: $PASS_THRESHOLD) **Elapsed:** $ELAPSED @@ -287,9 +287,9 @@ ok "Report written to ${HARNESS_DIR}/build-report.md" echo "" log "━━━ Final Results ━━━" if score_passes "$FINAL_SCORE" "$PASS_THRESHOLD"; then - echo -e "${GREEN} Result: PASS ✅${NC}" + echo -e "${GREEN} Result: PASS${NC}" else - echo -e "${RED} Result: FAIL ❌${NC}" + echo -e "${RED} Result: FAIL${NC}" fi echo -e " Score: ${CYAN}${FINAL_SCORE}${NC} / 10.0" echo -e " Iterations: ${NUM_ITERATIONS} / ${MAX_ITERATIONS}" diff --git a/scripts/lib/install-manifests.js b/scripts/lib/install-manifests.js index 0a7ecf45..cd6541d6 100644 --- a/scripts/lib/install-manifests.js +++ b/scripts/lib/install-manifests.js @@ -1,7 +1,7 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); -const { planInstallTargetScaffold } = require('./install-targets/registry'); +const { getInstallTargetAdapter, planInstallTargetScaffold } = require('./install-targets/registry'); const DEFAULT_REPO_ROOT = path.join(__dirname, '../..'); const SUPPORTED_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity', 'codex', 'gemini', 'opencode', 'codebuddy']; @@ -76,6 +76,48 @@ function dedupeStrings(values) { return [...new Set((Array.isArray(values) ? values : []).map(value => String(value).trim()).filter(Boolean))]; } +function readOptionalStringOption(options, key) { + if ( + !Object.prototype.hasOwnProperty.call(options, key) + || options[key] === null + || options[key] === undefined + ) { + return null; + } + + if (typeof options[key] !== 'string' || options[key].trim() === '') { + throw new Error(`${key} must be a non-empty string when provided`); + } + + return options[key]; +} + +function readModuleTargetsOrThrow(module) { + const moduleId = module && module.id ? module.id : ''; + const targets = module && module.targets; + + if (!Array.isArray(targets)) { + throw new Error(`Install module ${moduleId} has invalid targets; expected an array of supported target ids`); + } + + const normalizedTargets = targets.map(target => ( + typeof target === 'string' ? target.trim() : '' + )); + + if (normalizedTargets.some(target => target.length === 0)) { + throw new Error(`Install module ${moduleId} has invalid targets; expected an array of supported target ids`); + } + + const unsupportedTargets = normalizedTargets.filter(target => !SUPPORTED_INSTALL_TARGETS.includes(target)); + if (unsupportedTargets.length > 0) { + throw new Error( + `Install module ${moduleId} has unsupported targets: ${unsupportedTargets.join(', ')}` + ); + } + + return normalizedTargets; +} + function assertKnownModuleIds(moduleIds, manifests) { const unknownModuleIds = dedupeStrings(moduleIds) .filter(moduleId => !manifests.modulesById.has(moduleId)); @@ -125,6 +167,11 @@ function loadInstallManifests(options = {}) { ? profilesData.profiles : {}; const components = Array.isArray(componentsData.components) ? componentsData.components : []; + + for (const module of modules) { + readModuleTargetsOrThrow(module); + } + const modulesById = new Map(modules.map(module => [module.id, module])); const componentsById = new Map(components.map(component => [component.id, component])); @@ -361,6 +408,16 @@ function resolveInstallPlan(options = {}) { `Unknown install target: ${target}. Expected one of ${SUPPORTED_INSTALL_TARGETS.join(', ')}` ); } + const validatedProjectRoot = readOptionalStringOption(options, 'projectRoot'); + const validatedHomeDir = readOptionalStringOption(options, 'homeDir'); + const targetPlanningInput = target + ? { + repoRoot: manifests.repoRoot, + projectRoot: validatedProjectRoot || manifests.repoRoot, + homeDir: validatedHomeDir || os.homedir(), + } + : null; + const targetAdapter = target ? getInstallTargetAdapter(target) : null; const effectiveRequestedIds = dedupeStrings( requestedModuleIds.filter(moduleId => !excludedModuleOwners.has(moduleId)) @@ -396,7 +453,13 @@ function resolveInstallPlan(options = {}) { return; } - if (target && !module.targets.includes(target)) { + const supportsTarget = !target + || ( + readModuleTargetsOrThrow(module).includes(target) + && (!targetAdapter || targetAdapter.supportsModule(module, targetPlanningInput)) + ); + + if (!supportsTarget) { if (dependencyOf) { skippedTargetIds.add(rootRequesterId || dependencyOf); return false; @@ -444,9 +507,9 @@ function resolveInstallPlan(options = {}) { const scaffoldPlan = target ? planInstallTargetScaffold({ target, - repoRoot: manifests.repoRoot, - projectRoot: options.projectRoot || manifests.repoRoot, - homeDir: options.homeDir || os.homedir(), + repoRoot: targetPlanningInput.repoRoot, + projectRoot: targetPlanningInput.projectRoot, + homeDir: targetPlanningInput.homeDir, modules: selectedModules, }) : null; diff --git a/scripts/lib/install-targets/antigravity-project.js b/scripts/lib/install-targets/antigravity-project.js index 818a0a7a..2db1af3d 100644 --- a/scripts/lib/install-targets/antigravity-project.js +++ b/scripts/lib/install-targets/antigravity-project.js @@ -4,14 +4,28 @@ const { createFlatRuleOperations, createInstallTargetAdapter, createManagedScaffoldOperation, + normalizeRelativePath, } = require('./helpers'); +const SUPPORTED_SOURCE_PREFIXES = ['rules', 'commands', 'agents', 'skills', '.agents', 'AGENTS.md']; + +function supportsAntigravitySourcePath(sourceRelativePath) { + const normalizedPath = normalizeRelativePath(sourceRelativePath); + return SUPPORTED_SOURCE_PREFIXES.some(prefix => ( + normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`) + )); +} + module.exports = createInstallTargetAdapter({ id: 'antigravity-project', target: 'antigravity', kind: 'project', rootSegments: ['.agent'], installStatePathSegments: ['ecc-install-state.json'], + supportsModule(module) { + const paths = Array.isArray(module && module.paths) ? module.paths : []; + return paths.length > 0; + }, planOperations(input, adapter) { const modules = Array.isArray(input.modules) ? input.modules @@ -30,7 +44,9 @@ module.exports = createInstallTargetAdapter({ return modules.flatMap(module => { const paths = Array.isArray(module.paths) ? module.paths : []; - return paths.flatMap(sourceRelativePath => { + return paths + .filter(supportsAntigravitySourcePath) + .flatMap(sourceRelativePath => { if (sourceRelativePath === 'rules') { return createFlatRuleOperations({ moduleId: module.id, @@ -62,8 +78,8 @@ module.exports = createInstallTargetAdapter({ ]; } - return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)]; - }); + return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)]; + }); }); }, }); diff --git a/scripts/lib/install-targets/helpers.js b/scripts/lib/install-targets/helpers.js index fda3e387..ceb0d903 100644 --- a/scripts/lib/install-targets/helpers.js +++ b/scripts/lib/install-targets/helpers.js @@ -276,6 +276,13 @@ function createInstallTargetAdapter(config) { input )); }, + supportsModule(module, input = {}) { + if (typeof config.supportsModule === 'function') { + return config.supportsModule(module, input, adapter); + } + + return true; + }, validate(input = {}) { if (typeof config.validate === 'function') { return config.validate(input, adapter); diff --git a/skills/gan-style-harness/SKILL.md b/skills/gan-style-harness/SKILL.md index 5186860e..343572a6 100644 --- a/skills/gan-style-harness/SKILL.md +++ b/skills/gan-style-harness/SKILL.md @@ -48,14 +48,14 @@ This is the same dynamic as GANs (Generative Adversarial Networks): the Generato │ FEEDBACK LOOP │ │ │ │ ┌──────────┐ │ - │ │GENERATOR │──build──▶│──┐ + │ │GENERATOR │--build-->│──┐ │ │(Opus 4.6)│ │ │ │ └────▲─────┘ │ │ │ │ │ │ live app │ feedback │ │ │ │ │ │ │ ┌────┴─────┐ │ │ - │ │EVALUATOR │◀─test───│──┘ + │ │EVALUATOR │<-test----│──┘ │ │(Opus 4.6)│ │ │ │+Playwright│ │ │ └──────────┘ │ diff --git a/tests/ci/validators.test.js b/tests/ci/validators.test.js index b4dcefc6..b48a4ca3 100644 --- a/tests/ci/validators.test.js +++ b/tests/ci/validators.test.js @@ -156,12 +156,19 @@ function runCatalogValidator(overrides = {}) { const validatorPath = path.join(validatorsDir, 'catalog.js'); let source = fs.readFileSync(validatorPath, 'utf8'); source = stripShebang(source); - source = `process.argv.push('--text');\n${source}`; + const argv = Array.isArray(overrides.argv) && overrides.argv.length > 0 + ? overrides.argv + : ['--text']; + const argvPreamble = argv.map(arg => `process.argv.push(${JSON.stringify(arg)});`).join('\n'); + source = `${argvPreamble}\n${source}`; const resolvedOverrides = { ROOT: repoRoot, README_PATH: path.join(repoRoot, 'README.md'), AGENTS_PATH: path.join(repoRoot, 'AGENTS.md'), + README_ZH_CN_PATH: path.join(repoRoot, 'README.zh-CN.md'), + DOCS_ZH_CN_README_PATH: path.join(repoRoot, 'docs', 'zh-CN', 'README.md'), + DOCS_ZH_CN_AGENTS_PATH: path.join(repoRoot, 'docs', 'zh-CN', 'AGENTS.md'), ...overrides, }; @@ -176,29 +183,50 @@ function runCatalogValidator(overrides = {}) { function writeCatalogFixture(testDir, options = {}) { const { readmeCounts = { agents: 1, skills: 1, commands: 1 }, + readmeTableCounts = readmeCounts, + readmeParityCounts = readmeCounts, + readmeUnrelatedSkillsCount = 16, summaryCounts = { agents: 1, skills: 1, commands: 1 }, structureLines = [ 'agents/ — 1 specialized subagents', 'skills/ — 1 workflow skills and domain knowledge', 'commands/ — 1 slash commands', ], + zhRootReadmeCounts = { agents: 1, skills: 1, commands: 1 }, + zhDocsReadmeCounts = { agents: 1, skills: 1, commands: 1 }, + zhDocsTableCounts = zhDocsReadmeCounts, + zhDocsParityCounts = zhDocsReadmeCounts, + zhDocsUnrelatedSkillsCount = 16, + zhAgentsSummaryCounts = { agents: 1, skills: 1, commands: 1 }, + zhAgentsStructureLines = [ + 'agents/ — 1 个专业子代理', + 'skills/ — 1 个工作流技能和领域知识', + 'commands/ — 1 个斜杠命令', + ], } = options; const readmePath = path.join(testDir, 'README.md'); const agentsPath = path.join(testDir, 'AGENTS.md'); + const zhRootReadmePath = path.join(testDir, 'README.zh-CN.md'); + const zhDocsReadmePath = path.join(testDir, 'docs', 'zh-CN', 'README.md'); + const zhAgentsPath = path.join(testDir, 'docs', 'zh-CN', 'AGENTS.md'); fs.mkdirSync(path.join(testDir, 'agents'), { recursive: true }); fs.mkdirSync(path.join(testDir, 'commands'), { recursive: true }); fs.mkdirSync(path.join(testDir, 'skills', 'demo-skill'), { recursive: true }); + fs.mkdirSync(path.join(testDir, 'docs', 'zh-CN'), { recursive: true }); fs.writeFileSync(path.join(testDir, 'agents', 'planner.md'), '---\nmodel: sonnet\ntools: Read\n---\n# Planner'); 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| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| Agents | PASS: ${readmeCounts.agents} agents | Shared | Shared | 1 |\n| Commands | PASS: ${readmeCounts.commands} commands | Shared | Shared | 1 |\n| Skills | PASS: ${readmeCounts.skills} skills | Shared | Shared | 1 |\n`); + fs.writeFileSync(readmePath, `Access to ${readmeCounts.agents} agents, ${readmeCounts.skills} skills, and ${readmeCounts.commands} commands.\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`); + fs.writeFileSync(zhAgentsPath, `这是一个**生产就绪的 AI 编码插件**,提供 ${zhAgentsSummaryCounts.agents} 个专业代理、${zhAgentsSummaryCounts.skills} 项技能、${zhAgentsSummaryCounts.commands} 条命令以及自动化钩子工作流,用于软件开发。\n\n\`\`\`\n${zhAgentsStructureLines.join('\n')}\n\`\`\`\n`); - return { readmePath, agentsPath }; + return { readmePath, agentsPath, zhRootReadmePath, zhDocsReadmePath, zhAgentsPath }; } function runTests() { @@ -341,20 +369,41 @@ function runTests() { if (test('fails when README and AGENTS catalog counts drift', () => { const testDir = createTestDir(); - const { readmePath, agentsPath } = writeCatalogFixture(testDir, { + const { + readmePath, + agentsPath, + zhRootReadmePath, + zhDocsReadmePath, + zhAgentsPath, + } = writeCatalogFixture(testDir, { readmeCounts: { agents: 99, skills: 99, commands: 99 }, + readmeTableCounts: { agents: 99, skills: 99, commands: 99 }, + readmeParityCounts: { agents: 99, skills: 99, commands: 99 }, summaryCounts: { agents: 99, skills: 99, commands: 99 }, structureLines: [ 'agents/ — 99 specialized subagents', 'skills/ — 99 workflow skills and domain knowledge', 'commands/ — 99 slash commands', ], + zhRootReadmeCounts: { agents: 99, skills: 99, commands: 99 }, + zhDocsReadmeCounts: { agents: 99, skills: 99, commands: 99 }, + zhDocsTableCounts: { agents: 99, skills: 99, commands: 99 }, + zhDocsParityCounts: { agents: 99, skills: 99, commands: 99 }, + zhAgentsSummaryCounts: { agents: 99, skills: 99, commands: 99 }, + zhAgentsStructureLines: [ + 'agents/ — 99 个专业子代理', + 'skills/ — 99 个工作流技能和领域知识', + 'commands/ — 99 个斜杠命令', + ], }); 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, }); assert.strictEqual(result.code, 1, 'Should fail when catalog counts drift'); @@ -362,20 +411,154 @@ function runTests() { cleanupTestDir(testDir); })) passed++; else failed++; + if (test('fails when README parity table counts drift', () => { + const testDir = createTestDir(); + const { + readmePath, + agentsPath, + zhRootReadmePath, + zhDocsReadmePath, + zhAgentsPath, + } = writeCatalogFixture(testDir, { + readmeCounts: { agents: 1, skills: 1, commands: 1 }, + readmeTableCounts: { agents: 1, skills: 1, commands: 1 }, + readmeParityCounts: { agents: 9, skills: 8, commands: 7 }, + summaryCounts: { agents: 1, skills: 1, commands: 1 }, + }); + + 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, + }); + + assert.strictEqual(result.code, 1, 'Should fail when README parity table drifts'); + assert.ok( + (result.stdout + result.stderr).includes('README.md parity table'), + 'Should mention the README parity table mismatch' + ); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (test('fails when a tracked catalog document is missing', () => { + const testDir = createTestDir(); + const { + readmePath, + agentsPath, + zhRootReadmePath, + zhDocsReadmePath, + } = writeCatalogFixture(testDir); + const missingZhAgentsPath = path.join(testDir, 'docs', 'zh-CN', 'AGENTS.md'); + fs.rmSync(missingZhAgentsPath); + + 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: missingZhAgentsPath, + }); + + assert.strictEqual(result.code, 1, 'Should fail when a tracked doc is missing'); + assert.ok( + (result.stdout + result.stderr).includes('Failed to read AGENTS.md'), + 'Should mention the missing tracked document' + ); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (test('syncs tracked catalog docs in write mode without rewriting unrelated tables', () => { + const testDir = createTestDir(); + const { + readmePath, + agentsPath, + zhRootReadmePath, + zhDocsReadmePath, + zhAgentsPath, + } = writeCatalogFixture(testDir, { + readmeCounts: { agents: 9, skills: 9, commands: 9 }, + readmeTableCounts: { agents: 8, skills: 8, commands: 8 }, + readmeParityCounts: { agents: 7, skills: 7, commands: 7 }, + summaryCounts: { agents: 6, skills: 6, commands: 6 }, + zhRootReadmeCounts: { agents: 10, skills: 10, commands: 10 }, + zhDocsReadmeCounts: { agents: 11, skills: 11, commands: 11 }, + zhDocsTableCounts: { agents: 12, skills: 12, commands: 12 }, + zhDocsParityCounts: { agents: 13, skills: 13, commands: 13 }, + zhAgentsSummaryCounts: { agents: 14, skills: 14, commands: 14 }, + zhAgentsStructureLines: [ + 'agents/ — 15 个专业子代理', + 'skills/ — 16 个工作流技能和领域知识', + 'commands/ — 17 个斜杠命令', + ], + }); + + 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, + }); + + assert.strictEqual(result.code, 0, `Should sync and pass, got stderr: ${result.stderr}`); + + const readme = fs.readFileSync(readmePath, 'utf8'); + const agentsDoc = fs.readFileSync(agentsPath, 'utf8'); + const zhRootReadme = fs.readFileSync(zhRootReadmePath, 'utf8'); + const zhDocsReadme = fs.readFileSync(zhDocsReadmePath, 'utf8'); + const zhAgentsDoc = fs.readFileSync(zhAgentsPath, 'utf8'); + + assert.ok(readme.includes('Access to 1 agents, 1 skills, and 1 commands.'), 'Should sync README quick-start summary'); + assert.ok(readme.includes('| Agents | PASS: 1 agents |'), 'Should sync README comparison table'); + assert.ok(readme.includes('| Skills | 16 | .agents/skills/ |'), 'Should not rewrite unrelated README tables'); + assert.ok(readme.includes('| **Agents** | 1 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |'), 'Should sync README parity table'); + assert.ok(agentsDoc.includes('providing 1 specialized agents, 1 skills, 1 commands'), 'Should sync AGENTS summary'); + assert.ok(agentsDoc.includes('skills/ — 1 workflow skills and domain knowledge'), 'Should sync AGENTS structure'); + assert.ok(zhRootReadme.includes('你现在可以使用 1 个代理、1 个技能和 1 个命令'), 'Should sync README.zh-CN quick-start summary'); + assert.ok(zhDocsReadme.includes('你现在可以使用 1 个智能体、1 项技能和 1 个命令了'), 'Should sync docs/zh-CN/README quick-start summary'); + assert.ok(zhDocsReadme.includes('| 智能体 | \u2705 1 个 |'), 'Should sync docs/zh-CN/README comparison table'); + assert.ok(zhDocsReadme.includes('| 技能 | 16 | .agents/skills/ |'), 'Should not rewrite unrelated docs/zh-CN/README tables'); + assert.ok(zhDocsReadme.includes('| **智能体** | 1 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |'), 'Should sync docs/zh-CN/README parity table'); + assert.ok(zhAgentsDoc.includes('提供 1 个专业代理、1 项技能、1 条命令'), 'Should sync docs/zh-CN/AGENTS summary'); + assert.ok(zhAgentsDoc.includes('commands/ — 1 个斜杠命令'), 'Should sync docs/zh-CN/AGENTS structure'); + + cleanupTestDir(testDir); + })) passed++; else failed++; + if (test('accepts AGENTS project structure entries with varied spacing and dash styles', () => { const testDir = createTestDir(); - const { readmePath, agentsPath } = writeCatalogFixture(testDir, { + const { + readmePath, + agentsPath, + zhRootReadmePath, + zhDocsReadmePath, + zhAgentsPath, + } = writeCatalogFixture(testDir, { structureLines: [ ' agents/ - 1 specialized subagents ', '\tskills/\t–\t1+ workflow skills and domain knowledge\t', ' commands/ — 1 slash commands ', ], + zhAgentsStructureLines: [ + ' agents/ - 1 个专业子代理 ', + '\tskills/\t–\t1+ 个工作流技能和领域知识\t', + ' commands/ — 1 个斜杠命令 ', + ], }); 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, }); assert.strictEqual(result.code, 0, `Should accept formatting variations, got stderr: ${result.stderr}`); diff --git a/tests/lib/changed-files-store.test.js b/tests/lib/changed-files-store.test.js index c7c4e0fe..85dd6251 100644 --- a/tests/lib/changed-files-store.test.js +++ b/tests/lib/changed-files-store.test.js @@ -25,7 +25,7 @@ async function runTests() { try { store = await import(pathToFileURL(storePath).href) } catch (err) { - console.log('\n⚠ Skipping: build .opencode first (cd .opencode && npm run build)\n') + console.log('\n[warn] Skipping: build .opencode first (cd .opencode && npm run build)\n') process.exit(0) } diff --git a/tests/lib/install-manifests.test.js b/tests/lib/install-manifests.test.js index 72a488ae..4e64ea56 100644 --- a/tests/lib/install-manifests.test.js +++ b/tests/lib/install-manifests.test.js @@ -253,46 +253,142 @@ function runTests() { ); })) passed++; else failed++; + if (test('validates projectRoot and homeDir option types before adapter planning', () => { + assert.throws( + () => resolveInstallPlan({ profileId: 'core', target: 'cursor', projectRoot: 42 }), + /projectRoot must be a non-empty string when provided/ + ); + assert.throws( + () => resolveInstallPlan({ profileId: 'core', target: 'claude', homeDir: {} }), + /homeDir must be a non-empty string when provided/ + ); + })) passed++; else failed++; + if (test('skips a requested module when its dependency chain does not support the target', () => { const repoRoot = createTestRepo(); - writeJson(path.join(repoRoot, 'manifests', 'install-modules.json'), { - version: 1, - modules: [ - { - id: 'parent', - kind: 'skills', - description: 'Parent', - paths: ['parent'], - targets: ['claude'], - dependencies: ['child'], - defaultInstall: false, - cost: 'light', - stability: 'stable' - }, - { - id: 'child', - kind: 'skills', - description: 'Child', - paths: ['child'], - targets: ['cursor'], - dependencies: [], - defaultInstall: false, - cost: 'light', - stability: 'stable' + try { + writeJson(path.join(repoRoot, 'manifests', 'install-modules.json'), { + version: 1, + modules: [ + { + id: 'parent', + kind: 'skills', + description: 'Parent', + paths: ['parent'], + targets: ['claude'], + dependencies: ['child'], + defaultInstall: false, + cost: 'light', + stability: 'stable' + }, + { + id: 'child', + kind: 'skills', + description: 'Child', + paths: ['child'], + targets: ['cursor'], + dependencies: [], + defaultInstall: false, + cost: 'light', + stability: 'stable' + } + ] + }); + writeJson(path.join(repoRoot, 'manifests', 'install-profiles.json'), { + version: 1, + profiles: { + core: { description: 'Core', modules: ['parent'] } } - ] - }); - writeJson(path.join(repoRoot, 'manifests', 'install-profiles.json'), { - version: 1, - profiles: { - core: { description: 'Core', modules: ['parent'] } - } - }); + }); - const plan = resolveInstallPlan({ repoRoot, profileId: 'core', target: 'claude' }); - assert.deepStrictEqual(plan.selectedModuleIds, []); - assert.deepStrictEqual(plan.skippedModuleIds, ['parent']); - cleanupTestRepo(repoRoot); + const plan = resolveInstallPlan({ repoRoot, profileId: 'core', target: 'claude' }); + assert.deepStrictEqual(plan.selectedModuleIds, []); + assert.deepStrictEqual(plan.skippedModuleIds, ['parent']); + } finally { + cleanupTestRepo(repoRoot); + } + })) passed++; else failed++; + + if (test('fails fast when install manifest module targets is not an array', () => { + const repoRoot = createTestRepo(); + try { + writeJson(path.join(repoRoot, 'manifests', 'install-modules.json'), { + version: 1, + modules: [ + { + id: 'parent', + kind: 'skills', + description: 'Parent', + paths: ['parent'], + targets: 'claude', + dependencies: [], + defaultInstall: false, + cost: 'light', + stability: 'stable' + } + ] + }); + writeJson(path.join(repoRoot, 'manifests', 'install-profiles.json'), { + version: 1, + profiles: { + core: { description: 'Core', modules: ['parent'] } + } + }); + + assert.throws( + () => resolveInstallPlan({ repoRoot, profileId: 'core', target: 'claude' }), + /Install module parent has invalid targets; expected an array of supported target ids/ + ); + } finally { + cleanupTestRepo(repoRoot); + } + })) passed++; else failed++; + + if (test('keeps antigravity modules selected while filtering unsupported source paths', () => { + const repoRoot = createTestRepo(); + try { + writeJson(path.join(repoRoot, 'manifests', 'install-modules.json'), { + version: 1, + modules: [ + { + id: 'unsupported-antigravity', + kind: 'skills', + description: 'Unsupported', + paths: ['.cursor', 'skills/example'], + targets: ['antigravity'], + dependencies: [], + defaultInstall: false, + cost: 'light', + stability: 'stable' + } + ] + }); + writeJson(path.join(repoRoot, 'manifests', 'install-profiles.json'), { + version: 1, + profiles: { + core: { description: 'Core', modules: ['unsupported-antigravity'] } + } + }); + + const plan = resolveInstallPlan({ + repoRoot, + profileId: 'core', + target: 'antigravity', + projectRoot: '/workspace/app', + }); + assert.deepStrictEqual(plan.selectedModuleIds, ['unsupported-antigravity']); + assert.deepStrictEqual(plan.skippedModuleIds, []); + assert.ok( + plan.operations.every(operation => operation.sourceRelativePath !== '.cursor'), + 'Unsupported antigravity paths should be filtered from planned operations' + ); + assert.ok( + plan.operations.some(operation => operation.sourceRelativePath === 'skills/example'), + 'Supported antigravity skill paths should still be planned' + ); + } finally { + cleanupTestRepo(repoRoot); + } })) passed++; else failed++; console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);