@givolt/i3-composer-agent 2.2.0 → 2.2.2

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 (137) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/README.md +77 -17
  6. package/bin/asset-fingerprint.cjs +3 -4
  7. package/bin/i3-composer-agent.cjs +15 -7
  8. package/bin/install.cjs +48 -57
  9. package/bin/legacy-native-files.json +329 -0
  10. package/bin/native-command-validation.cjs +27 -0
  11. package/bin/native-layout.cjs +135 -0
  12. package/bin/native-migration.cjs +93 -0
  13. package/bin/native-runtime.cjs +57 -0
  14. package/bin/native-version-validation.cjs +8 -0
  15. package/bin/tool-selector.cjs +164 -0
  16. package/bin/wizard.cjs +9 -44
  17. package/docs/codebuddy-setup.md +12 -3
  18. package/docs/codex-setup.md +9 -4
  19. package/docs/workbuddy-setup.md +13 -4
  20. package/evals/README.md +95 -0
  21. package/evals/cases/i3-api-and-interface-design.json +45 -0
  22. package/evals/cases/i3-browser-testing-with-devtools.json +44 -0
  23. package/evals/cases/i3-ci-cd-and-automation.json +45 -0
  24. package/evals/cases/i3-code-review-and-quality.json +49 -0
  25. package/evals/cases/i3-code-simplification.json +45 -0
  26. package/evals/cases/i3-constraint-driven-development.json +79 -0
  27. package/evals/cases/i3-context-engineering.json +44 -0
  28. package/evals/cases/i3-debugging-and-error-recovery.json +58 -0
  29. package/evals/cases/i3-deprecation-and-migration.json +52 -0
  30. package/evals/cases/i3-documentation-and-adrs.json +44 -0
  31. package/evals/cases/i3-doubt-driven-development.json +43 -0
  32. package/evals/cases/i3-frontend-ui-engineering.json +52 -0
  33. package/evals/cases/i3-git-workflow-and-versioning.json +47 -0
  34. package/evals/cases/i3-idea-refine.json +43 -0
  35. package/evals/cases/i3-incremental-implementation.json +57 -0
  36. package/evals/cases/i3-interview-me.json +42 -0
  37. package/evals/cases/i3-observability-and-instrumentation.json +44 -0
  38. package/evals/cases/i3-performance-optimization.json +53 -0
  39. package/evals/cases/i3-planning-and-task-breakdown.json +45 -0
  40. package/evals/cases/i3-security-and-hardening.json +53 -0
  41. package/evals/cases/i3-shipping-and-launch.json +56 -0
  42. package/evals/cases/i3-source-driven-development.json +44 -0
  43. package/evals/cases/i3-spec-driven-development.json +72 -0
  44. package/evals/cases/i3-test-driven-development.json +73 -0
  45. package/evals/cases/i3-using-agent-skills.json +44 -0
  46. package/evals/fixtures/i3-api-and-interface-design/service-brief.md +19 -0
  47. package/evals/fixtures/i3-browser-testing-with-devtools/README.md +5 -0
  48. package/evals/fixtures/i3-browser-testing-with-devtools/index.html +24 -0
  49. package/evals/fixtures/i3-browser-testing-with-devtools/server.js +15 -0
  50. package/evals/fixtures/i3-ci-cd-and-automation/package.json +8 -0
  51. package/evals/fixtures/i3-ci-cd-and-automation/src/slug.js +3 -0
  52. package/evals/fixtures/i3-ci-cd-and-automation/test/slug.test.js +9 -0
  53. package/evals/fixtures/i3-code-review-and-quality/user-search.diff +16 -0
  54. package/evals/fixtures/i3-code-simplification/config-parser.js +46 -0
  55. package/evals/fixtures/i3-code-simplification/config-parser.test.js +15 -0
  56. package/evals/fixtures/i3-context-engineering/context-audit.md +15 -0
  57. package/evals/fixtures/i3-debugging-and-error-recovery/pagination.js +8 -0
  58. package/evals/fixtures/i3-debugging-and-error-recovery/pagination.test.js +9 -0
  59. package/evals/fixtures/i3-debugging-and-error-recovery/time-pressure.md +6 -0
  60. package/evals/fixtures/i3-deprecation-and-migration/api-inventory.md +9 -0
  61. package/evals/fixtures/i3-documentation-and-adrs/decision-context.md +16 -0
  62. package/evals/fixtures/i3-doubt-driven-development/migration-plan.md +19 -0
  63. package/evals/fixtures/i3-frontend-ui-engineering/Button.tsx +7 -0
  64. package/evals/fixtures/i3-frontend-ui-engineering/design-system.md +11 -0
  65. package/evals/fixtures/i3-git-workflow-and-versioning/.eval/working-tree.patch +21 -0
  66. package/evals/fixtures/i3-git-workflow-and-versioning/app.js +7 -0
  67. package/evals/fixtures/i3-git-workflow-and-versioning/app.test.js +9 -0
  68. package/evals/fixtures/i3-incremental-implementation/reports.js +7 -0
  69. package/evals/fixtures/i3-incremental-implementation/reports.test.js +12 -0
  70. package/evals/fixtures/i3-incremental-implementation/tasks/plan.md +8 -0
  71. package/evals/fixtures/i3-observability-and-instrumentation/operations.md +11 -0
  72. package/evals/fixtures/i3-observability-and-instrumentation/payment-retry.js +14 -0
  73. package/evals/fixtures/i3-performance-optimization/benchmark.js +15 -0
  74. package/evals/fixtures/i3-performance-optimization/products.js +14 -0
  75. package/evals/fixtures/i3-planning-and-task-breakdown/notifications-spec.md +18 -0
  76. package/evals/fixtures/i3-security-and-hardening/webhook.js +11 -0
  77. package/evals/fixtures/i3-security-and-hardening/webhook.test.js +13 -0
  78. package/evals/fixtures/i3-shipping-and-launch/authority-pressure.md +6 -0
  79. package/evals/fixtures/i3-shipping-and-launch/launch-status.md +11 -0
  80. package/evals/fixtures/i3-source-driven-development/framework-task.md +10 -0
  81. package/evals/fixtures/i3-spec-driven-development/billing-brief.md +16 -0
  82. package/evals/fixtures/i3-test-driven-development/BUG.md +10 -0
  83. package/evals/fixtures/i3-test-driven-development/README.md +28 -0
  84. package/evals/fixtures/i3-test-driven-development/package.json +9 -0
  85. package/evals/fixtures/i3-test-driven-development/src/split.js +8 -0
  86. package/evals/fixtures/i3-test-driven-development/test/split.test.js +13 -0
  87. package/evals/fixtures/i3-using-agent-skills/incident.md +6 -0
  88. package/evals/fixtures/incremental-implementation-pressure/draft-export.js +17 -0
  89. package/evals/fixtures/incremental-implementation-pressure/scenario.md +9 -0
  90. package/evals/fixtures/spec-driven-development-decomposition/portal-brief.md +22 -0
  91. package/evals/fixtures/test-driven-development-ecosystem/README.md +9 -0
  92. package/evals/fixtures/test-driven-development-ecosystem/ledger.py +15 -0
  93. package/evals/fixtures/test-driven-development-ecosystem/test_ledger.py +19 -0
  94. package/evals/plugin/code-review-fires/graders/off-by-one.md +7 -0
  95. package/evals/plugin/code-review-fires/graders/severity-labels.md +4 -0
  96. package/evals/plugin/code-review-fires/graders/skill-fired.md +5 -0
  97. package/evals/plugin/code-review-fires/prompt.md +31 -0
  98. package/evals/plugin/code-review-stays-quiet/graders/not-fired.md +8 -0
  99. package/evals/plugin/code-review-stays-quiet/graders/tdd-fired.md +6 -0
  100. package/evals/plugin/code-review-stays-quiet/prompt.md +8 -0
  101. package/evals/plugin/code-review-stays-quiet-on-commit-message/graders/not-fired.md +8 -0
  102. package/evals/plugin/code-review-stays-quiet-on-commit-message/prompt.md +25 -0
  103. package/evals/skill-impact.md +6 -0
  104. package/evals/zh-CN/cases/i3-api-and-interface-design.json +46 -0
  105. package/evals/zh-CN/cases/i3-browser-testing-with-devtools.json +45 -0
  106. package/evals/zh-CN/cases/i3-ci-cd-and-automation.json +46 -0
  107. package/evals/zh-CN/cases/i3-code-review-and-quality.json +46 -0
  108. package/evals/zh-CN/cases/i3-code-simplification.json +46 -0
  109. package/evals/zh-CN/cases/i3-constraint-driven-development.json +70 -0
  110. package/evals/zh-CN/cases/i3-context-engineering.json +45 -0
  111. package/evals/zh-CN/cases/i3-debugging-and-error-recovery.json +60 -0
  112. package/evals/zh-CN/cases/i3-deprecation-and-migration.json +45 -0
  113. package/evals/zh-CN/cases/i3-documentation-and-adrs.json +45 -0
  114. package/evals/zh-CN/cases/i3-doubt-driven-development.json +45 -0
  115. package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +45 -0
  116. package/evals/zh-CN/cases/i3-git-workflow-and-versioning.json +45 -0
  117. package/evals/zh-CN/cases/i3-idea-refine.json +44 -0
  118. package/evals/zh-CN/cases/i3-incremental-implementation.json +59 -0
  119. package/evals/zh-CN/cases/i3-interview-me.json +43 -0
  120. package/evals/zh-CN/cases/i3-observability-and-instrumentation.json +46 -0
  121. package/evals/zh-CN/cases/i3-performance-optimization.json +46 -0
  122. package/evals/zh-CN/cases/i3-planning-and-task-breakdown.json +46 -0
  123. package/evals/zh-CN/cases/i3-security-and-hardening.json +46 -0
  124. package/evals/zh-CN/cases/i3-shipping-and-launch.json +59 -0
  125. package/evals/zh-CN/cases/i3-source-driven-development.json +45 -0
  126. package/evals/zh-CN/cases/i3-spec-driven-development.json +62 -0
  127. package/evals/zh-CN/cases/i3-test-driven-development.json +76 -0
  128. package/evals/zh-CN/cases/i3-using-agent-skills.json +45 -0
  129. package/package.json +236 -5
  130. package/plugin.json +1 -1
  131. package/scripts/lib/skill-lint.js +586 -0
  132. package/scripts/run-evals.js +659 -0
  133. package/scripts/validate-artifact-paths.js +130 -0
  134. package/scripts/validate-commands.js +224 -0
  135. package/scripts/validate-reference-links.js +187 -0
  136. package/scripts/validate-skills.js +70 -0
  137. package/scripts/validate-versions.js +35 -0
@@ -6,7 +6,7 @@
6
6
  "plugins": [
7
7
  {
8
8
  "name": "i3-composer-agent",
9
- "version": "2.2.0",
9
+ "version": "2.2.2",
10
10
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
11
11
  "source": {
12
12
  "source": "local",
@@ -8,7 +8,7 @@
8
8
  "plugins": [
9
9
  {
10
10
  "name": "i3-composer-agent",
11
- "version": "2.2.0",
11
+ "version": "2.2.2",
12
12
  "source": "./",
13
13
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
14
14
  "homepage": "https://github.com/49780522/i3-composer-agent",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "author": {
6
6
  "name": "Givolt"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "author": {
6
6
  "name": "Givolt"
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # i3 Composer Agent v2.2.0
1
+ # i3 Composer Agent v2.2.2
2
2
 
3
3
  **Intelligence. Integration. Innovation.**
4
4
 
@@ -18,15 +18,17 @@ npx @givolt/i3-composer-agent --workbuddy --scope global
18
18
  ```
19
19
 
20
20
  首屏依次为 CodeBuddy、WorkBuddy、Codex、Claude Code、Cursor、Gemini CLI,
21
- WorkBuddy 在第 2 项。选 `7`/`更多` 打开其余工具,每页 10 项;`n` 下一页、`p` 上一页、
22
- `b` 返回,`/关键词` 搜索全部 80 个工具。编号在分页和搜索中保持一致。
23
- 输入编号或 ID(中英文逗号均可)累积多选,跨页保留;`-编号/ID` 移除选择。
24
- 回车或 `done`/`完成` 确认已选工具,随后选择项目/全局范围并确认安装;`q`/`取消` 退出。
25
- 通过参数预选的工具会保留,仍可继续多选。
26
-
27
- 全部目标均安装 25 个技能和共享参考资料。
28
- CodeBuddy、WorkBuddy、Claude Code 另安装 9 个短命令和 4 个 Agent 角色。
29
- 其他工具通过其原生 Skills 机制使用;短命令支持取决于宿主。
21
+ WorkBuddy 在第 2 项。上下键移动,空格勾选/取消,回车确认;列表原地刷新,
22
+ 下方实时显示已选工具。移到“更多”按空格或直接按 `m` 打开其他工具。
23
+ 更多列表上下移动可跨页,`n`/`p` 或 PageDown/PageUp 整页翻页,`b`/左键返回常用。
24
+ 按 `/` 输入关键词实时搜索全部 80 个工具;回车或下键结束输入,再用空格选择。
25
+ 搜索、翻页和命令参数预选均保留选择。搜索外 `q`、Esc 或 Ctrl-C 取消。
26
+ 确认工具后选择项目/全局范围并确认安装。
27
+
28
+ 全部目标均安装 25 个技能、共享参考资料,以及完整评测/Hook/脚本配套。
29
+ 全部目标同时提供 9 个命令和 4 个 Agent 角色提示。
30
+ CodeBuddy、WorkBuddy、Claude Code 使用原生命令入口;其他工具通过 Skills 机制使用,
31
+ 命令和角色文件可作为资料读取,是否自动发现取决于宿主。
30
32
  目录预设来自固定的 [Skills CLI 工具登记](https://github.com/vercel-labs/skills/blob/48dc9e8eb8aa19040e91cb90ba0b642c9cbad57e/src/agents.ts)
31
33
  并增加 WorkBuddy。目录预设验证与每个宿主的实际执行验收分别记录。
32
34
 
@@ -52,12 +54,68 @@ CodeBuddy 和 WorkBuddy 的项目安装共用 `.codebuddy/`,相同目标只安
52
54
  同版本、同范围的清单所属未修改内容会复用,新增客户端合并到清单。
53
55
  升级、修改过的内容及未登记同名冲突需确认替换;非交互使用 `--force` 前可先看 dry-run。
54
56
  安装成功后清理替换掉的旧内容及本安装器所属历史备份,失败则恢复原文件。
55
- 迁移只清理旧清单所属的无前缀 Skill/命令路径,保留自定义内容。
56
- 标准 MIT 许可随共享参考资料安装。`npx -y` 确认 npm 下载;包的 `--yes` 确认安装计划。
57
+ 升级按文件归属清理旧副本,保留用户新增文件;旧评测结果迁入新的 evals 分组。
58
+ 同名结果内容不同时会在写入前停止;未归属本包的资料冲突也需先处理,`--force` 不会接管。
59
+ 成功后不保留临时备份。`npx -y` 确认 npm 下载;包的 `--yes` 确认安装计划。
57
60
 
58
- ## 注册完整插件
61
+ ## 安装内容与可选功能
59
62
 
60
- 完整插件包含 Skills、Commands、Agents、Hooks 和 references。准备持久目录:
63
+ | 内容 | 默认安装 | 用途 |
64
+ | --- | --- | --- |
65
+ | `skills/`(含技能内部的 `scripts/` 等资源) | 是 | 工程工作流及运行辅助文件 |
66
+ | `references/i3-composer-agent/` | 是 | 技能引用的检查清单与参考资料 |
67
+ | `commands/`、`agents/` | 是;发现与调用依宿主支持 | 短命令与专业角色提示 |
68
+ | `hooks/` | 是,文件安装后按宿主方式启用 | 会话注入、文档缓存和代码块保护 |
69
+ | `scripts/` | 是 | 评测运行器、五项校验脚本及其共享库 |
70
+ | `evals/` | 是 | 中英文触发/行为用例、测试项目样例及插件评分规则 |
71
+
72
+ 以项目安装的 `.codebuddy/` 为例,默认目录为:
73
+
74
+ ```text
75
+ .codebuddy/
76
+ ├─ skills/ # 宿主和评测共用的实际技能
77
+ ├─ commands/ # 一份命令
78
+ ├─ agents/ # 一份角色提示
79
+ ├─ references/i3-composer-agent/
80
+ ├─ evals/i3-composer-agent/ # 用例、样例、插件评测与运行结果
81
+ ├─ hooks/i3-composer-agent/
82
+ ├─ scripts/i3-composer-agent/ # .cjs 入口及 lib/
83
+ ├─ docs/i3-composer-agent/
84
+ └─ .i3-composer-agent.json # 版本、布局、能力与文件归属清单
85
+ ```
86
+
87
+ 每个宿主基址只有一份技能;公共资料按插件名称分组。技能内部的 scripts/references 保留。
88
+ WorkBuddy 全局安装将基址换为 `~/.workbuddy/`,Codex 项目安装为 `.agents/`。
89
+ 评测、校验和适用的 Hook 读取宿主使用的实际内容,不生成第二份完整插件。
90
+
91
+ 从业务项目目录运行以下检查,不调用模型;也可用脚本绝对路径从任意目录运行:
92
+
93
+ ```bash
94
+ node .codebuddy/scripts/i3-composer-agent/validate-skills.cjs
95
+ node .codebuddy/scripts/i3-composer-agent/validate-versions.cjs
96
+ node .codebuddy/scripts/i3-composer-agent/validate-commands.cjs
97
+ node .codebuddy/scripts/i3-composer-agent/validate-reference-links.cjs
98
+ node .codebuddy/scripts/i3-composer-agent/validate-artifact-paths.cjs
99
+ node .codebuddy/scripts/i3-composer-agent/run-evals.cjs --locale zh-CN --min-rank1 95
100
+ ```
101
+
102
+ 模型行为评测需要主动运行并消耗模型额度,详见 [评测说明](evals/README.md)。
103
+ 原生脚本按安装清单检查本包内容,支持业务项目的 ESM/CommonJS 设置。
104
+ 独立插件导出目录仍可执行 `npm test`;上述检查均不能代替业务项目测试或宿主执行验收。
105
+ 评测结果不进入安装替换清单。安装过程不执行评测,也不自动启用 Hook。
106
+
107
+ 可选 Hook 不影响普通技能和命令的使用,默认中文交互也不依赖 Hook。
108
+ 未配置 Hook 时,自动注入、缓存和代码块保护这些增强功能不会运行。
109
+ 会话注入仅适合缺少原生技能路由的宿主;已有原生路由时不应重复注入元技能。
110
+ 缓存与代码块保护的配置说明位于 `hooks/i3-composer-agent/` 下的 `SDD-CACHE.md`、
111
+ `SIMPLIFY-IGNORE.md`,当前示例针对 Claude Code,其他宿主需适配自己的事件协议。
112
+ 脚本依赖 Bash、jq;缓存还需 curl 和哈希工具,代码块保护还需其文档列出的工具。
113
+ Windows PowerShell 本身不会自动提供这些脚本依赖。
114
+
115
+ ## 通过宿主插件管理器安装
116
+
117
+ 默认配套已包含 Skills、Commands、Agents、references、evals、hooks 和 scripts。
118
+ 若希望另行导出目录,交给宿主插件管理器管理:
61
119
 
62
120
  ```bash
63
121
  npx @givolt/i3-composer-agent --prepare --codebuddy
@@ -65,7 +123,9 @@ npx @givolt/i3-composer-agent --prepare --codex
65
123
  npx @givolt/i3-composer-agent --prepare --claude
66
124
  ```
67
125
 
68
- 此模式准备 `./i3-composer-agent/` 并输出宿主注册命令。
126
+ `--prepare` 只准备 `./i3-composer-agent/` 运行文件并输出宿主注册命令,
127
+ 不会把插件自动注册到宿主,也不会启用可选 Hook。普通技能安装完成后无需再执行它。
128
+ 需要让宿主插件管理器统一发现和管理插件时,再按下面步骤注册:
69
129
  在 CodeBuddy Code(终端)或 Claude Code 中执行:
70
130
 
71
131
  ```text
@@ -80,8 +140,8 @@ codex plugin marketplace add ./i3-composer-agent
80
140
  codex plugin add i3-composer-agent@i3-composer-agent
81
141
  ```
82
142
 
83
- 按宿主管理器选择安装范围,启用后新建会话。原生文件安装与完整插件注册是两种入口。
84
- 原生安装不自动启用插件 Hook;无需 Hook,入口本身也要求默认中文回复。
143
+ 按宿主管理器选择安装范围,启用后新建会话。原生文件安装与插件注册是两种入口。
144
+ 本包没有默认 Hook 注册配置;插件启用后,可选 Hook 仍需按上面的用途和宿主要求单独配置。
85
145
 
86
146
  ## 命令
87
147
 
@@ -3,7 +3,7 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const crypto = require('node:crypto');
6
- const { nativeContent } = require('./native-content.cjs');
6
+ const { renderAsset } = require('./native-layout.cjs');
7
7
 
8
8
  // Compare the complete tree without following links. When asset is provided,
9
9
  // hash the source as it will be rendered by the native installer.
@@ -24,9 +24,8 @@ function fingerprint(file, asset) {
24
24
  if (!visit(path.join(current, entry), path.join(relative, entry))) return false;
25
25
  }
26
26
  } else {
27
- const content = asset && current.endsWith('.md')
28
- ? Buffer.from(nativeContent(fs.readFileSync(current, 'utf8'), asset))
29
- : fs.readFileSync(current);
27
+ const bytes = fs.readFileSync(current);
28
+ const content = asset ? renderAsset(bytes, asset) : bytes;
30
29
  hash.update(String(content.length) + '\n');
31
30
  hash.update(content);
32
31
  }
@@ -34,7 +34,7 @@ ${pkg.name}
34
34
  --project-dir <dir> 项目安装根目录,默认当前目录
35
35
  --yes 确认明确指定的非交互安装
36
36
  --list-agents 列出全部支持的工具目录
37
- --prepare 准备完整插件,供宿主原生插件管理器注册
37
+ --prepare 另行导出插件目录并显示宿主注册步骤
38
38
  --extract 导出完整插件内容
39
39
  --dir <directory> 插件输出目录;隐含 --prepare
40
40
  --dry-run 显示安装/导出计划,不写入文件
@@ -43,11 +43,14 @@ ${pkg.name}
43
43
  --help 显示帮助
44
44
 
45
45
  终端向导先显示常用工具,WorkBuddy 在第 2 项;选择“更多”可分页/搜索全部工具。
46
- 编号或 ID 支持多选,跨页保留选择;回车或 done/完成确认,q/取消退出。
46
+ 上下键移动/跨页,空格勾选/取消,回车确认;列表下实时显示已选,q/Esc 退出。
47
47
  CodeBuddy、WorkBuddy、Claude Code 同时安装短命令和 Agent 角色。
48
- 其他工具获得 Skills 与参考资料,短命令支持取决于宿主。
48
+ 其他工具通过 Skills 调用,短命令支持取决于宿主。
49
+ 所有工具默认安装单份 skills、commands、agents;资料按 references/evals/hooks/scripts/docs 分组。
50
+ 评测、脚本和 Hook 读取同一份已安装技能。--prepare 用于另行导出标准插件并按宿主方式注册。
49
51
  WorkBuddy 桌面版建议全局安装,便于技能列表和输入补全发现。
50
- 完整插件及 Hook 注册使用 --prepare。
52
+ 评测需主动运行;Hook 文件已提供,需按宿主事件协议单独启用。
53
+ --prepare 仅供插件管理器安装方式使用,不自动注册插件或启用 Hook。
51
54
  `;
52
55
 
53
56
  function quote(value) { return "'" + value.replaceAll("'", "'\\''") + "'"; }
@@ -134,6 +137,7 @@ function prepare() {
134
137
  }
135
138
  console.log(identity);
136
139
  console.log(`已在 ${directory} 准备好 ${name}`);
140
+ console.log('包含 evals、hooks、scripts 及依赖资料;评测需主动运行,Hook 需按宿主事件协议启用。');
137
141
  if (host === 'extract') return;
138
142
  console.log('\n请在目标宿主中执行以下原生命令注册并启用插件:');
139
143
  const selector = `${name}@${name}`;
@@ -198,11 +202,15 @@ async function main() {
198
202
  if (plan.reused.length) console.log(`已复用同版本的 ${plan.reused.length} 个未修改内容。`);
199
203
  for (const target of plan.targets) {
200
204
  console.log(`已为 ${target.agents.join(', ')} 安装技能:${target.skillsDirectory}`);
201
- 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
+ if (target.nativeCommands) console.log(`命令:/i3-spec、/i3-plan、/i3-build、/i3-ship(共 ${target.catalog.commands.length} 个工作流):${target.commandsDirectory}`);
206
+ else console.log(`命令和角色资料已提供;是否自动发现取决于宿主:${target.commandsDirectory}、${target.personasDirectory}`);
207
+ console.log(`评测资料:${target.evalsDirectory}`);
208
+ console.log(`中文评测:node "${path.join(target.scriptsDirectory, 'run-evals.cjs')}" --locale zh-CN --min-rank1 95`);
209
+ console.log(`Hook:${target.hooksDirectory}(尚未启用)`);
202
210
  }
211
+ for (const directory of plan.preserved || []) console.log(`旧目录中保留了用户文件:${directory}`);
203
212
  console.log('请重新加载 IDE 窗口或新建宿主会话以发现安装内容。');
204
213
  if (plan.agents.some(id => !['codebuddy', 'workbuddy', 'claude-code'].includes(id))) console.log('这些工具通过技能 ID 调用;生命周期短命令支持取决于宿主。');
205
- console.log('原生技能安装完成;插件 Hook 需按宿主方式注册和启用。');
206
- if (plan.agents.some(id => ['codebuddy', 'codex', 'claude-code'].includes(id))) console.log('如需 CodeBuddy、Codex 或 Claude Code 完整插件,请使用 --prepare 和对应宿主参数。');
214
+ console.log('安装完成。评测说明见 evals/i3-composer-agent/README.md;Hook 需按宿主事件协议启用。');
207
215
  }
208
216
  main().catch(error => { console.error(`${name}: ${error.message}`); process.exitCode = 1; });
package/bin/install.cjs CHANGED
@@ -6,11 +6,13 @@ const os = require('node:os');
6
6
  const crypto = require('node:crypto');
7
7
  const registry = require('./agent-targets.json');
8
8
  const { cleanupBackups } = require('./cleanup-backups.cjs');
9
- const { nativeContent } = require('./native-content.cjs');
9
+ const { nativeAssets, renderAsset } = require('./native-layout.cjs');
10
+ const { migrationAssets, pruneRetiredDirectories } = require('./native-migration.cjs');
10
11
  const { fingerprint } = require('./asset-fingerprint.cjs');
11
12
  const root = path.resolve(__dirname, '..');
12
13
  const name = 'i3-composer-agent';
13
14
  const version = require('../package.json').version;
15
+ const pluginFiles = require('../package.json').i3ComposerAgent.pluginFiles;
14
16
  const aliases = { claude: 'claude-code', gemini: 'gemini-cli', copilot: 'github-copilot' };
15
17
  function exists(file) {
16
18
  try { fs.lstatSync(file); return true; }
@@ -46,9 +48,12 @@ function checkDestination(file) {
46
48
  throw new Error('安装目标必须位于包目录之外。');
47
49
  }
48
50
  }
51
+ function assetPath(base, file) { return path.relative(base, file).split(path.sep).join('/'); }
49
52
  function manifestContent(plan, asset) {
50
- 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
+ return JSON.stringify({ name, version, layoutVersion: 2, entryPrefix: 'i3-', scope: plan.scope, agents: asset.target.agents,
54
+ capabilities: { nativeCommands: !!asset.target.nativeCommands, nativeAgents: !!asset.target.nativeCommands, hooks: 'manual' },
55
+ catalog: asset.target.catalog, resources: asset.target.resources,
56
+ assets: plan.assets.filter(item => item.base === asset.base && !['legacy', 'result'].includes(item.kind)).map(item => assetPath(asset.base, item.destination)) }, null, 2) + '\n';
52
57
  }
53
58
  function planInstallation({ agents, scope, projectDir = process.cwd(), home = os.homedir(), env = process.env }) {
54
59
  agents = selectAgents(agents);
@@ -59,30 +64,26 @@ function planInstallation({ agents, scope, projectDir = process.cwd(), home = os
59
64
  const agent = registry.agents.find(item => item.id === id);
60
65
  const skillsDirectory = destination(scope === 'project' ? path.resolve(projectDir, agent.projectSkillsDir) : globalSkillsDir(agent, home, env));
61
66
  const base = path.dirname(skillsDirectory);
62
- if (!targets.has(skillsDirectory)) targets.set(skillsDirectory, { agents: [], skillsDirectory, referencesDirectory: path.join(base, 'references'), base });
67
+ if (!targets.has(skillsDirectory)) targets.set(skillsDirectory, { agents: [], skillsDirectory, referencesDirectory: path.join(base, 'references', name),
68
+ commandsDirectory: path.join(base, 'commands'), personasDirectory: path.join(base, 'agents'),
69
+ evalsDirectory: path.join(base, 'evals', name), scriptsDirectory: path.join(base, 'scripts', name),
70
+ hooksDirectory: path.join(base, 'hooks', name), docsDirectory: path.join(base, 'docs', name), base });
63
71
  const target = targets.get(skillsDirectory);
64
72
  target.agents.push(id);
65
73
  if (['codebuddy', 'workbuddy', 'claude-code'].includes(id)) {
66
- target.commandsDirectory = path.join(base, 'commands');
67
- target.personasDirectory = path.join(base, 'agents');
74
+ target.nativeCommands = true;
68
75
  }
69
76
  }
70
77
  const assets = [];
71
- function addDirectory(sourceDirectory, destinationDirectory, kind) {
72
- if (exists(destinationDirectory) && fs.lstatSync(destinationDirectory).isSymbolicLink()) {
73
- throw new Error(`安装目录是符号链接(symlink): ${destinationDirectory}. 请选择其真实目录。`);
74
- }
75
- for (const entry of fs.readdirSync(path.join(root, sourceDirectory)).sort()) {
76
- if (entry === '.DS_Store') continue;
77
- assets.push({ source: path.join(root, sourceDirectory, entry), destination: destination(path.join(destinationDirectory, entry)), kind });
78
- }
79
- }
80
78
  for (const target of targets.values()) {
81
79
  const manifestFile = path.join(target.base, `.${name}.json`);
82
80
  const previous = exists(manifestFile) && !fs.lstatSync(manifestFile).isSymbolicLink()
83
81
  ? JSON.parse(fs.readFileSync(manifestFile, 'utf8')) : undefined;
84
82
  const owned = previous?.name === name && Array.isArray(previous.assets);
85
- const sameVersion = owned && previous.version === version && previous.scope === scope && previous.entryPrefix === 'i3-';
83
+ // Earlier Windows installs persisted backslashes. Normalize before lookup
84
+ // and containment checks so owned upgrades work across both manifest forms.
85
+ if (owned) previous.assets = previous.assets.map(relative => typeof relative === 'string' ? relative.replaceAll('\\', '/') : relative);
86
+ const sameVersion = owned && previous.layoutVersion === 2 && previous.version === version && previous.scope === scope && previous.entryPrefix === 'i3-';
86
87
  if (owned && previous.scope === scope && Array.isArray(previous.agents)) {
87
88
  const applicable = previous.agents.filter(id => {
88
89
  const agent = registry.agents.find(item => item.id === id);
@@ -92,49 +93,38 @@ function planInstallation({ agents, scope, projectDir = process.cwd(), home = os
92
93
  });
93
94
  target.agents = [...new Set([...applicable, ...target.agents])];
94
95
  }
96
+ target.nativeCommands = target.agents.some(id => ['codebuddy', 'workbuddy', 'claude-code'].includes(id));
95
97
  const start = assets.length;
96
- addDirectory('skills', target.skillsDirectory, 'skill');
97
- addDirectory('references', target.referencesDirectory, 'reference');
98
- if (target.commandsDirectory) {
99
- addDirectory('.claude/commands', target.commandsDirectory, 'command');
100
- addDirectory('agents', target.personasDirectory, 'persona');
101
- const docs = path.join(target.base, 'docs');
102
- if (exists(docs) && fs.lstatSync(docs).isSymbolicLink()) throw new Error(`安装目录是符号链接(symlink): ${docs}.`);
103
- assets.push({ source: path.join(root, 'docs/agents.md'), destination: destination(path.join(target.base, 'docs/agents.md')), kind: 'documentation' });
104
- }
105
- const licenseDirectory = path.join(target.referencesDirectory, name);
106
- if (exists(licenseDirectory) && fs.lstatSync(licenseDirectory).isSymbolicLink()) throw new Error(`安装目录是符号链接(symlink): ${licenseDirectory}.`);
107
- assets.push({ source: path.join(root, 'LICENSE'), destination: destination(path.join(licenseDirectory, 'LICENSE')), kind: 'attribution' });
108
- if (owned) {
109
- const legacy = fs.readdirSync(path.join(root, 'skills')).filter(id => id.startsWith('i3-')).map(id => `skills/${id.slice(3)}`);
110
- 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
- legacy.push(`${name}-LICENSE`, `${name}-NOTICE.md`);
112
- const current = new Set(assets.slice(start).map(asset => path.relative(target.base, asset.destination)));
113
- for (const relative of previous.assets) {
114
- // Only installer-owned top-level assets can be retired. Ignore malformed
115
- // or escaping manifest paths rather than extending their deletion scope.
116
- if (typeof relative !== 'string' || !/^(skills|commands|agents|references)\/[^/\\]+$/.test(relative)
117
- || relative.includes('\0') || ['.', '..'].includes(relative.split('/')[1])) continue;
118
- if (!current.has(relative)) legacy.push(relative);
119
- }
120
- for (const relative of new Set(legacy)) {
121
- const file = destination(path.join(target.base, relative));
122
- if (previous.assets.includes(relative) && exists(file)) assets.push({ destination: file, kind: 'legacy' });
98
+ const additions = nativeAssets(root, target, pluginFiles, version);
99
+ const current = new Set(additions.map(asset => asset.relative));
100
+ assets.push(...additions, ...migrationAssets(target, owned ? previous : undefined, current));
101
+ for (const asset of additions) {
102
+ // A new grouped resource must not take ownership of an unrelated file,
103
+ // even when --force confirms replacement of a previous installation.
104
+ const previousOwned = owned && previous.assets.some(relative => relative === asset.relative
105
+ || (previous.layoutVersion !== 2 && asset.relative.startsWith(relative + '/')));
106
+ asset.protectedResource = /^(references|evals|hooks|scripts|docs)\//.test(asset.relative);
107
+ if (exists(asset.destination) && !previousOwned && asset.protectedResource) {
108
+ throw new Error(`未归属本包的同名资源,不能覆盖,请先处理:${asset.destination}`);
123
109
  }
124
110
  }
125
111
  assets.push({ target, destination: destination(manifestFile), kind: 'manifest', mergeable: sameVersion });
126
112
  for (const asset of assets.slice(start)) {
113
+ if (destination(asset.destination) !== asset.destination) throw new Error(`安装目录包含符号链接(symlink):${asset.destination}`);
127
114
  asset.base = target.base;
128
115
  asset.buddyCommand = asset.kind === 'command' && target.agents.some(id => ['codebuddy', 'workbuddy'].includes(id));
129
116
  asset.scope = scope;
130
117
  asset.personasDirectory = target.personasDirectory;
131
- if (sameVersion && previous.assets.includes(path.relative(target.base, asset.destination)) && asset.source && exists(asset.destination)) {
118
+ if (sameVersion && previous.assets.includes(assetPath(target.base, asset.destination)) && asset.source && exists(asset.destination)) {
132
119
  asset.expectedFingerprint = fingerprint(asset.source, asset);
133
120
  asset.reusable = fingerprint(asset.destination) === asset.expectedFingerprint;
134
121
  }
135
122
  }
136
123
  }
137
124
  for (const asset of assets) { checkDestination(asset.destination); asset.existing = exists(asset.destination); }
125
+ for (const asset of assets.filter(item => item.existing)) {
126
+ if (!fs.lstatSync(asset.destination).isFile()) throw new Error(`目标文件被目录或符号链接占用(symlink):${asset.destination}`);
127
+ }
138
128
  const plan = { mode: 'install', scope, agents, targets: [...targets.values()], assets };
139
129
  for (const asset of assets.filter(item => item.kind === 'manifest' && item.mergeable)) {
140
130
  asset.observedFingerprint = fingerprint(asset.destination);
@@ -158,13 +148,6 @@ function executeInstallation(plan, { force = false } = {}) {
158
148
  fs.mkdirSync(directory);
159
149
  createdDirectories.push(directory);
160
150
  }
161
- function portable(file, asset, markdown = false) {
162
- if (fs.statSync(file).isDirectory()) {
163
- for (const entry of fs.readdirSync(file)) portable(path.join(file, entry), asset);
164
- } else if (markdown || file.endsWith('.md')) {
165
- fs.writeFileSync(file, nativeContent(fs.readFileSync(file, 'utf8'), asset));
166
- }
167
- }
168
151
  try {
169
152
  // Stage every target before replacing the first installed asset.
170
153
  for (const [index, asset] of plan.assets.entries()) {
@@ -178,16 +161,22 @@ function executeInstallation(plan, { force = false } = {}) {
178
161
  if (asset.kind === 'manifest') {
179
162
  fs.writeFileSync(asset.staged, manifestContent(plan, asset));
180
163
  } else {
181
- fs.cpSync(asset.source, asset.staged, { recursive: true, filter: file => {
182
- if (fs.lstatSync(file).isSymbolicLink()) throw new Error(`包内容中包含符号链接(symlink): ${file}`);
183
- return path.basename(file) !== '.DS_Store';
184
- } });
185
- portable(asset.staged, asset, asset.source.endsWith('.md'));
164
+ if (!fs.lstatSync(asset.source).isFile()) throw new Error(`包内容不是普通文件:${asset.source}`);
165
+ const bytes = fs.readFileSync(asset.source);
166
+ fs.writeFileSync(asset.staged, asset.kind === 'result' ? bytes : renderAsset(bytes, asset));
167
+ fs.chmodSync(asset.staged, fs.statSync(asset.source).mode & 0o777);
186
168
  }
187
169
  }
188
170
  for (const asset of plan.assets) {
189
171
  const context = contexts.get(asset.base);
172
+ if (destination(asset.destination) !== asset.destination) throw new Error(`安装期间目录变成符号链接(symlink):${asset.destination}`);
190
173
  mkdir(path.dirname(asset.destination));
174
+ if (exists(asset.destination) && !fs.lstatSync(asset.destination).isFile()) throw new Error(`安装期间目标不再是普通文件:${asset.destination}`);
175
+ if (!asset.existing && exists(asset.destination) && (asset.protectedResource || asset.kind === 'result')) {
176
+ throw new Error(`安装期间出现未归属文件:${asset.destination}`);
177
+ }
178
+ if (asset.kind === 'result' && fingerprint(asset.source) !== asset.expectedFingerprint) throw new Error(`安装期间评测结果已改变:${asset.source}`);
179
+ if (asset.kind === 'result' && asset.existing && fingerprint(asset.destination) !== asset.expectedFingerprint) throw new Error(`安装期间目标评测结果已改变:${asset.destination}`);
191
180
  if (asset.reusable) {
192
181
  const unchanged = fingerprint(asset.destination) === asset.expectedFingerprint
193
182
  && fingerprint(asset.staged) === asset.expectedFingerprint;
@@ -210,12 +199,14 @@ function executeInstallation(plan, { force = false } = {}) {
210
199
  change.written = true;
211
200
  }
212
201
  }
202
+ pruneRetiredDirectories(plan.assets);
213
203
  completed = true;
204
+ plan.preserved = plan.targets.map(target => path.join(target.base, name)).filter(file => exists(file));
214
205
  return [];
215
206
  } catch (error) {
216
207
  for (const change of committed.reverse()) {
217
208
  if (change.written) fs.rmSync(change.destination, { recursive: true, force: true });
218
- if (change.backup) fs.renameSync(change.backup, change.destination);
209
+ if (change.backup) { mkdir(path.dirname(change.destination)); fs.renameSync(change.backup, change.destination); }
219
210
  }
220
211
  rolledBack = true;
221
212
  throw error;