@givolt/i3-composer-agent 2.1.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude/commands/i3-build.md +13 -1
  3. package/.claude/commands/i3-code-simplify.md +13 -1
  4. package/.claude/commands/i3-constraints.md +13 -1
  5. package/.claude/commands/i3-plan.md +13 -1
  6. package/.claude/commands/i3-review.md +13 -1
  7. package/.claude/commands/i3-ship.md +18 -2
  8. package/.claude/commands/i3-spec.md +13 -1
  9. package/.claude/commands/i3-test.md +13 -1
  10. package/.claude/commands/i3-webperf.md +13 -1
  11. package/.claude-plugin/marketplace.json +3 -3
  12. package/.claude-plugin/plugin.json +2 -2
  13. package/.codex-plugin/plugin.json +7 -6
  14. package/.gemini/commands/i3-build.toml +13 -1
  15. package/.gemini/commands/i3-code-simplify.toml +13 -1
  16. package/.gemini/commands/i3-constraints.toml +13 -1
  17. package/.gemini/commands/i3-planning.toml +13 -1
  18. package/.gemini/commands/i3-review.toml +13 -1
  19. package/.gemini/commands/i3-ship.toml +13 -1
  20. package/.gemini/commands/i3-spec.toml +13 -1
  21. package/.gemini/commands/i3-test.toml +13 -1
  22. package/.gemini/commands/i3-webperf.toml +13 -1
  23. package/README.md +141 -109
  24. package/agents/code-reviewer.md +13 -1
  25. package/agents/security-auditor.md +13 -1
  26. package/agents/test-engineer.md +13 -1
  27. package/agents/web-performance-auditor.md +13 -1
  28. package/bin/asset-fingerprint.cjs +1 -0
  29. package/bin/i3-composer-agent.cjs +58 -52
  30. package/bin/install.cjs +42 -19
  31. package/bin/native-content.cjs +27 -17
  32. package/bin/tool-menu.cjs +9 -0
  33. package/bin/tool-selector.cjs +164 -0
  34. package/bin/wizard.cjs +19 -30
  35. package/commands/i3-build.toml +13 -1
  36. package/commands/i3-code-simplify.toml +13 -1
  37. package/commands/i3-constraints.toml +13 -1
  38. package/commands/i3-planning.toml +13 -1
  39. package/commands/i3-review.toml +13 -1
  40. package/commands/i3-ship.toml +13 -1
  41. package/commands/i3-spec.toml +13 -1
  42. package/commands/i3-test.toml +13 -1
  43. package/commands/i3-webperf.toml +13 -1
  44. package/docs/adoption-guide.md +8 -0
  45. package/docs/agents.md +8 -0
  46. package/docs/antigravity-setup.md +8 -0
  47. package/docs/codebuddy-setup.md +31 -62
  48. package/docs/codex-setup.md +18 -29
  49. package/docs/commandcode-setup.md +8 -0
  50. package/docs/copilot-cli-setup.md +8 -0
  51. package/docs/copilot-setup.md +8 -0
  52. package/docs/cursor-setup.md +8 -0
  53. package/docs/gemini-cli-setup.md +8 -0
  54. package/docs/getting-started.md +8 -0
  55. package/docs/opencode-setup.md +8 -0
  56. package/docs/other-hosts.md +8 -0
  57. package/docs/windsurf-setup.md +8 -0
  58. package/docs/workbuddy-setup.md +41 -65
  59. package/evals/README.md +86 -0
  60. package/evals/cases/i3-api-and-interface-design.json +45 -0
  61. package/evals/cases/i3-browser-testing-with-devtools.json +44 -0
  62. package/evals/cases/i3-ci-cd-and-automation.json +45 -0
  63. package/evals/cases/i3-code-review-and-quality.json +49 -0
  64. package/evals/cases/i3-code-simplification.json +45 -0
  65. package/evals/cases/i3-constraint-driven-development.json +79 -0
  66. package/evals/cases/i3-context-engineering.json +44 -0
  67. package/evals/cases/i3-debugging-and-error-recovery.json +58 -0
  68. package/evals/cases/i3-deprecation-and-migration.json +52 -0
  69. package/evals/cases/i3-documentation-and-adrs.json +44 -0
  70. package/evals/cases/i3-doubt-driven-development.json +43 -0
  71. package/evals/cases/i3-frontend-ui-engineering.json +52 -0
  72. package/evals/cases/i3-git-workflow-and-versioning.json +47 -0
  73. package/evals/cases/i3-idea-refine.json +43 -0
  74. package/evals/cases/i3-incremental-implementation.json +57 -0
  75. package/evals/cases/i3-interview-me.json +42 -0
  76. package/evals/cases/i3-observability-and-instrumentation.json +44 -0
  77. package/evals/cases/i3-performance-optimization.json +53 -0
  78. package/evals/cases/i3-planning-and-task-breakdown.json +45 -0
  79. package/evals/cases/i3-security-and-hardening.json +53 -0
  80. package/evals/cases/i3-shipping-and-launch.json +56 -0
  81. package/evals/cases/i3-source-driven-development.json +44 -0
  82. package/evals/cases/i3-spec-driven-development.json +72 -0
  83. package/evals/cases/i3-test-driven-development.json +73 -0
  84. package/evals/cases/i3-using-agent-skills.json +44 -0
  85. package/evals/fixtures/i3-api-and-interface-design/service-brief.md +19 -0
  86. package/evals/fixtures/i3-browser-testing-with-devtools/README.md +5 -0
  87. package/evals/fixtures/i3-browser-testing-with-devtools/index.html +24 -0
  88. package/evals/fixtures/i3-browser-testing-with-devtools/server.js +15 -0
  89. package/evals/fixtures/i3-ci-cd-and-automation/package.json +8 -0
  90. package/evals/fixtures/i3-ci-cd-and-automation/src/slug.js +3 -0
  91. package/evals/fixtures/i3-ci-cd-and-automation/test/slug.test.js +9 -0
  92. package/evals/fixtures/i3-code-review-and-quality/user-search.diff +16 -0
  93. package/evals/fixtures/i3-code-simplification/config-parser.js +46 -0
  94. package/evals/fixtures/i3-code-simplification/config-parser.test.js +15 -0
  95. package/evals/fixtures/i3-context-engineering/context-audit.md +15 -0
  96. package/evals/fixtures/i3-debugging-and-error-recovery/pagination.js +8 -0
  97. package/evals/fixtures/i3-debugging-and-error-recovery/pagination.test.js +9 -0
  98. package/evals/fixtures/i3-debugging-and-error-recovery/time-pressure.md +6 -0
  99. package/evals/fixtures/i3-deprecation-and-migration/api-inventory.md +9 -0
  100. package/evals/fixtures/i3-documentation-and-adrs/decision-context.md +16 -0
  101. package/evals/fixtures/i3-doubt-driven-development/migration-plan.md +19 -0
  102. package/evals/fixtures/i3-frontend-ui-engineering/Button.tsx +7 -0
  103. package/evals/fixtures/i3-frontend-ui-engineering/design-system.md +11 -0
  104. package/evals/fixtures/i3-git-workflow-and-versioning/.eval/working-tree.patch +21 -0
  105. package/evals/fixtures/i3-git-workflow-and-versioning/app.js +7 -0
  106. package/evals/fixtures/i3-git-workflow-and-versioning/app.test.js +9 -0
  107. package/evals/fixtures/i3-incremental-implementation/reports.js +7 -0
  108. package/evals/fixtures/i3-incremental-implementation/reports.test.js +12 -0
  109. package/evals/fixtures/i3-incremental-implementation/tasks/plan.md +8 -0
  110. package/evals/fixtures/i3-observability-and-instrumentation/operations.md +11 -0
  111. package/evals/fixtures/i3-observability-and-instrumentation/payment-retry.js +14 -0
  112. package/evals/fixtures/i3-performance-optimization/benchmark.js +15 -0
  113. package/evals/fixtures/i3-performance-optimization/products.js +14 -0
  114. package/evals/fixtures/i3-planning-and-task-breakdown/notifications-spec.md +18 -0
  115. package/evals/fixtures/i3-security-and-hardening/webhook.js +11 -0
  116. package/evals/fixtures/i3-security-and-hardening/webhook.test.js +13 -0
  117. package/evals/fixtures/i3-shipping-and-launch/authority-pressure.md +6 -0
  118. package/evals/fixtures/i3-shipping-and-launch/launch-status.md +11 -0
  119. package/evals/fixtures/i3-source-driven-development/framework-task.md +10 -0
  120. package/evals/fixtures/i3-spec-driven-development/billing-brief.md +16 -0
  121. package/evals/fixtures/i3-test-driven-development/BUG.md +10 -0
  122. package/evals/fixtures/i3-test-driven-development/README.md +28 -0
  123. package/evals/fixtures/i3-test-driven-development/package.json +9 -0
  124. package/evals/fixtures/i3-test-driven-development/src/split.js +8 -0
  125. package/evals/fixtures/i3-test-driven-development/test/split.test.js +13 -0
  126. package/evals/fixtures/i3-using-agent-skills/incident.md +6 -0
  127. package/evals/fixtures/incremental-implementation-pressure/draft-export.js +17 -0
  128. package/evals/fixtures/incremental-implementation-pressure/scenario.md +9 -0
  129. package/evals/fixtures/spec-driven-development-decomposition/portal-brief.md +22 -0
  130. package/evals/fixtures/test-driven-development-ecosystem/README.md +9 -0
  131. package/evals/fixtures/test-driven-development-ecosystem/ledger.py +15 -0
  132. package/evals/fixtures/test-driven-development-ecosystem/test_ledger.py +19 -0
  133. package/evals/plugin/code-review-fires/graders/off-by-one.md +7 -0
  134. package/evals/plugin/code-review-fires/graders/severity-labels.md +4 -0
  135. package/evals/plugin/code-review-fires/graders/skill-fired.md +5 -0
  136. package/evals/plugin/code-review-fires/prompt.md +31 -0
  137. package/evals/plugin/code-review-stays-quiet/graders/not-fired.md +8 -0
  138. package/evals/plugin/code-review-stays-quiet/graders/tdd-fired.md +6 -0
  139. package/evals/plugin/code-review-stays-quiet/prompt.md +8 -0
  140. package/evals/plugin/code-review-stays-quiet-on-commit-message/graders/not-fired.md +8 -0
  141. package/evals/plugin/code-review-stays-quiet-on-commit-message/prompt.md +25 -0
  142. package/evals/zh-CN/cases/i3-api-and-interface-design.json +46 -0
  143. package/evals/zh-CN/cases/i3-browser-testing-with-devtools.json +45 -0
  144. package/evals/zh-CN/cases/i3-ci-cd-and-automation.json +46 -0
  145. package/evals/zh-CN/cases/i3-code-review-and-quality.json +46 -0
  146. package/evals/zh-CN/cases/i3-code-simplification.json +46 -0
  147. package/evals/zh-CN/cases/i3-constraint-driven-development.json +70 -0
  148. package/evals/zh-CN/cases/i3-context-engineering.json +45 -0
  149. package/evals/zh-CN/cases/i3-debugging-and-error-recovery.json +60 -0
  150. package/evals/zh-CN/cases/i3-deprecation-and-migration.json +45 -0
  151. package/evals/zh-CN/cases/i3-documentation-and-adrs.json +45 -0
  152. package/evals/zh-CN/cases/i3-doubt-driven-development.json +45 -0
  153. package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +45 -0
  154. package/evals/zh-CN/cases/i3-git-workflow-and-versioning.json +45 -0
  155. package/evals/zh-CN/cases/i3-idea-refine.json +44 -0
  156. package/evals/zh-CN/cases/i3-incremental-implementation.json +59 -0
  157. package/evals/zh-CN/cases/i3-interview-me.json +43 -0
  158. package/evals/zh-CN/cases/i3-observability-and-instrumentation.json +46 -0
  159. package/evals/zh-CN/cases/i3-performance-optimization.json +46 -0
  160. package/evals/zh-CN/cases/i3-planning-and-task-breakdown.json +46 -0
  161. package/evals/zh-CN/cases/i3-security-and-hardening.json +46 -0
  162. package/evals/zh-CN/cases/i3-shipping-and-launch.json +59 -0
  163. package/evals/zh-CN/cases/i3-source-driven-development.json +45 -0
  164. package/evals/zh-CN/cases/i3-spec-driven-development.json +62 -0
  165. package/evals/zh-CN/cases/i3-test-driven-development.json +76 -0
  166. package/evals/zh-CN/cases/i3-using-agent-skills.json +45 -0
  167. package/hooks/SDD-CACHE.md +8 -0
  168. package/hooks/SIMPLIFY-IGNORE.md +8 -0
  169. package/hooks/sdd-cache-pre.sh +5 -5
  170. package/hooks/session-start-test.sh +1 -1
  171. package/hooks/session-start.sh +3 -3
  172. package/hooks/simplify-ignore.sh +8 -8
  173. package/package.json +235 -6
  174. package/plugin.json +2 -2
  175. package/references/accessibility-checklist.md +8 -0
  176. package/references/definition-of-done.md +8 -0
  177. package/references/observability-checklist.md +8 -0
  178. package/references/orchestration-patterns.md +8 -0
  179. package/references/performance-checklist.md +8 -0
  180. package/references/security-checklist.md +8 -0
  181. package/references/testing-patterns.md +8 -0
  182. package/scripts/lib/skill-lint.js +586 -0
  183. package/scripts/run-evals.js +659 -0
  184. package/scripts/validate-artifact-paths.js +130 -0
  185. package/scripts/validate-commands.js +224 -0
  186. package/scripts/validate-reference-links.js +187 -0
  187. package/scripts/validate-skills.js +70 -0
  188. package/scripts/validate-versions.js +35 -0
  189. package/skills/i3-api-and-interface-design/SKILL.md +13 -1
  190. package/skills/i3-browser-testing-with-devtools/SKILL.md +13 -1
  191. package/skills/i3-ci-cd-and-automation/SKILL.md +13 -1
  192. package/skills/i3-code-review-and-quality/SKILL.md +13 -1
  193. package/skills/i3-code-simplification/SKILL.md +13 -1
  194. package/skills/i3-constraint-driven-development/SKILL.md +13 -1
  195. package/skills/i3-constraint-driven-development/references/floor-guard.md +8 -0
  196. package/skills/i3-context-engineering/SKILL.md +13 -1
  197. package/skills/i3-debugging-and-error-recovery/SKILL.md +13 -1
  198. package/skills/i3-deprecation-and-migration/SKILL.md +13 -1
  199. package/skills/i3-documentation-and-adrs/SKILL.md +13 -1
  200. package/skills/i3-doubt-driven-development/SKILL.md +13 -1
  201. package/skills/i3-frontend-ui-engineering/SKILL.md +13 -1
  202. package/skills/i3-git-workflow-and-versioning/SKILL.md +13 -1
  203. package/skills/i3-idea-refine/SKILL.md +13 -1
  204. package/skills/i3-idea-refine/examples.md +8 -0
  205. package/skills/i3-idea-refine/frameworks.md +8 -0
  206. package/skills/i3-idea-refine/refinement-criteria.md +8 -0
  207. package/skills/i3-idea-refine/scripts/idea-refine.sh +2 -2
  208. package/skills/i3-incremental-implementation/SKILL.md +13 -1
  209. package/skills/i3-interview-me/SKILL.md +13 -1
  210. package/skills/i3-observability-and-instrumentation/SKILL.md +13 -1
  211. package/skills/i3-performance-optimization/SKILL.md +13 -1
  212. package/skills/i3-performance-optimization/references/optimization-patterns.md +8 -0
  213. package/skills/i3-planning-and-task-breakdown/SKILL.md +13 -1
  214. package/skills/i3-security-and-hardening/SKILL.md +13 -1
  215. package/skills/i3-security-and-hardening/references/hardening-patterns.md +8 -0
  216. package/skills/i3-shipping-and-launch/SKILL.md +13 -1
  217. package/skills/i3-source-driven-development/SKILL.md +13 -1
  218. package/skills/i3-spec-driven-development/SKILL.md +13 -1
  219. package/skills/i3-test-driven-development/SKILL.md +13 -1
  220. package/skills/i3-using-agent-skills/SKILL.md +13 -1
@@ -13,9 +13,9 @@ const identity = `i3 Composer Agent v${pkg.version}`;
13
13
  const help = `${identity}
14
14
  ${pkg.name}
15
15
 
16
- Install engineering skills with a multi-tool installation wizard.
16
+ 通过多工具安装向导安装软件工程技能,默认中文回复。
17
17
 
18
- Usage:
18
+ 用法:
19
19
  npx ${pkg.name}
20
20
  npx ${pkg.name} --codebuddy
21
21
  npx ${pkg.name} --workbuddy --scope global
@@ -24,28 +24,32 @@ Usage:
24
24
  npx ${pkg.name} --prepare --claude --dir <directory>
25
25
  npx ${pkg.name} --extract --dir <directory>
26
26
 
27
- Options:
28
- --agents <ids> Comma-separated tool IDs; multiple tools are supported
29
- --codebuddy Preselect CodeBuddy desktop IDE / CodeBuddy Code
30
- --workbuddy Preselect WorkBuddy (global scope recommended)
31
- --codex Preselect Codex
32
- --claude Preselect Claude Code
33
- --scope <scope> project or global (or use --global / --local)
34
- --project-dir <dir> Project installation root (default: current directory)
35
- --yes Confirm explicit noninteractive installation
36
- --list-agents List all supported tool directories
37
- --prepare Prepare a complete plugin for native plugin registration
38
- --dir <directory> Plugin output directory; implies --prepare
39
- --dry-run Show the installation or bundle plan without writing files
40
- --force Replace existing assets; remove old files after success
41
- --version Print the package version
42
- --help Show this help
27
+ 选项:
28
+ --agents <ids> 工具 ID,逗号分隔,可同时选择多个
29
+ --codebuddy 预选 CodeBuddy 桌面 IDE / CodeBuddy Code
30
+ --workbuddy 预选 WorkBuddy(桌面版建议全局安装)
31
+ --codex 预选 Codex
32
+ --claude 预选 Claude Code
33
+ --scope <scope> project 项目 / global 全局(也可 --global / --local)
34
+ --project-dir <dir> 项目安装根目录,默认当前目录
35
+ --yes 确认明确指定的非交互安装
36
+ --list-agents 列出全部支持的工具目录
37
+ --prepare 另行导出插件目录并显示宿主注册步骤
38
+ --extract 导出完整插件内容
39
+ --dir <directory> 插件输出目录;隐含 --prepare
40
+ --dry-run 显示安装/导出计划,不写入文件
41
+ --force 确认替换已有内容,成功后清理旧文件
42
+ --version 输出包版本
43
+ --help 显示帮助
43
44
 
44
- The terminal wizard selects tools and scope, then installs native skill files.
45
- CodeBuddy, WorkBuddy and Claude Code also receive short commands and Agent roles.
46
- WorkBuddy desktop: use global scope for Skills list and input completion.
47
- Other tools receive skills and references; command availability is host-specific.
48
- Use --prepare for complete plugins, including hooks and host registration.
45
+ 终端向导先显示常用工具,WorkBuddy 在第 2 项;选择“更多”可分页/搜索全部工具。
46
+ 上下键移动/跨页,空格勾选/取消,回车确认;列表下实时显示已选,q/Esc 退出。
47
+ CodeBuddy、WorkBuddy、Claude Code 同时安装短命令和 Agent 角色。
48
+ 其他工具通过 Skills 调用,短命令支持取决于宿主。
49
+ 所有工具默认安装完整配套目录 i3-composer-agent/,含 evals、hooks、scripts 及所需资料。
50
+ WorkBuddy 桌面版建议全局安装,便于技能列表和输入补全发现。
51
+ 评测需主动运行;Hook 文件已提供,需按宿主事件协议单独启用。
52
+ --prepare 仅供插件管理器安装方式使用,不自动注册插件或启用 Hook。
49
53
  `;
50
54
 
51
55
  function quote(value) { return "'" + value.replaceAll("'", "'\\''") + "'"; }
@@ -55,7 +59,7 @@ function realDestination(target) {
55
59
  while (!fs.existsSync(existing)) {
56
60
  suffix.unshift(path.basename(existing));
57
61
  const parent = path.dirname(existing);
58
- if (parent === existing) throw new Error(`Cannot resolve destination: ${target}`);
62
+ if (parent === existing) throw new Error(`无法解析输出目录: ${target}`);
59
63
  existing = parent;
60
64
  }
61
65
  return path.join(fs.realpathSync(existing), ...suffix);
@@ -68,29 +72,29 @@ function prepare() {
68
72
  for (let i = 0; i < args.length; i++) {
69
73
  const arg = args[i];
70
74
  if (arg === '--prepare') continue;
71
- if (arg === '--workbuddy') throw new Error('For native WorkBuddy installation, use --workbuddy --scope project|global --yes without --prepare or --dir.');
75
+ if (arg === '--workbuddy') throw new Error('安装 WorkBuddy 原生技能请使用 --workbuddy --scope project|global --yes,移除 --prepare 和 --dir。');
72
76
  if (['--codebuddy', '--codex', '--claude', '--extract'].includes(arg)) {
73
- if (host) throw new Error('Choose exactly one host flag or --extract.');
77
+ if (host) throw new Error('请选择一个宿主参数或 --extract。');
74
78
  host = arg.slice(2);
75
79
  } else if (arg === '--dir') {
76
- if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error('--dir requires a directory.');
80
+ if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error('--dir 需要指定目录。');
77
81
  directory = path.resolve(args[++i]);
78
82
  } else if (arg === '--dry-run') dryRun = true;
79
83
  else if (arg === '--force') force = true;
80
- else throw new Error(`Unknown option: ${arg}`);
84
+ else throw new Error(`未知选项: ${arg}`);
81
85
  }
82
- if (!host) throw new Error('Choose --codebuddy, --codex, --claude, or --extract.');
86
+ if (!host) throw new Error('请选择 --codebuddy、--codex、--claude 或 --extract。');
83
87
  directory = realDestination(directory);
84
88
  const sourceRoot = fs.realpathSync(root);
85
89
  if (directory === sourceRoot || directory.startsWith(sourceRoot + path.sep) || sourceRoot.startsWith(directory + path.sep)) {
86
- throw new Error('The output directory must be outside this package directory.');
90
+ throw new Error('输出目录必须位于包目录之外。');
87
91
  }
88
92
  let exists = false;
89
93
  try { fs.lstatSync(directory); exists = true; } catch (error) { if (error.code !== 'ENOENT') throw error; }
90
94
  if (exists) {
91
- if (!force) throw new Error(`Output already exists: ${directory}. Choose a new directory or use --force for an i3 bundle.`);
95
+ if (!force) throw new Error(`输出目录已存在: ${directory}. 请选择新目录,或对已有 i3 插件使用 --force。`);
92
96
  const manifest = JSON.parse(fs.readFileSync(path.join(directory, 'plugin.json'), 'utf8'));
93
- if (manifest.name !== name) throw new Error('Refusing to replace a directory belonging to another plugin.');
97
+ if (manifest.name !== name) throw new Error('不能替换属于其他插件的目录。');
94
98
  }
95
99
  if (dryRun) {
96
100
  console.log(JSON.stringify({ name, version: pkg.version, directory, host, files: pkg.i3ComposerAgent.pluginFiles, replacesExisting: exists }, null, 2));
@@ -109,7 +113,7 @@ function prepare() {
109
113
  recursive: true,
110
114
  filter: file => {
111
115
  if (path.basename(file) === '.DS_Store') return false;
112
- if (fs.lstatSync(file).isSymbolicLink()) throw new Error(`Bundle contains a symlink: ${file}`);
116
+ if (fs.lstatSync(file).isSymbolicLink()) throw new Error(`插件内容中包含符号链接: ${file}`);
113
117
  return !/(?:^|[\\/])(?:results|node_modules|\.git)(?:[\\/]|$)/.test(path.relative(root, file));
114
118
  },
115
119
  });
@@ -131,18 +135,19 @@ function prepare() {
131
135
  });
132
136
  }
133
137
  console.log(identity);
134
- console.log(`Prepared ${name} at ${directory}`);
138
+ console.log(`已在 ${directory} 准备好 ${name}`);
139
+ console.log('包含 evals、hooks、scripts 及依赖资料;评测需主动运行,Hook 需按宿主事件协议启用。');
135
140
  if (host === 'extract') return;
136
- console.log('\nRegister and enable the plugin with these native commands:');
141
+ console.log('\n请在目标宿主中执行以下原生命令注册并启用插件:');
137
142
  const selector = `${name}@${name}`;
138
143
  if (host === 'codex') {
139
144
  console.log(`codex plugin marketplace add ${quote(directory)}`);
140
145
  console.log(`codex plugin add ${selector}`);
141
- console.log('Start a new Codex session and invoke the existing skill names. Lifecycle slash-command adaptation is separate.');
146
+ console.log('请新建 Codex 会话,通过 $i3- 开头的技能名称调用;生命周期短命令由宿主适配决定。');
142
147
  } else {
143
148
  console.log(`/plugin marketplace add ${quote(directory)}`);
144
149
  console.log(`/plugin install ${selector}`);
145
- console.log(`Use /${name}:i3-spec, /${name}:i3-plan, /${name}:i3-build, and /${name}:i3-ship in ${host === 'claude' ? 'Claude Code' : 'CodeBuddy'}.`);
150
+ console.log(`在目标宿主使用 /${name}:i3-spec、/${name}:i3-plan、/${name}:i3-build 和 /${name}:i3-ship: ${host === 'claude' ? 'Claude Code' : 'CodeBuddy'}.`);
146
151
  }
147
152
  }
148
153
  async function main() {
@@ -151,7 +156,7 @@ async function main() {
151
156
  if (args.some(arg => ['--extract', '--prepare', '--dir'].includes(arg))) { prepare(); return; }
152
157
  const installer = require('./install.cjs');
153
158
  if (args.length === 1 && args[0] === '--list-agents') {
154
- for (const agent of installer.registry.agents) console.log(`${agent.id.padEnd(20)} ${agent.label}${agent.globalSkillsDir ? '' : ' (project only)'}`);
159
+ for (const agent of require('./tool-menu.cjs').orderedAgents) console.log(`${agent.id.padEnd(20)} ${agent.label}${agent.globalSkillsDir ? '' : '(仅项目安装)'}`);
155
160
  return;
156
161
  }
157
162
  const options = { agents: [], projectDir: process.cwd() };
@@ -159,27 +164,27 @@ async function main() {
159
164
  const arg = args[i];
160
165
  if (['--codebuddy', '--workbuddy', '--codex', '--claude'].includes(arg)) options.agents.push(arg.slice(2));
161
166
  else if (['--agents', '--scope', '--project-dir'].includes(arg)) {
162
- if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error(`${arg} requires a value.`);
167
+ if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error(`${arg} 需要指定值。`);
163
168
  const value = args[++i];
164
169
  if (arg === '--agents') options.agents.push(value);
165
- else if (arg === '--scope') { if (options.scope && options.scope !== value) throw new Error('Choose one installation scope.'); options.scope = value; }
170
+ else if (arg === '--scope') { if (options.scope && options.scope !== value) throw new Error('请选择一个安装范围。'); options.scope = value; }
166
171
  else options.projectDir = path.resolve(value);
167
172
  } else if (arg === '--global' || arg === '--local') {
168
173
  const scope = arg === '--global' ? 'global' : 'project';
169
- if (options.scope && options.scope !== scope) throw new Error('Choose one installation scope.');
174
+ if (options.scope && options.scope !== scope) throw new Error('请选择一个安装范围。');
170
175
  options.scope = scope;
171
176
  } else if (arg === '--yes') options.yes = true;
172
177
  else if (arg === '--force') options.force = true;
173
178
  else if (arg === '--dry-run') options.dryRun = true;
174
- else throw new Error(`Unknown option: ${arg}`);
179
+ else throw new Error(`未知选项: ${arg}`);
175
180
  }
176
181
  options.agents = installer.selectAgents(options.agents);
177
182
  if (process.stdin.isTTY && process.stdout.isTTY && !options.yes && !options.dryRun) {
178
183
  const selection = await require('./wizard.cjs').choose(options, identity);
179
- if (!selection) { console.log('Installation cancelled. No files changed.'); return; }
184
+ if (!selection) { console.log('安装已取消,未修改文件。'); return; }
180
185
  Object.assign(options, selection);
181
186
  } else if (!options.agents.length || !options.scope || (!options.yes && !options.dryRun)) {
182
- throw new Error('Use an interactive terminal, or specify --agents <ids> --scope project|global --yes.');
187
+ throw new Error('请在交互终端使用向导,或明确指定 --agents <ids> --scope project|global --yes。');
183
188
  }
184
189
  const plan = installer.planInstallation(options);
185
190
  if (options.dryRun) {
@@ -188,19 +193,20 @@ async function main() {
188
193
  }
189
194
  if (!options.yes) {
190
195
  const confirmed = await require('./wizard.cjs').confirm(plan);
191
- if (!confirmed) { console.log('Installation cancelled. No files changed.'); return; }
196
+ if (!confirmed) { console.log('安装已取消,未修改文件。'); return; }
192
197
  options.force = plan.conflicts.length > 0;
193
198
  }
194
199
  installer.executeInstallation(plan, { force: options.force });
195
200
  console.log(identity);
196
- if (plan.reused.length) console.log(`Reused ${plan.reused.length} unchanged assets from the same package version.`);
201
+ if (plan.reused.length) console.log(`已复用同版本的 ${plan.reused.length} 个未修改内容。`);
197
202
  for (const target of plan.targets) {
198
- console.log(`Installed ${target.agents.join(', ')} skills at ${target.skillsDirectory}`);
199
- if (target.commandsDirectory) console.log(`Commands: /i3-spec, /i3-plan, /i3-build, /i3-ship (all ${fs.readdirSync(target.commandsDirectory).filter(file => file.endsWith('.md')).length} workflows) at ${target.commandsDirectory}`);
203
+ console.log(`已为 ${target.agents.join(', ')} 安装技能:${target.skillsDirectory}`);
204
+ if (target.commandsDirectory) console.log(`命令:/i3-spec、/i3-plan、/i3-build、/i3-ship(共 ${fs.readdirSync(target.commandsDirectory).filter(file => file.endsWith('.md')).length} 个工作流):${target.commandsDirectory}`);
205
+ console.log(`配套目录(含 evals、hooks、scripts):${target.supportDirectory}`);
206
+ console.log(`中文评测:node "${path.join(target.supportDirectory, 'scripts/run-evals.js')}" --locale zh-CN --min-rank1 95`);
200
207
  }
201
- console.log('Reload the IDE window or start a new host session to discover installed files.');
202
- if (plan.agents.some(id => !['codebuddy', 'workbuddy', 'claude-code'].includes(id))) console.log('These tools use the existing skill IDs; lifecycle slash-command support depends on the host.');
203
- console.log('Native skill installation does not enable plugin hooks.');
204
- if (plan.agents.some(id => ['codebuddy', 'codex', 'claude-code'].includes(id))) console.log('For CodeBuddy, Codex or Claude Code plugin registration, use --prepare with the matching host flag.');
208
+ console.log('请重新加载 IDE 窗口或新建宿主会话以发现安装内容。');
209
+ if (plan.agents.some(id => !['codebuddy', 'workbuddy', 'claude-code'].includes(id))) console.log('这些工具通过技能 ID 调用;生命周期短命令支持取决于宿主。');
210
+ console.log('安装完成。评测运行方式见配套目录 evals/README.md;Hook 文件已安装,尚未启用。');
205
211
  }
206
212
  main().catch(error => { console.error(`${name}: ${error.message}`); process.exitCode = 1; });
package/bin/install.cjs CHANGED
@@ -11,19 +11,20 @@ const { fingerprint } = require('./asset-fingerprint.cjs');
11
11
  const root = path.resolve(__dirname, '..');
12
12
  const name = 'i3-composer-agent';
13
13
  const version = require('../package.json').version;
14
+ const pluginFiles = require('../package.json').i3ComposerAgent.pluginFiles;
14
15
  const aliases = { claude: 'claude-code', gemini: 'gemini-cli', copilot: 'github-copilot' };
15
16
  function exists(file) {
16
17
  try { fs.lstatSync(file); return true; }
17
18
  catch (error) { if (error.code === 'ENOENT') return false; throw error; }
18
19
  }
19
20
  function selectAgents(values) {
20
- const ids = [...new Set(values.flatMap(value => value.split(',')).map(id => aliases[id.trim()] || id.trim()).filter(Boolean))];
21
- for (const id of ids) if (!registry.agents.some(agent => agent.id === id)) throw new Error(`Unknown tool: ${id}. Use --list-agents.`);
21
+ const ids = [...new Set(values.flatMap(value => value.split(/[,,]/)).map(id => aliases[id.trim()] || id.trim()).filter(Boolean))];
22
+ for (const id of ids) if (!registry.agents.some(agent => agent.id === id)) throw new Error(`未知工具:${id}。请使用 --list-agents 查看支持列表。`);
22
23
  return ids;
23
24
  }
24
25
  function globalSkillsDir(agent, home, env) {
25
26
  const config = agent.globalSkillsDir;
26
- if (!config) throw new Error(`${agent.label} supports project installation only. Choose project scope or remove this tool.`);
27
+ if (!config) throw new Error(`${agent.label} 仅支持项目安装(project installation only),请选择项目范围或移除此工具。`);
27
28
  let base = home;
28
29
  if (config.base === 'config') base = env.XDG_CONFIG_HOME && path.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : path.join(home, '.config');
29
30
  if (config.base === 'env') base = env[config.env]?.trim() || path.join(home, config.fallback);
@@ -43,23 +44,24 @@ function destination(file) {
43
44
  function checkDestination(file) {
44
45
  const source = fs.realpathSync(root);
45
46
  if (file === source || file.startsWith(source + path.sep) || source.startsWith(file + path.sep)) {
46
- throw new Error('Installation destinations must be outside this package directory.');
47
+ throw new Error('安装目标必须位于包目录之外。');
47
48
  }
48
49
  }
50
+ function assetPath(base, file) { return path.relative(base, file).split(path.sep).join('/'); }
49
51
  function manifestContent(plan, asset) {
50
52
  return JSON.stringify({ name, version, entryPrefix: 'i3-', scope: plan.scope, agents: asset.target.agents,
51
- assets: plan.assets.filter(item => item.base === asset.base && item.kind !== 'legacy').map(item => path.relative(asset.base, item.destination)) }, null, 2) + '\n';
53
+ assets: plan.assets.filter(item => item.base === asset.base && item.kind !== 'legacy').map(item => assetPath(asset.base, item.destination)) }, null, 2) + '\n';
52
54
  }
53
55
  function planInstallation({ agents, scope, projectDir = process.cwd(), home = os.homedir(), env = process.env }) {
54
56
  agents = selectAgents(agents);
55
- if (!agents.length) throw new Error('Choose at least one tool.');
56
- if (!['project', 'global'].includes(scope)) throw new Error('Choose --scope project or --scope global.');
57
+ if (!agents.length) throw new Error('请至少选择一个工具。');
58
+ if (!['project', 'global'].includes(scope)) throw new Error('请选择 --scope project 或 --scope global。');
57
59
  const targets = new Map();
58
60
  for (const id of agents) {
59
61
  const agent = registry.agents.find(item => item.id === id);
60
62
  const skillsDirectory = destination(scope === 'project' ? path.resolve(projectDir, agent.projectSkillsDir) : globalSkillsDir(agent, home, env));
61
63
  const base = path.dirname(skillsDirectory);
62
- if (!targets.has(skillsDirectory)) targets.set(skillsDirectory, { agents: [], skillsDirectory, referencesDirectory: path.join(base, 'references'), base });
64
+ if (!targets.has(skillsDirectory)) targets.set(skillsDirectory, { agents: [], skillsDirectory, referencesDirectory: path.join(base, 'references'), supportDirectory: path.join(base, name), base });
63
65
  const target = targets.get(skillsDirectory);
64
66
  target.agents.push(id);
65
67
  if (['codebuddy', 'workbuddy', 'claude-code'].includes(id)) {
@@ -70,7 +72,7 @@ function planInstallation({ agents, scope, projectDir = process.cwd(), home = os
70
72
  const assets = [];
71
73
  function addDirectory(sourceDirectory, destinationDirectory, kind) {
72
74
  if (exists(destinationDirectory) && fs.lstatSync(destinationDirectory).isSymbolicLink()) {
73
- throw new Error(`Installation directory is a symlink: ${destinationDirectory}. Choose its canonical location instead.`);
75
+ throw new Error(`安装目录是符号链接(symlink): ${destinationDirectory}. 请选择其真实目录。`);
74
76
  }
75
77
  for (const entry of fs.readdirSync(path.join(root, sourceDirectory)).sort()) {
76
78
  if (entry === '.DS_Store') continue;
@@ -82,6 +84,9 @@ function planInstallation({ agents, scope, projectDir = process.cwd(), home = os
82
84
  const previous = exists(manifestFile) && !fs.lstatSync(manifestFile).isSymbolicLink()
83
85
  ? JSON.parse(fs.readFileSync(manifestFile, 'utf8')) : undefined;
84
86
  const owned = previous?.name === name && Array.isArray(previous.assets);
87
+ // Earlier Windows installs persisted backslashes. Normalize before lookup
88
+ // and containment checks so owned upgrades work across both manifest forms.
89
+ if (owned) previous.assets = previous.assets.map(relative => typeof relative === 'string' ? relative.replaceAll('\\', '/') : relative);
85
90
  const sameVersion = owned && previous.version === version && previous.scope === scope && previous.entryPrefix === 'i3-';
86
91
  if (owned && previous.scope === scope && Array.isArray(previous.agents)) {
87
92
  const applicable = previous.agents.filter(id => {
@@ -99,17 +104,34 @@ function planInstallation({ agents, scope, projectDir = process.cwd(), home = os
99
104
  addDirectory('.claude/commands', target.commandsDirectory, 'command');
100
105
  addDirectory('agents', target.personasDirectory, 'persona');
101
106
  const docs = path.join(target.base, 'docs');
102
- if (exists(docs) && fs.lstatSync(docs).isSymbolicLink()) throw new Error(`Installation directory is a symlink: ${docs}.`);
107
+ if (exists(docs) && fs.lstatSync(docs).isSymbolicLink()) throw new Error(`安装目录是符号链接(symlink): ${docs}.`);
103
108
  assets.push({ source: path.join(root, 'docs/agents.md'), destination: destination(path.join(target.base, 'docs/agents.md')), kind: 'documentation' });
104
109
  }
105
110
  const licenseDirectory = path.join(target.referencesDirectory, name);
106
- if (exists(licenseDirectory) && fs.lstatSync(licenseDirectory).isSymbolicLink()) throw new Error(`Installation directory is a symlink: ${licenseDirectory}.`);
111
+ if (exists(licenseDirectory) && fs.lstatSync(licenseDirectory).isSymbolicLink()) throw new Error(`安装目录是符号链接(symlink): ${licenseDirectory}.`);
107
112
  assets.push({ source: path.join(root, 'LICENSE'), destination: destination(path.join(licenseDirectory, 'LICENSE')), kind: 'attribution' });
113
+ // Keep the canonical plugin layout for eval fixtures, validators and Hook
114
+ // relative paths. Native entry points above remain host-adapted copies.
115
+ for (const relative of pluginFiles) {
116
+ const file = path.join(target.supportDirectory, relative);
117
+ if (destination(file) !== file) throw new Error(`配套目录包含符号链接(symlink): ${file}`);
118
+ assets.push({ source: path.join(root, relative), destination: file, kind: 'support' });
119
+ }
108
120
  if (owned) {
109
121
  const legacy = fs.readdirSync(path.join(root, 'skills')).filter(id => id.startsWith('i3-')).map(id => `skills/${id.slice(3)}`);
110
122
  if (target.commandsDirectory) legacy.push(...fs.readdirSync(path.join(root, '.claude/commands')).filter(file => file.startsWith('i3-') && file.endsWith('.md')).map(file => `commands/${file.slice(3)}`));
111
123
  legacy.push(`${name}-LICENSE`, `${name}-NOTICE.md`);
112
- for (const relative of legacy) {
124
+ const current = new Set(assets.slice(start).map(asset => assetPath(target.base, asset.destination)));
125
+ for (const relative of previous.assets) {
126
+ // Retire only owned native entries or entries inside this package's
127
+ // companion directory. Never let a manifest extend the deletion scope.
128
+ if (typeof relative !== 'string' || relative.includes('\0') || relative.includes(':')
129
+ || relative.split('/').some(part => ['', '.', '..'].includes(part))) continue;
130
+ if (!/^(skills|commands|agents|references)\/[^/]+$/.test(relative) && !relative.startsWith(`${name}/`)) continue;
131
+ if (relative.startsWith(`${name}/`) && destination(path.join(target.base, relative)) !== path.join(target.base, relative)) continue;
132
+ if (!current.has(relative)) legacy.push(relative);
133
+ }
134
+ for (const relative of new Set(legacy)) {
113
135
  const file = destination(path.join(target.base, relative));
114
136
  if (previous.assets.includes(relative) && exists(file)) assets.push({ destination: file, kind: 'legacy' });
115
137
  }
@@ -120,7 +142,7 @@ function planInstallation({ agents, scope, projectDir = process.cwd(), home = os
120
142
  asset.buddyCommand = asset.kind === 'command' && target.agents.some(id => ['codebuddy', 'workbuddy'].includes(id));
121
143
  asset.scope = scope;
122
144
  asset.personasDirectory = target.personasDirectory;
123
- if (sameVersion && previous.assets.includes(path.relative(target.base, asset.destination)) && asset.source && exists(asset.destination)) {
145
+ if (sameVersion && previous.assets.includes(assetPath(target.base, asset.destination)) && asset.source && exists(asset.destination)) {
124
146
  asset.expectedFingerprint = fingerprint(asset.source, asset);
125
147
  asset.reusable = fingerprint(asset.destination) === asset.expectedFingerprint;
126
148
  }
@@ -138,7 +160,7 @@ function planInstallation({ agents, scope, projectDir = process.cwd(), home = os
138
160
  return plan;
139
161
  }
140
162
  function executeInstallation(plan, { force = false } = {}) {
141
- if (plan.conflicts.length && !force) throw new Error('Existing installation assets require confirmation or --force before replacement.');
163
+ if (plan.conflicts.length && !force) throw new Error('已有安装内容需要确认或使用 --force 才能替换。');
142
164
  const contexts = new Map();
143
165
  const createdDirectories = [];
144
166
  const committed = [];
@@ -171,8 +193,9 @@ function executeInstallation(plan, { force = false } = {}) {
171
193
  fs.writeFileSync(asset.staged, manifestContent(plan, asset));
172
194
  } else {
173
195
  fs.cpSync(asset.source, asset.staged, { recursive: true, filter: file => {
174
- if (fs.lstatSync(file).isSymbolicLink()) throw new Error(`Package contains a symlink: ${file}`);
175
- return path.basename(file) !== '.DS_Store';
196
+ if (fs.lstatSync(file).isSymbolicLink()) throw new Error(`包内容中包含符号链接(symlink): ${file}`);
197
+ return path.basename(file) !== '.DS_Store' && (asset.kind !== 'support'
198
+ || !/(?:^|[\\/])(?:results|node_modules|\.git)(?:[\\/]|$)/.test(path.relative(root, file)));
176
199
  } });
177
200
  portable(asset.staged, asset, asset.source.endsWith('.md'));
178
201
  }
@@ -184,12 +207,12 @@ function executeInstallation(plan, { force = false } = {}) {
184
207
  const unchanged = fingerprint(asset.destination) === asset.expectedFingerprint
185
208
  && fingerprint(asset.staged) === asset.expectedFingerprint;
186
209
  if (unchanged) continue;
187
- if (!force) throw new Error(`Destination changed during installation: ${asset.destination}. Retry with confirmed replacement.`);
210
+ if (!force) throw new Error(`安装期间目标已改变: ${asset.destination}. 请重新检查并确认替换后重试。`);
188
211
  } else if (asset.mergeable) {
189
- if (fingerprint(asset.destination) !== asset.observedFingerprint && !force) throw new Error(`Manifest changed during installation: ${asset.destination}. Retry with confirmed replacement.`);
212
+ if (fingerprint(asset.destination) !== asset.observedFingerprint && !force) throw new Error(`安装期间清单已改变: ${asset.destination}. 请重新检查并确认替换后重试。`);
190
213
  }
191
214
  // A destination may have appeared since planning; never replace it implicitly.
192
- if (exists(asset.destination) && !force && !asset.mergeable) throw new Error(`Destination appeared during installation: ${asset.destination}. Retry with confirmed replacement.`);
215
+ if (exists(asset.destination) && !force && !asset.mergeable) throw new Error(`安装期间出现了新的目标: ${asset.destination}. 请重新检查并确认替换后重试。`);
193
216
  const change = { destination: asset.destination, written: false };
194
217
  if (exists(asset.destination)) {
195
218
  change.backup = path.join(context.backup, path.relative(asset.base, asset.destination));
@@ -1,29 +1,39 @@
1
1
  'use strict';
2
-
3
- // Project commands are identical for both Buddy clients, including when the
4
- // other client is added later. Global commands use the selected profile path.
2
+ function replaceBlock(content, name, replacement) {
3
+ const start = `<!-- i3:${name}:start -->`, end = `<!-- i3:${name}:end -->`;
4
+ if (content.split(start).length !== 2 || content.split(end).length !== 2 || content.indexOf(start) >= content.indexOf(end)) {
5
+ throw new Error(`i3-ship 宿主适配标记 ${name} 缺失、重复或顺序错误,请重新安装完整发行包。`);
6
+ }
7
+ const from = content.indexOf(start), to = content.indexOf(end) + end.length;
8
+ return content.slice(0, from) + `${start}\n${replacement}\n${end}` + content.slice(to);
9
+ }
10
+ // Adapt stable blocks, never their prose. Shared project commands work for
11
+ // either Buddy client; global paths belong to the selected profile.
5
12
  function nativeContent(content, asset) {
13
+ if (asset.kind === 'support') return content;
6
14
  content = content.replaceAll('i3-composer-agent:', '').replaceAll('agent-skills:', '');
7
15
  if (!asset.buddyCommand) return content;
8
16
  const projectPersonas = '.codebuddy/agents/';
9
- const userPersonas = asset.scope === 'project'
10
- ? "the current host's user-level agents directory"
17
+ const userPersonas = asset.scope === 'project' ? '当前宿主的用户级 agents 目录'
11
18
  : `\`${asset.personasDirectory.replaceAll('\\', '/')}/\``;
12
19
  content = content.replaceAll('CLAUDE.md', 'CODEBUDDY.md')
13
20
  .replaceAll('`~/.claude/agents/`', userPersonas)
14
21
  .replaceAll('.claude/agents/', projectPersonas);
15
22
  if (!asset.source.endsWith('i3-ship.md')) return content;
16
- return content
17
- .replace('Spawn three subagents concurrently using the Agent tool. **Issue all three Agent tool calls in a single assistant turn so they execute in parallel** — sequential calls defeat the purpose of this command.',
18
- 'Use the host\'s native subagent capability to run three specialist personas concurrently. Issue all three calls together when the host supports parallel execution.')
19
- .replace("In Claude Code, each call passes `subagent_type` matching the persona's `name` field:",
20
- 'Select the following persona names using the host\'s native subagent mechanism:')
21
- .replace('In other harnesses without an Agent tool, invoke each persona\'s system prompt sequentially and treat their outputs as if returned in parallel — the merge phase still works.',
22
- 'If native subagents are unavailable, read each persona prompt from the installed agents directory, perform the three reviews sequentially, and retain the same merge phase.')
23
- .replace(/Constraints \(from Claude Code's subagent model\):\n[\s\S]*?\n\n\*\*Persona resolution\.\*\*[^\n]*/,
24
- `Constraints:\n- Each specialist reviews independently and returns a report to this session.\n- Specialists do not delegate to one another.\n- Follow the host's execution and permission limits.\n\n**Persona resolution.** Read installed persona prompts from \`${asset.scope === 'project' ? projectPersonas : asset.personasDirectory.replaceAll('\\', '/') + '/'}\`. For project installations, check ${userPersonas} for the current host's user-defined personas, and follow its native precedence rules.`)
25
- .replace('The three Phase A personas run in parallel — never sequentially.',
26
- 'Run the three Phase A personas in parallel when native subagents are available; otherwise use the sequential fallback above.');
27
- }
23
+ const personas = asset.scope === 'project' ? projectPersonas : asset.personasDirectory.replaceAll('\\', '/') + '/';
24
+ content = replaceBlock(content, 'ship-dispatch', `使用宿主的原生子代理能力同时启动三个专业角色;支持并行调用时,在同一轮发出三个调用。
28
25
 
26
+ 1. **\`code-reviewer\`**:对待发布改动进行正确性、可读性、架构、安全、性能五维审查,返回标准评审报告。
27
+ 2. **\`security-auditor\`**:审查漏洞、威胁模型、OWASP Top 10、密钥、认证授权及依赖 CVE,返回标准安全报告。
28
+ 3. **\`test-engineer\`**:检查正常、边界、错误和并发路径及测试覆盖,返回标准覆盖分析。
29
+
30
+ 如果宿主的原生子代理不可用,读取安装后的角色提示,依次完成三个独立审查,保留各自报告;之后仍由主会话汇总。
31
+
32
+ 约束:
33
+ - 角色独立审查,只返回报告,不互相委派。
34
+ - 遵守当前宿主的执行、上下文和权限限制。
35
+ - 角色解析:读取 \`${personas}\`。项目安装另按宿主优先级检查${userPersonas}的自定义角色。
36
+ - 后续主会话合并重复发现,按原门槛输出 GO/NO-GO;必须提供回滚计划。严重(Critical)问题默认 NO-GO,除非用户明确接受风险。`);
37
+ return replaceBlock(content, 'ship-parallel-rule', '宿主支持原生子代理时,阶段 A 的三个角色并行运行;不支持时采用上文的顺序降级。');
38
+ }
29
39
  module.exports = { nativeContent };
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+ const registry = require('./agent-targets.json');
3
+ const commonIds = ['codebuddy', 'workbuddy', 'codex', 'claude-code', 'cursor', 'gemini-cli'];
4
+ const orderedAgents = [...commonIds.map(id => registry.agents.find(agent => agent.id === id)),
5
+ ...registry.agents.filter(agent => !commonIds.includes(agent.id))];
6
+ if (orderedAgents.some(agent => !agent) || new Set(orderedAgents.map(agent => agent.id)).size !== registry.agents.length) {
7
+ throw new Error('安装目录排序与工具登记不一致。');
8
+ }
9
+ module.exports = { orderedAgents, commonIds };
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ const readline = require('node:readline');
4
+ const { orderedAgents, commonIds } = require('./tool-menu.cjs');
5
+
6
+ // Terminal columns are not JavaScript string lengths: Chinese characters use
7
+ // two cells. Keep every rendered line within the viewport before repainting.
8
+ function cellWidth(character) {
9
+ const code = character.codePointAt(0);
10
+ if (/\p{Mark}/u.test(character) || code === 0x200d) return 0;
11
+ return code >= 0x1100 && (code <= 0x115f || code === 0x2329 || code === 0x232a
12
+ || (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f)
13
+ || (code >= 0xac00 && code <= 0xd7a3) || (code >= 0xf900 && code <= 0xfaff)
14
+ || (code >= 0xfe10 && code <= 0xfe19) || (code >= 0xfe30 && code <= 0xfe6f)
15
+ || (code >= 0xff00 && code <= 0xff60) || (code >= 0xffe0 && code <= 0xffe6)
16
+ || (code >= 0x1f300 && code <= 0x1faff) || (code >= 0x20000 && code <= 0x3fffd)) ? 2 : 1;
17
+ }
18
+ function width(text) { return [...text].reduce((size, character) => size + cellWidth(character), 0); }
19
+ function clip(text, columns) {
20
+ if (width(text) <= columns) return text;
21
+ const suffix = columns >= 3 ? '...' : '';
22
+ let result = '', size = 0;
23
+ for (const character of text) {
24
+ size += cellWidth(character);
25
+ if (size > columns - suffix.length) break;
26
+ result += character;
27
+ }
28
+ return result + suffix;
29
+ }
30
+ function wrap(text, columns) {
31
+ const lines = [];
32
+ let line = '', size = 0;
33
+ for (const character of text) {
34
+ const cells = cellWidth(character);
35
+ if (line && size + cells > columns) { lines.push(line); line = ''; size = 0; }
36
+ line += character; size += cells;
37
+ }
38
+ lines.push(line);
39
+ return lines;
40
+ }
41
+
42
+ function selectTools(initialSelection, input = process.stdin, output = process.stdout) {
43
+ const selected = new Set(initialSelection);
44
+ let view = 'common', cursor = 0, commonCursor = 0, query = '', searching = false, message = '';
45
+ let frame = [], pageSize = 10, finished = false;
46
+ const wasRaw = Boolean(input.isRaw), wasFlowing = input.readableFlowing;
47
+ const choices = () => view === 'common' ? [...orderedAgents.slice(0, commonIds.length), null]
48
+ : view === 'more' ? orderedAgents.slice(commonIds.length)
49
+ : orderedAgents.filter(agent => `${agent.label} ${agent.id}`.toLowerCase().includes(query.toLowerCase()));
50
+ const selectedLabels = () => [...selected].map(id => orderedAgents.find(agent => agent.id === id).label).join('、');
51
+
52
+ function clearFrame() {
53
+ if (!frame.length) return;
54
+ const columns = output.columns || 80;
55
+ const rows = frame.reduce((count, line) => count + Math.max(1, Math.ceil(width(line) / columns)), 0);
56
+ readline.moveCursor(output, 0, -rows);
57
+ readline.cursorTo(output, 0);
58
+ readline.clearScreenDown(output);
59
+ frame = [];
60
+ }
61
+ function render() {
62
+ if (finished) return;
63
+ const columns = Math.max(1, (output.columns || 80) - 1), rows = output.rows || 24;
64
+ const entries = choices();
65
+ cursor = Math.max(0, Math.min(cursor, entries.length - 1));
66
+ const summary = wrap(`已选(${selected.size}):${selectedLabels() || '无'}`, columns);
67
+ const summaryLimit = rows < 16 ? 1 : 3;
68
+ if (summary.length > summaryLimit) {
69
+ summary.length = summaryLimit;
70
+ summary[summaryLimit - 1] = clip(summary[summaryLimit - 1], Math.max(0, columns - 3)) + '...';
71
+ }
72
+ const hints = searching
73
+ ? ['输入关键词实时搜索', 'Enter/↓ 浏览结果 · Esc返回 · Ctrl-C取消']
74
+ : ['↑↓ 移动/跨页 · 空格勾选 · Enter确认', 'm 更多 · / 搜索 · b/← 返回 · q/Esc取消'];
75
+ const footer = ['', ...summary, ...hints.flatMap(line => wrap(line, columns)), message];
76
+ const searchLine = view === 'search' ? [`搜索:${query}${searching ? '|' : ''}`] : [];
77
+ pageSize = Math.max(1, Math.min(10, rows - footer.length - searchLine.length - 2));
78
+ const page = Math.floor(cursor / pageSize), pages = Math.max(1, Math.ceil(entries.length / pageSize));
79
+ const title = view === 'common' ? '选择常用工具(支持多选):'
80
+ : `${view === 'more' ? '更多工具' : '搜索结果'}:第 ${page + 1}/${pages} 页,共 ${entries.length} 项`;
81
+ const lines = [title, ...searchLine];
82
+ for (let index = page * pageSize; index < Math.min(entries.length, (page + 1) * pageSize); index++) {
83
+ const agent = entries[index], pointer = index === cursor ? '>' : ' ';
84
+ lines.push(agent ? `${pointer} [${selected.has(agent.id) ? 'x' : ' '}] ${orderedAgents.indexOf(agent) + 1}. ${agent.label} [${agent.id}]${agent.globalSkillsDir ? '' : '(仅项目)'}`
85
+ : `${pointer} 更多工具(空格展开)`);
86
+ }
87
+ if (!entries.length) lines.push('没有匹配工具;按 / 重新搜索,或 Esc 返回。');
88
+ clearFrame();
89
+ frame = [...lines, ...footer].map(line => clip(line, columns));
90
+ output.write(frame.join('\n') + '\n');
91
+ }
92
+
93
+ return new Promise((resolve, reject) => {
94
+ function finish(value, error) {
95
+ if (finished) return;
96
+ finished = true;
97
+ input.off('keypress', keypress);
98
+ input.off('end', cancel);
99
+ input.off('error', fail);
100
+ output.off('resize', redraw);
101
+ process.off('SIGINT', cancel);
102
+ input.setRawMode(wasRaw);
103
+ if (!wasFlowing) input.pause();
104
+ try {
105
+ clearFrame();
106
+ output.write('\x1b[?25h');
107
+ if (value) output.write(`已选择工具:${selectedLabels()}\n`);
108
+ } catch (outputError) { error ||= outputError; }
109
+ if (error) reject(error); else resolve(value);
110
+ }
111
+ function cancel() { finish(null); }
112
+ function fail(error) { finish(null, error); }
113
+ function redraw() { try { render(); } catch (error) { fail(error); } }
114
+ function openMore() {
115
+ if (view === 'common') commonCursor = cursor;
116
+ view = 'more'; cursor = 0; searching = false;
117
+ }
118
+ function back() { view = 'common'; cursor = commonCursor; searching = false; query = ''; }
119
+ function keypress(text, key = {}) {
120
+ if (finished) return;
121
+ try {
122
+ message = '';
123
+ if (key.ctrl && ['c', 'd'].includes(key.name)) { cancel(); return; }
124
+ if (searching) {
125
+ if (key.name === 'escape') back();
126
+ else if (['return', 'enter', 'down'].includes(key.name)) searching = false;
127
+ else if (key.name === 'backspace') { query = [...query].slice(0, -1).join(''); cursor = 0; }
128
+ else if (text && !key.ctrl && !key.meta && !/[\x00-\x1f\x7f]/.test(text)) { query += text; cursor = 0; }
129
+ } else if (key.name === 'escape') {
130
+ if (view === 'search') back(); else { cancel(); return; }
131
+ } else if (text === 'q') { cancel(); return; }
132
+ else if (['return', 'enter'].includes(key.name)) {
133
+ if (selected.size) { finish([...selected]); return; }
134
+ message = '请至少用空格选择一个工具。';
135
+ } else if (key.name === 'space' || text === ' ') {
136
+ const entry = choices()[cursor];
137
+ if (entry) { if (selected.has(entry.id)) selected.delete(entry.id); else selected.add(entry.id); }
138
+ else if (view === 'common') openMore();
139
+ } else if (['up', 'down', 'pageup', 'pagedown'].includes(key.name) || ['n', 'p'].includes(text)) {
140
+ const direction = ['up', 'pageup'].includes(key.name) || text === 'p' ? -1 : 1;
141
+ const step = ['pageup', 'pagedown'].includes(key.name) || ['n', 'p'].includes(text) ? pageSize : 1;
142
+ cursor = Math.max(0, Math.min(choices().length - 1, cursor + direction * step));
143
+ } else if (text === 'm' || (key.name === 'right' && view === 'common')) openMore();
144
+ else if (text === 'b' || key.name === 'left') back();
145
+ else if (text === '/') { if (view === 'common') commonCursor = cursor; view = 'search'; query = ''; cursor = 0; searching = true; }
146
+ render();
147
+ } catch (error) { fail(error); }
148
+ }
149
+ try {
150
+ readline.emitKeypressEvents(input);
151
+ input.setRawMode(true);
152
+ input.on('keypress', keypress);
153
+ input.once('end', cancel);
154
+ input.once('error', fail);
155
+ output.on('resize', redraw);
156
+ process.once('SIGINT', cancel);
157
+ output.write('\x1b[?25l');
158
+ render();
159
+ input.resume();
160
+ } catch (error) { fail(error); }
161
+ });
162
+ }
163
+
164
+ module.exports = { selectTools };