@fitlab-ai/agent-infra 0.9.1 → 0.9.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 (283) hide show
  1. package/README.md +16 -4
  2. package/README.zh-CN.md +16 -4
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +15 -0
  6. package/dist/lib/agent-client.js +163 -0
  7. package/dist/lib/agent-clients/adapter.js +294 -0
  8. package/dist/lib/agent-clients/adapters/claude-code.js +62 -0
  9. package/dist/lib/agent-clients/adapters/codex-sandbox.js +210 -0
  10. package/dist/lib/agent-clients/adapters/codex.js +54 -0
  11. package/dist/lib/agent-clients/adapters/gemini-cli.js +64 -0
  12. package/dist/lib/agent-clients/adapters/index.js +20 -0
  13. package/dist/lib/agent-clients/adapters/opencode.js +64 -0
  14. package/dist/lib/agent-clients/config.js +177 -0
  15. package/dist/lib/agent-clients/custom-tuis.js +99 -0
  16. package/dist/lib/agent-clients/invocation.js +9 -0
  17. package/dist/lib/agent-clients/next-steps.js +38 -0
  18. package/dist/lib/agent-clients/project-assets.js +45 -0
  19. package/dist/lib/agent-clients/reconcile.js +235 -0
  20. package/dist/lib/agent-clients/registry.js +72 -0
  21. package/dist/lib/agent-clients/schema.js +57 -0
  22. package/dist/lib/agent-clients/types.js +29 -0
  23. package/dist/lib/builtin-tuis.js +9 -14
  24. package/dist/lib/defaults.json +85 -73
  25. package/dist/lib/git/review-snapshot.js +25 -10
  26. package/dist/lib/git/reviewed-commit-equivalence.js +110 -0
  27. package/dist/lib/init.js +47 -91
  28. package/dist/lib/internal/agent-client.js +117 -0
  29. package/dist/lib/internal/git-workflow.js +1 -1
  30. package/dist/lib/internal/release-workflow.js +71 -16
  31. package/dist/lib/internal/task-orchestration.js +151 -0
  32. package/dist/lib/internal/task-review.js +65 -0
  33. package/dist/lib/merge.js +2 -1
  34. package/dist/lib/platform/adapters.js +38 -1
  35. package/dist/lib/platform/change-request-git-evidence.js +113 -0
  36. package/dist/lib/platform/merged-pr-equivalence.js +41 -11
  37. package/dist/lib/platform/pr-checks.js +38 -10
  38. package/dist/lib/platform/pull-requests.js +110 -6
  39. package/dist/lib/platform/releases.js +1 -1
  40. package/dist/lib/platform/verification-required.js +8 -10
  41. package/dist/lib/prompt.js +15 -3
  42. package/dist/lib/release/channels.js +4 -1
  43. package/dist/lib/release/workflow.js +4 -2
  44. package/dist/lib/remove-dir.js +25 -0
  45. package/dist/lib/run/tui.js +8 -2
  46. package/dist/lib/sandbox/agent-client-reconciler.js +234 -0
  47. package/dist/lib/sandbox/clipboard/linux.js +2 -1
  48. package/dist/lib/sandbox/commands/create.js +76 -180
  49. package/dist/lib/sandbox/commands/enter.js +17 -1
  50. package/dist/lib/sandbox/commands/prune.js +2 -2
  51. package/dist/lib/sandbox/commands/rm.js +3 -2
  52. package/dist/lib/sandbox/config.js +21 -4
  53. package/dist/lib/sandbox/constants.js +3 -0
  54. package/dist/lib/sandbox/dockerfile.js +2 -1
  55. package/dist/lib/sandbox/managed-fs.js +10 -5
  56. package/dist/lib/sandbox/recovery.js +52 -36
  57. package/dist/lib/sandbox/shell.js +1 -1
  58. package/dist/lib/sandbox/tool-types.js +8 -0
  59. package/dist/lib/sandbox/tools.js +2 -126
  60. package/dist/lib/task/artifact-lifecycle.js +1 -49
  61. package/dist/lib/task/commands/decisions.js +50 -20
  62. package/dist/lib/task/delegation-receipts.js +107 -0
  63. package/dist/lib/task/events.js +100 -1
  64. package/dist/lib/task/orchestration.js +420 -0
  65. package/dist/lib/task/review-artifacts.js +143 -59
  66. package/dist/lib/task/review-finalization.js +166 -0
  67. package/dist/lib/task/review-fingerprint.js +4 -0
  68. package/dist/lib/task/verification-checks.js +2 -1
  69. package/dist/lib/task/verification-engine.js +156 -28
  70. package/dist/lib/task/verification-review.js +1 -1
  71. package/dist/lib/task/verification.js +3 -1
  72. package/dist/lib/task/workspace-snapshot.js +34 -0
  73. package/dist/lib/update.js +42 -161
  74. package/lib/agent-client.ts +214 -0
  75. package/lib/agent-clients/adapter.ts +461 -0
  76. package/lib/agent-clients/adapters/claude-code.ts +70 -0
  77. package/lib/agent-clients/adapters/codex-sandbox.ts +245 -0
  78. package/lib/agent-clients/adapters/codex.ts +58 -0
  79. package/lib/agent-clients/adapters/gemini-cli.ts +65 -0
  80. package/lib/agent-clients/adapters/index.ts +27 -0
  81. package/lib/agent-clients/adapters/opencode.ts +66 -0
  82. package/lib/agent-clients/config.ts +278 -0
  83. package/lib/agent-clients/custom-tuis.ts +149 -0
  84. package/lib/agent-clients/invocation.ts +19 -0
  85. package/lib/agent-clients/next-steps.ts +72 -0
  86. package/lib/agent-clients/project-assets.ts +96 -0
  87. package/lib/agent-clients/reconcile.ts +324 -0
  88. package/lib/agent-clients/registry.ts +144 -0
  89. package/lib/agent-clients/schema.ts +62 -0
  90. package/lib/agent-clients/types.ts +78 -0
  91. package/lib/benchmark/schemas/v1.0.0/case-manifest.schema.json +120 -0
  92. package/lib/benchmark/schemas/v1.0.0/common.schema.json +109 -0
  93. package/lib/benchmark/schemas/v1.0.0/grader-result.schema.json +160 -0
  94. package/lib/benchmark/schemas/v1.0.0/run-manifest.schema.json +122 -0
  95. package/lib/benchmark/schemas/v1.0.0/subject.schema.json +227 -0
  96. package/lib/builtin-tuis.ts +16 -15
  97. package/lib/defaults.json +24 -12
  98. package/lib/git/review-snapshot.ts +39 -9
  99. package/lib/git/reviewed-commit-equivalence.ts +135 -0
  100. package/lib/init.ts +53 -121
  101. package/lib/internal/agent-client.ts +131 -0
  102. package/lib/internal/git-workflow.ts +1 -1
  103. package/lib/internal/release-workflow.ts +68 -16
  104. package/lib/internal/task-orchestration.ts +132 -0
  105. package/lib/internal/task-review.ts +65 -0
  106. package/lib/merge.ts +2 -1
  107. package/lib/platform/adapters.ts +142 -3
  108. package/lib/platform/change-request-git-evidence.ts +159 -0
  109. package/lib/platform/merged-pr-equivalence.ts +51 -14
  110. package/lib/platform/pr-checks.ts +50 -22
  111. package/lib/platform/pull-requests.ts +134 -25
  112. package/lib/platform/releases.ts +1 -1
  113. package/lib/platform/verification-required.ts +12 -10
  114. package/lib/prompt.ts +16 -3
  115. package/lib/release/channels.ts +4 -1
  116. package/lib/release/workflow.ts +4 -3
  117. package/lib/remove-dir.ts +28 -0
  118. package/lib/run/tui.ts +9 -1
  119. package/lib/sandbox/agent-client-reconciler.ts +378 -0
  120. package/lib/sandbox/clipboard/linux.ts +2 -1
  121. package/lib/sandbox/commands/create.ts +111 -192
  122. package/lib/sandbox/commands/enter.ts +23 -1
  123. package/lib/sandbox/commands/prune.ts +2 -2
  124. package/lib/sandbox/commands/rm.ts +3 -2
  125. package/lib/sandbox/config.ts +31 -4
  126. package/lib/sandbox/constants.ts +6 -0
  127. package/lib/sandbox/dockerfile.ts +2 -1
  128. package/lib/sandbox/managed-fs.ts +18 -5
  129. package/lib/sandbox/recovery.ts +98 -67
  130. package/lib/sandbox/shell.ts +1 -1
  131. package/lib/sandbox/tool-types.ts +143 -0
  132. package/lib/sandbox/tools.ts +5 -157
  133. package/lib/task/artifact-lifecycle.ts +1 -38
  134. package/lib/task/commands/decisions.ts +53 -20
  135. package/lib/task/delegation-receipts.ts +170 -0
  136. package/lib/task/events.ts +110 -2
  137. package/lib/task/orchestration.ts +543 -0
  138. package/lib/task/review-artifacts.ts +201 -69
  139. package/lib/task/review-finalization.ts +275 -0
  140. package/lib/task/review-fingerprint.ts +5 -0
  141. package/lib/task/verification-checks.ts +2 -1
  142. package/lib/task/verification-engine.ts +177 -28
  143. package/lib/task/verification-review.ts +1 -0
  144. package/lib/task/verification.ts +5 -2
  145. package/lib/task/workspace-snapshot.ts +36 -0
  146. package/lib/update.ts +48 -184
  147. package/package.json +2 -2
  148. package/templates/.agents/README.en.md +21 -20
  149. package/templates/.agents/README.zh-CN.md +21 -20
  150. package/templates/.agents/hooks/lifecycle-delegation.js +64 -0
  151. package/templates/.agents/rules/README.en.md +2 -0
  152. package/templates/.agents/rules/README.zh-CN.md +2 -0
  153. package/templates/.agents/rules/commit-and-pr.en.md +1 -0
  154. package/templates/.agents/rules/commit-and-pr.zh-CN.md +1 -0
  155. package/templates/.agents/rules/human-decision-context.en.md +14 -12
  156. package/templates/.agents/rules/human-decision-context.zh-CN.md +14 -12
  157. package/templates/.agents/rules/lifecycle-orchestration.en.md +21 -0
  158. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +21 -0
  159. package/templates/.agents/rules/next-step-output.en.md +16 -0
  160. package/templates/.agents/rules/next-step-output.zh-CN.md +16 -0
  161. package/templates/.agents/rules/no-mid-flow-questions.en.md +2 -0
  162. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +2 -0
  163. package/templates/.agents/rules/release-commands.github.en.md +11 -5
  164. package/templates/.agents/rules/release-commands.github.zh-CN.md +11 -5
  165. package/templates/.agents/rules/review-handshake.en.md +4 -4
  166. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  167. package/templates/.agents/rules/review-method.en.md +113 -0
  168. package/templates/.agents/rules/review-method.zh-CN.md +98 -0
  169. package/templates/.agents/skills/analyze-task/SKILL.en.md +5 -5
  170. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +5 -5
  171. package/templates/.agents/skills/block-task/SKILL.en.md +4 -4
  172. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +4 -4
  173. package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -4
  174. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -5
  175. package/templates/.agents/skills/check-task/SKILL.en.md +16 -16
  176. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -16
  177. package/templates/.agents/skills/close-codescan/SKILL.en.md +4 -4
  178. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +4 -4
  179. package/templates/.agents/skills/close-dependabot/SKILL.en.md +4 -4
  180. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +4 -4
  181. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  182. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  183. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +14 -16
  184. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +14 -16
  185. package/templates/.agents/skills/code-task/reference/output-template.en.md +3 -3
  186. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +3 -3
  187. package/templates/.agents/skills/commit/SKILL.en.md +11 -1
  188. package/templates/.agents/skills/commit/SKILL.zh-CN.md +11 -1
  189. package/templates/.agents/skills/commit/reference/task-status-update.en.md +24 -28
  190. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +24 -28
  191. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  192. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  193. package/templates/.agents/skills/create-pr/SKILL.en.md +10 -10
  194. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +10 -10
  195. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +3 -3
  196. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +3 -3
  197. package/templates/.agents/skills/create-task/SKILL.en.md +11 -11
  198. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +11 -11
  199. package/templates/.agents/skills/import-codescan/SKILL.en.md +5 -5
  200. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +5 -5
  201. package/templates/.agents/skills/import-dependabot/SKILL.en.md +5 -5
  202. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +5 -5
  203. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -5
  204. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -5
  205. package/templates/.agents/skills/init-labels/SKILL.en.md +4 -4
  206. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +4 -4
  207. package/templates/.agents/skills/init-milestones/SKILL.en.md +4 -4
  208. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +4 -4
  209. package/templates/.agents/skills/plan-task/SKILL.en.md +5 -5
  210. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +5 -5
  211. package/templates/.agents/skills/refine-title/SKILL.en.md +1 -1
  212. package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +1 -1
  213. package/templates/.agents/skills/release/SKILL.en.md +1 -1
  214. package/templates/.agents/skills/review-analysis/SKILL.en.md +13 -2
  215. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +14 -3
  216. package/templates/.agents/skills/review-analysis/config/verify.en.json +6 -0
  217. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +6 -0
  218. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +13 -15
  219. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +13 -15
  220. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +51 -5
  221. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +52 -6
  222. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +25 -22
  223. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +25 -22
  224. package/templates/.agents/skills/review-code/SKILL.en.md +28 -8
  225. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +28 -8
  226. package/templates/.agents/skills/review-code/config/verify.en.json +7 -0
  227. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +7 -0
  228. package/templates/.agents/skills/review-code/reference/concurrency-risks.en.md +25 -0
  229. package/templates/.agents/skills/review-code/reference/concurrency-risks.zh-CN.md +25 -0
  230. package/templates/.agents/skills/review-code/reference/cross-platform-risks.en.md +27 -0
  231. package/templates/.agents/skills/review-code/reference/cross-platform-risks.zh-CN.md +27 -0
  232. package/templates/.agents/skills/review-code/reference/documentation-antipatterns.en.md +13 -0
  233. package/templates/.agents/skills/review-code/reference/documentation-antipatterns.zh-CN.md +13 -0
  234. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +25 -0
  235. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +25 -0
  236. package/templates/.agents/skills/review-code/reference/output-templates.en.md +22 -24
  237. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +22 -24
  238. package/templates/.agents/skills/review-code/reference/report-template.en.md +54 -7
  239. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +54 -7
  240. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +29 -24
  241. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +29 -24
  242. package/templates/.agents/skills/review-code/reference/security-risks.en.md +25 -0
  243. package/templates/.agents/skills/review-code/reference/security-risks.zh-CN.md +25 -0
  244. package/templates/.agents/skills/review-plan/SKILL.en.md +13 -2
  245. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +14 -3
  246. package/templates/.agents/skills/review-plan/config/verify.en.json +6 -0
  247. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +6 -0
  248. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +13 -15
  249. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +13 -15
  250. package/templates/.agents/skills/review-plan/reference/report-template.en.md +61 -5
  251. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +61 -5
  252. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +21 -24
  253. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +21 -24
  254. package/templates/.agents/skills/run-task/SKILL.en.md +22 -0
  255. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +22 -0
  256. package/templates/.agents/skills/run-task/config/verify.en.json +9 -0
  257. package/templates/.agents/skills/run-task/config/verify.zh-CN.json +9 -0
  258. package/templates/.agents/skills/test/SKILL.en.md +4 -4
  259. package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -4
  260. package/templates/.agents/skills/test-integration/SKILL.en.md +4 -4
  261. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +4 -4
  262. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -5
  263. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -5
  264. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +647 -203
  265. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +4 -4
  266. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +4 -4
  267. package/templates/.agents/skills/watch-pr/SKILL.en.md +8 -8
  268. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +8 -8
  269. package/templates/.claude/agents/agent-infra-lifecycle-executor.en.md +6 -0
  270. package/templates/.claude/agents/agent-infra-lifecycle-executor.zh-CN.md +6 -0
  271. package/templates/.claude/agents/agent-infra-lifecycle-reviewer.en.md +6 -0
  272. package/templates/.claude/agents/agent-infra-lifecycle-reviewer.zh-CN.md +6 -0
  273. package/templates/.claude/commands/run-task.en.md +8 -0
  274. package/templates/.claude/commands/run-task.zh-CN.md +8 -0
  275. package/templates/.claude/settings.json +24 -0
  276. package/templates/.codex/agents/agent-infra-lifecycle-executor.en.toml +3 -0
  277. package/templates/.codex/agents/agent-infra-lifecycle-executor.zh-CN.toml +3 -0
  278. package/templates/.codex/agents/agent-infra-lifecycle-reviewer.en.toml +3 -0
  279. package/templates/.codex/agents/agent-infra-lifecycle-reviewer.zh-CN.toml +3 -0
  280. package/templates/.gemini/commands/_project_/run-task.en.toml +8 -0
  281. package/templates/.gemini/commands/_project_/run-task.zh-CN.toml +8 -0
  282. package/templates/.opencode/commands/run-task.en.md +11 -0
  283. package/templates/.opencode/commands/run-task.zh-CN.md +11 -0
@@ -28,17 +28,35 @@ const DEFAULTS = {
28
28
  "platform": {
29
29
  "type": "github"
30
30
  },
31
+ "agentClients": [
32
+ {
33
+ "id": "claude-code",
34
+ "enabled": true,
35
+ "installInSandbox": true
36
+ },
37
+ {
38
+ "id": "codex",
39
+ "enabled": true,
40
+ "installInSandbox": true
41
+ },
42
+ {
43
+ "id": "gemini-cli",
44
+ "enabled": true,
45
+ "installInSandbox": true
46
+ },
47
+ {
48
+ "id": "opencode",
49
+ "enabled": true,
50
+ "installInSandbox": true
51
+ }
52
+ ],
31
53
  "sandbox": {
32
54
  "engine": null,
33
55
  "runtimes": [
34
56
  "node22"
35
57
  ],
36
58
  "tools": [
37
- "agent-infra",
38
- "claude-code",
39
- "codex",
40
- "gemini-cli",
41
- "opencode"
59
+ "agent-infra"
42
60
  ],
43
61
  "refreshIntervalDays": 7,
44
62
  "dockerfile": null,
@@ -65,16 +83,12 @@ const DEFAULTS = {
65
83
  ".agents/templates/",
66
84
  ".agents/workflows/",
67
85
  ".agents/workspace/README.md",
68
- ".claude/commands/",
69
- ".codex/hooks.json",
70
- ".gemini/commands/",
71
86
  ".git-hooks/check-large-files.cjs",
72
87
  ".git-hooks/check-version-format.sh",
73
88
  ".github/scripts/",
74
89
  ".github/workflows/metadata-sync.yml",
75
90
  ".github/workflows/pr-label.yml",
76
- ".github/workflows/status-label.yml",
77
- ".opencode/commands/"
91
+ ".github/workflows/status-label.yml"
78
92
  ],
79
93
  "guardedManaged": [
80
94
  ".github/workflows/metadata-sync.yml",
@@ -93,8 +107,6 @@ const DEFAULTS = {
93
107
  ".agents/skills/test-integration/SKILL.*",
94
108
  ".agents/skills/test/SKILL.*",
95
109
  ".agents/skills/upgrade-dependency/SKILL.*",
96
- ".claude/settings.json",
97
- ".gemini/settings.json",
98
110
  ".git-hooks/pre-commit",
99
111
  ".gitignore"
100
112
  ],
@@ -102,64 +114,283 @@ const DEFAULTS = {
102
114
  }
103
115
  };
104
116
 
105
- const AGENT_INFRA_SANDBOX_TOOL = 'agent-infra';
106
- const LEGACY_DEFAULT_SANDBOX_TOOLS = ['claude-code', 'codex', 'gemini-cli', 'opencode'];
107
- const DEFAULT_SANDBOX_TOOLS = [AGENT_INFRA_SANDBOX_TOOL, ...LEGACY_DEFAULT_SANDBOX_TOOLS];
117
+ const AGENT_CLIENT_MANIFEST = [
118
+ {
119
+ "id": "claude-code",
120
+ "displayName": "Claude Code",
121
+ "invocation": "/${skillName}",
122
+ "ownedPathPrefixes": [
123
+ ".claude/"
124
+ ],
125
+ "managed": [
126
+ ".claude/commands/",
127
+ ".claude/agents/"
128
+ ],
129
+ "merged": [
130
+ ".claude/settings.json"
131
+ ],
132
+ "ejected": []
133
+ },
134
+ {
135
+ "id": "codex",
136
+ "displayName": "Codex",
137
+ "invocation": "$${skillName}",
138
+ "ownedPathPrefixes": [
139
+ ".codex/"
140
+ ],
141
+ "managed": [
142
+ ".codex/hooks.json",
143
+ ".codex/agents/"
144
+ ],
145
+ "merged": [],
146
+ "ejected": []
147
+ },
148
+ {
149
+ "id": "gemini-cli",
150
+ "displayName": "Gemini CLI",
151
+ "invocation": "/${projectName}:${skillName}",
152
+ "ownedPathPrefixes": [
153
+ ".gemini/"
154
+ ],
155
+ "managed": [
156
+ ".gemini/commands/"
157
+ ],
158
+ "merged": [
159
+ ".gemini/settings.json"
160
+ ],
161
+ "ejected": []
162
+ },
163
+ {
164
+ "id": "opencode",
165
+ "displayName": "OpenCode",
166
+ "invocation": "/${skillName}",
167
+ "ownedPathPrefixes": [
168
+ ".opencode/"
169
+ ],
170
+ "managed": [
171
+ ".opencode/commands/"
172
+ ],
173
+ "merged": [],
174
+ "ejected": []
175
+ }
176
+ ];
177
+ const CUSTOM_TUI_CONTRACT = {
178
+ "requiredFields": [
179
+ "name",
180
+ "dir",
181
+ "invoke"
182
+ ],
183
+ "allowedPlaceholders": [
184
+ "skillName",
185
+ "projectName"
186
+ ]
187
+ };
108
188
  // Add a new identifier here only after shipping matching .{platform}. template variants.
109
189
  const KNOWN_PLATFORMS = new Set(['github', 'none']);
110
190
  const KNOWN_LANGUAGES = new Set(['en', 'zh-CN']);
111
191
 
112
- // Single source of truth for built-in TUI ids and owned path prefixes.
113
- // Keep in sync with lib/builtin-tuis.ts (enforced by tests/unit/scripts/sync-templates-consts.test.ts).
114
- const BUILTIN_TUI_IDS = ['claude-code', 'codex', 'gemini-cli', 'opencode'];
115
- const BUILTIN_TUI_OWNED_PATH_PREFIXES = {
116
- 'claude-code': ['.claude/'],
117
- 'codex': ['.codex/'],
118
- 'gemini-cli': ['.gemini/'],
119
- 'opencode': ['.opencode/']
120
- };
121
-
122
- function resolveEnabledTUIs(value) {
123
- // Missing field / null / non-array → full set (backward compat).
124
- if (!Array.isArray(value)) return new Set(BUILTIN_TUI_IDS);
125
- // Empty array is a meaningful user choice: no built-in TUI managed.
126
- const set = new Set();
127
- for (const v of value) {
128
- if (typeof v === 'string' && BUILTIN_TUI_IDS.includes(v)) set.add(v);
192
+ const BUILTIN_TUI_IDS = AGENT_CLIENT_MANIFEST.map((entry) => entry.id);
193
+
194
+ function normalizeAgentClientConfig(cfg) {
195
+ const own = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
196
+ const diagnostics = [];
197
+ const failure = (code, path) => ({
198
+ source: null,
199
+ state: null,
200
+ canonical: null,
201
+ remainingSandboxTools: undefined,
202
+ removeLegacyTuis: false,
203
+ changed: false,
204
+ diagnostics,
205
+ error: `${code} at ${path}`,
206
+ errorCode: code,
207
+ errorPath: path
208
+ });
209
+ const projectTuis = (value) => {
210
+ if (!Array.isArray(value)) {
211
+ return Object.fromEntries(BUILTIN_TUI_IDS.map((id) => [id, true]));
212
+ }
213
+ const enabled = new Set();
214
+ for (const [index, candidate] of value.entries()) {
215
+ if (BUILTIN_TUI_IDS.includes(candidate)) enabled.add(candidate);
216
+ else diagnostics.push({ code: 'LEGACY_VALUE_IGNORED', path: `tuis[${index}]` });
217
+ }
218
+ return Object.fromEntries(BUILTIN_TUI_IDS.map((id) => [id, enabled.has(id)]));
219
+ };
220
+ const projectSandbox = (value) => {
221
+ if (!Array.isArray(value) || value.length === 0) {
222
+ return {
223
+ installed: Object.fromEntries(BUILTIN_TUI_IDS.map((id) => [id, true])),
224
+ clientSignals: new Set(),
225
+ remainingTools: Array.isArray(value) ? [] : undefined
226
+ };
227
+ }
228
+ const installed = new Set();
229
+ const remainingTools = [];
230
+ for (const [index, candidate] of value.entries()) {
231
+ if (BUILTIN_TUI_IDS.includes(candidate)) installed.add(candidate);
232
+ else if (typeof candidate === 'string') remainingTools.push(candidate);
233
+ else diagnostics.push({ code: 'LEGACY_VALUE_IGNORED', path: `sandbox.tools[${index}]` });
234
+ }
235
+ return {
236
+ installed: Object.fromEntries(BUILTIN_TUI_IDS.map((id) => [id, installed.has(id)])),
237
+ clientSignals: installed,
238
+ remainingTools
239
+ };
240
+ };
241
+ const serialize = (state) => BUILTIN_TUI_IDS.map((id) => ({ id, ...state[id] }));
242
+ const hasCanonical = own(cfg, 'agentClients');
243
+ const hasLegacyTuis = own(cfg, 'tuis');
244
+ const sandbox = cfg.sandbox && typeof cfg.sandbox === 'object' && !Array.isArray(cfg.sandbox)
245
+ ? cfg.sandbox
246
+ : undefined;
247
+ const hasSandboxTools = sandbox !== undefined && own(sandbox, 'tools');
248
+ const tuiProjection = projectTuis(cfg.tuis);
249
+ const sandboxProjection = projectSandbox(sandbox?.tools);
250
+
251
+ if (!hasCanonical) {
252
+ const state = Object.fromEntries(BUILTIN_TUI_IDS.map((id) => [id, {
253
+ enabled: tuiProjection[id],
254
+ installInSandbox: sandboxProjection.installed[id]
255
+ }]));
256
+ return {
257
+ source: 'legacy',
258
+ state,
259
+ canonical: serialize(state),
260
+ remainingSandboxTools: sandboxProjection.remainingTools,
261
+ removeLegacyTuis: hasLegacyTuis,
262
+ changed: true,
263
+ diagnostics,
264
+ error: null,
265
+ errorCode: null,
266
+ errorPath: null
267
+ };
129
268
  }
130
- return set;
131
- }
132
269
 
133
- function isPathOwnedByDisabledTUI(rel, enabledSet) {
134
- const normalized = String(rel || '').replace(/\\/g, '/').replace(/^\.\//, '');
135
- for (const tui of BUILTIN_TUI_IDS) {
136
- if (enabledSet.has(tui)) continue;
137
- for (const prefix of BUILTIN_TUI_OWNED_PATH_PREFIXES[tui]) {
138
- const trimmed = prefix.replace(/\/$/, '');
139
- if (normalized === trimmed || normalized.startsWith(prefix)) return true;
270
+ if (!Array.isArray(cfg.agentClients)) return failure('INVALID_AGENT_CLIENTS', 'agentClients');
271
+ const entries = new Map();
272
+ for (const [index, candidate] of cfg.agentClients.entries()) {
273
+ const entryPath = `agentClients[${index}]`;
274
+ if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)) {
275
+ return failure('INVALID_AGENT_CLIENTS', entryPath);
276
+ }
277
+ const keys = Object.keys(candidate);
278
+ if (
279
+ keys.length !== 3
280
+ || !own(candidate, 'id')
281
+ || !own(candidate, 'enabled')
282
+ || !own(candidate, 'installInSandbox')
283
+ ) {
284
+ return failure('INVALID_AGENT_CLIENTS', entryPath);
285
+ }
286
+ if (!BUILTIN_TUI_IDS.includes(candidate.id)) {
287
+ return failure('UNKNOWN_AGENT_CLIENT', `${entryPath}.id`);
140
288
  }
289
+ if (entries.has(candidate.id)) return failure('DUPLICATE_AGENT_CLIENT', `${entryPath}.id`);
290
+ if (typeof candidate.enabled !== 'boolean' || typeof candidate.installInSandbox !== 'boolean') {
291
+ return failure('INVALID_AGENT_CLIENTS', entryPath);
292
+ }
293
+ entries.set(candidate.id, {
294
+ enabled: candidate.enabled,
295
+ installInSandbox: candidate.installInSandbox
296
+ });
141
297
  }
142
- return false;
298
+ if (BUILTIN_TUI_IDS.some((id) => !entries.has(id))) {
299
+ return failure('MISSING_AGENT_CLIENT', 'agentClients');
300
+ }
301
+ const state = Object.fromEntries(BUILTIN_TUI_IDS.map((id) => [id, entries.get(id)]));
302
+ if (hasLegacyTuis && BUILTIN_TUI_IDS.some((id) => state[id].enabled !== tuiProjection[id])) {
303
+ return failure('LEGACY_CONFLICT', 'tuis');
304
+ }
305
+ if ([...sandboxProjection.clientSignals].some((id) => !state[id].installInSandbox)) {
306
+ return failure('LEGACY_CONFLICT', 'sandbox.tools');
307
+ }
308
+ const sameOrder = cfg.agentClients.every((entry, index) => entry.id === BUILTIN_TUI_IDS[index]);
309
+ return {
310
+ source: 'canonical',
311
+ state,
312
+ canonical: serialize(state),
313
+ remainingSandboxTools: hasSandboxTools ? sandboxProjection.remainingTools : undefined,
314
+ removeLegacyTuis: hasLegacyTuis,
315
+ changed: hasLegacyTuis || sandboxProjection.clientSignals.size > 0 || !sameOrder,
316
+ diagnostics,
317
+ error: null,
318
+ errorCode: null,
319
+ errorPath: null
320
+ };
143
321
  }
144
322
 
145
- function isLegacyDefaultSandboxTools(value) {
146
- if (!Array.isArray(value) || value.length !== LEGACY_DEFAULT_SANDBOX_TOOLS.length) {
147
- return false;
323
+ function materializeAgentClientConfig(cfg, normalized) {
324
+ cfg.agentClients = normalized.canonical;
325
+ delete cfg.tuis;
326
+ if (normalized.remainingSandboxTools !== undefined) {
327
+ cfg.sandbox = {
328
+ ...(cfg.sandbox || {}),
329
+ tools: [
330
+ 'agent-infra',
331
+ ...normalized.remainingSandboxTools.filter((tool) => tool !== 'agent-infra')
332
+ ]
333
+ };
148
334
  }
149
- const tools = new Set(value);
150
- return LEGACY_DEFAULT_SANDBOX_TOOLS.every(tool => tools.has(tool));
151
335
  }
152
336
 
153
- function migrateSandboxTools(cfg) {
154
- const tools = cfg.sandbox?.tools;
155
- if (!isLegacyDefaultSandboxTools(tools)) {
156
- return false;
157
- }
158
- cfg.sandbox = {
159
- ...cfg.sandbox,
160
- tools: [...DEFAULT_SANDBOX_TOOLS]
337
+ function assetMatches(entry, target) {
338
+ const normalizedEntry = norm(entry);
339
+ const normalizedTarget = norm(target);
340
+ return normalizedEntry.endsWith('/')
341
+ ? normalizedTarget.startsWith(normalizedEntry)
342
+ : normalizedTarget === normalizedEntry;
343
+ }
344
+
345
+ function adapterAssets(adapters, category) {
346
+ return adapters.flatMap((adapter) => adapter[category]);
347
+ }
348
+
349
+ function planProjectRegistry(current, sharedDefaults, enabledSet) {
350
+ const enabledAdapters = AGENT_CLIENT_MANIFEST.filter((adapter) => enabledSet.has(adapter.id));
351
+ const disabledAdapters = AGENT_CLIENT_MANIFEST.filter((adapter) => !enabledSet.has(adapter.id));
352
+ const allAdapterAssets = new Set(
353
+ AGENT_CLIENT_MANIFEST.flatMap((adapter) => [
354
+ ...adapter.managed,
355
+ ...adapter.merged,
356
+ ...adapter.ejected
357
+ ])
358
+ );
359
+ const enabled = {
360
+ managed: adapterAssets(enabledAdapters, 'managed'),
361
+ merged: adapterAssets(enabledAdapters, 'merged'),
362
+ ejected: adapterAssets(enabledAdapters, 'ejected')
363
+ };
364
+ const unique = (values) => [...new Set(values)];
365
+ const ejected = unique([
366
+ ...current.ejected,
367
+ ...enabled.ejected,
368
+ ...sharedDefaults.ejected
369
+ ]);
370
+ const ejectedSet = new Set(ejected);
371
+ const preserve = (values, enabledValues) => values.filter((entry) =>
372
+ !ejectedSet.has(entry)
373
+ && (!allAdapterAssets.has(entry) || enabledValues.includes(entry))
374
+ );
375
+ const managed = unique([
376
+ ...preserve(current.managed, enabled.managed),
377
+ ...enabled.managed.filter((entry) => !ejectedSet.has(entry)),
378
+ ...sharedDefaults.managed.filter((entry) => !ejectedSet.has(entry))
379
+ ]);
380
+ const managedSet = new Set(managed);
381
+ const merged = unique([
382
+ ...preserve(current.merged, enabled.merged),
383
+ ...enabled.merged.filter((entry) => !ejectedSet.has(entry) && !managedSet.has(entry)),
384
+ ...sharedDefaults.merged.filter((entry) => !ejectedSet.has(entry) && !managedSet.has(entry))
385
+ ]);
386
+
387
+ return {
388
+ registry: { managed, merged, ejected },
389
+ enabledManaged: enabled.managed,
390
+ enabledMerged: enabled.merged,
391
+ enabledEjected: enabled.ejected,
392
+ disabledManaged: adapterAssets(disabledAdapters, 'managed')
161
393
  };
162
- return true;
163
394
  }
164
395
 
165
396
  function norm(p) { return p.replace(/\\/g, '/'); }
@@ -618,9 +849,49 @@ function generateOpenCodeCommand(skill, lang) {
618
849
  }
619
850
 
620
851
  function validateCustomTUIs(projectRoot, customTUIs, report) {
621
- const tools = Array.isArray(customTUIs) ? customTUIs : [];
852
+ if (customTUIs === undefined) return [];
853
+ if (!Array.isArray(customTUIs)) {
854
+ recordCustomTUISkipped(report, {
855
+ index: -1,
856
+ name: '',
857
+ dir: '',
858
+ reason: 'customTUIs must be an array'
859
+ });
860
+ return [];
861
+ }
862
+ const tools = customTUIs;
622
863
  return tools
623
864
  .map((tool, index) => {
865
+ if (
866
+ typeof tool !== 'object'
867
+ || tool === null
868
+ || Array.isArray(tool)
869
+ ) {
870
+ recordCustomTUISkipped(report, {
871
+ index,
872
+ name: '',
873
+ dir: '',
874
+ reason: 'invalid custom TUI'
875
+ });
876
+ return null;
877
+ }
878
+
879
+ for (const field of CUSTOM_TUI_CONTRACT.requiredFields) {
880
+ if (
881
+ typeof tool[field] !== 'string'
882
+ || tool[field].trim() === ''
883
+ || /[\r\n]/.test(tool[field])
884
+ ) {
885
+ recordCustomTUISkipped(report, {
886
+ index,
887
+ name: String(tool.name || ''),
888
+ dir: String(tool.dir || ''),
889
+ reason: `invalid ${field}`
890
+ });
891
+ return null;
892
+ }
893
+ }
894
+
624
895
  if (typeof tool?.dir !== 'string' || tool.dir.trim() === '') {
625
896
  recordCustomTUISkipped(report, {
626
897
  index,
@@ -631,6 +902,16 @@ function validateCustomTUIs(projectRoot, customTUIs, report) {
631
902
  return null;
632
903
  }
633
904
 
905
+ if (tool.dir.includes('\\')) {
906
+ recordCustomTUISkipped(report, {
907
+ index,
908
+ name: tool.name,
909
+ dir: tool.dir,
910
+ reason: 'dir must use POSIX separators'
911
+ });
912
+ return null;
913
+ }
914
+
634
915
  if (!isInsideProject(projectRoot, tool.dir)) {
635
916
  recordCustomTUISkipped(report, {
636
917
  index,
@@ -641,6 +922,27 @@ function validateCustomTUIs(projectRoot, customTUIs, report) {
641
922
  return null;
642
923
  }
643
924
 
925
+ const placeholders = [...tool.invoke.matchAll(/\$\{([^}]+)\}/g)]
926
+ .map((match) => match[1]);
927
+ if (
928
+ !placeholders.includes('skillName')
929
+ || placeholders.some((placeholder) =>
930
+ !CUSTOM_TUI_CONTRACT.allowedPlaceholders.includes(placeholder)
931
+ )
932
+ || tool.invoke
933
+ .replaceAll('${skillName}', '')
934
+ .replaceAll('${projectName}', '')
935
+ .includes('${')
936
+ ) {
937
+ recordCustomTUISkipped(report, {
938
+ index,
939
+ name: tool.name,
940
+ dir: tool.dir,
941
+ reason: 'invalid invoke placeholders'
942
+ });
943
+ return null;
944
+ }
945
+
644
946
  return { ...tool, index, dir: normDir(tool.dir) };
645
947
  })
646
948
  .filter(Boolean);
@@ -764,27 +1066,34 @@ function learnAndGenerateCommands(projectRoot, customSkills, tool, templateSkill
764
1066
  }
765
1067
  }
766
1068
 
767
- function generateCustomCommands(projectRoot, customSkills, project, lang, report, customTUIs, templateSkillNames, enabledTUIs) {
1069
+ function generateCustomCommands(
1070
+ projectRoot,
1071
+ customSkills,
1072
+ project,
1073
+ lang,
1074
+ report,
1075
+ customTUIs,
1076
+ templateSkillNames,
1077
+ enabledTUIs,
1078
+ managedWriter
1079
+ ) {
768
1080
  for (const skill of customSkills) {
769
1081
  if (enabledTUIs.has('claude-code')) {
770
- writeIfChanged(
771
- projectRoot,
1082
+ managedWriter(
772
1083
  `.claude/commands/${skill.dirName}.md`,
773
1084
  generateClaudeCommand(skill, lang),
774
1085
  report.custom.commands
775
1086
  );
776
1087
  }
777
1088
  if (enabledTUIs.has('gemini-cli')) {
778
- writeIfChanged(
779
- projectRoot,
1089
+ managedWriter(
780
1090
  '.gemini/commands/' + project + '/' + skill.dirName + '.toml',
781
1091
  generateGeminiCommand(skill, lang),
782
1092
  report.custom.commands
783
1093
  );
784
1094
  }
785
1095
  if (enabledTUIs.has('opencode')) {
786
- writeIfChanged(
787
- projectRoot,
1096
+ managedWriter(
788
1097
  `.opencode/commands/${skill.dirName}.md`,
789
1098
  generateOpenCodeCommand(skill, lang),
790
1099
  report.custom.commands
@@ -935,7 +1244,8 @@ function syncTemplates(projectRoot, templateRootOverride) {
935
1244
  return { error: 'No .agents/.airc.json in project root.' };
936
1245
  }
937
1246
 
938
- const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
1247
+ const originalConfigText = fs.readFileSync(cfgPath, 'utf8');
1248
+ const cfg = JSON.parse(originalConfigText);
939
1249
  const configPathRel = norm(path.relative(projectRoot, cfgPath));
940
1250
  let templateRoot = templateRootOverride;
941
1251
  if (!templateRoot) {
@@ -951,20 +1261,43 @@ function syncTemplates(projectRoot, templateRootOverride) {
951
1261
  }
952
1262
  }
953
1263
  const version = resolveVersionFromTemplateRoot(templateRoot);
954
- const hadTemplateSource = Object.prototype.hasOwnProperty.call(cfg, 'templateSource');
955
1264
 
956
1265
  const { project, org, language: lang = 'en' } = cfg;
957
1266
  const platformType = cfg.platform?.type || DEFAULTS.platform.type;
958
- const enabledTUIs = resolveEnabledTUIs(cfg.tuis);
1267
+ const enabledResolution = normalizeAgentClientConfig(cfg);
1268
+ if (enabledResolution.error) {
1269
+ return { error: enabledResolution.error };
1270
+ }
1271
+ const enabledTUIs = new Set(
1272
+ BUILTIN_TUI_IDS.filter((id) => enabledResolution.state[id].enabled)
1273
+ );
1274
+ materializeAgentClientConfig(cfg, enabledResolution);
959
1275
  const customTUIsConfig = Array.isArray(cfg.customTUIs) ? cfg.customTUIs : [];
960
1276
  const vars = { project, org };
961
1277
  const templateSkillNames = listTemplateSkillNames(templateRoot);
962
1278
  const protectedCustomSkills = detectCustomSkills(projectRoot, templateSkillNames);
963
1279
 
964
- const managed = [...(cfg.files.managed || [])];
965
- const merged = [...(cfg.files.merged || [])];
966
- const ejected = [...(cfg.files.ejected || [])];
1280
+ cfg.files ||= {};
1281
+ const currentRegistry = {
1282
+ managed: [...(cfg.files.managed || [])],
1283
+ merged: [...(cfg.files.merged || [])],
1284
+ ejected: [...(cfg.files.ejected || [])]
1285
+ };
1286
+ const sharedDefaults = {
1287
+ managed: (DEFAULTS.files.managed || []).filter(
1288
+ (entry) => !isPathOwnedByOtherPlatform(entry, platformType)
1289
+ ),
1290
+ merged: (DEFAULTS.files.merged || []).filter(
1291
+ (entry) => !isPathOwnedByOtherPlatform(entry, platformType)
1292
+ ),
1293
+ ejected: [...(DEFAULTS.files.ejected || [])]
1294
+ };
1295
+ const assetPlan = planProjectRegistry(currentRegistry, sharedDefaults, enabledTUIs);
1296
+ const managed = [...assetPlan.registry.managed];
1297
+ const merged = [...assetPlan.registry.merged];
1298
+ const ejected = [...assetPlan.registry.ejected];
967
1299
  const guardedManaged = new Set((DEFAULTS.files.guardedManaged || []).map(norm));
1300
+ const allClientManaged = adapterAssets(AGENT_CLIENT_MANIFEST, 'managed');
968
1301
  const managedBaselines = cfg.files.managedBaselines && typeof cfg.files.managedBaselines === 'object'
969
1302
  && !Array.isArray(cfg.files.managedBaselines)
970
1303
  ? { ...cfg.files.managedBaselines }
@@ -972,7 +1305,10 @@ function syncTemplates(projectRoot, templateRootOverride) {
972
1305
  let baselinesChanged = false;
973
1306
 
974
1307
  for (const target of Object.keys(managedBaselines)) {
975
- if (!guardedManaged.has(norm(target))) {
1308
+ if (
1309
+ !guardedManaged.has(norm(target))
1310
+ && !allClientManaged.some((entry) => assetMatches(entry, target))
1311
+ ) {
976
1312
  delete managedBaselines[target];
977
1313
  baselinesChanged = true;
978
1314
  }
@@ -982,6 +1318,7 @@ function syncTemplates(projectRoot, templateRootOverride) {
982
1318
  templateVersion: version,
983
1319
  templateRoot: norm(templateRoot),
984
1320
  registryAdded: [],
1321
+ registryRemoved: [],
985
1322
  templateSources: {
986
1323
  configured: 0,
987
1324
  loaded: 0,
@@ -1022,17 +1359,23 @@ function syncTemplates(projectRoot, templateRootOverride) {
1022
1359
  templateSkillNames
1023
1360
  );
1024
1361
 
1025
- const known = new Set([...managed, ...merged, ...ejected]);
1026
- for (const e of (DEFAULTS.files.managed || [])) {
1027
- if (isPathOwnedByOtherPlatform(e, platformType)) continue;
1028
- if (isPathOwnedByDisabledTUI(e, enabledTUIs)) continue;
1029
- if (!known.has(e)) { managed.push(e); known.add(e); report.registryAdded.push({ entry: e, list: 'managed' }); }
1030
- }
1031
- for (const e of (DEFAULTS.files.merged || [])) {
1032
- if (isPathOwnedByOtherPlatform(e, platformType)) continue;
1033
- if (isPathOwnedByDisabledTUI(e, enabledTUIs)) continue;
1034
- if (!known.has(e)) { merged.push(e); known.add(e); report.registryAdded.push({ entry: e, list: 'merged' }); }
1362
+ for (const category of ['managed', 'merged', 'ejected']) {
1363
+ const before = currentRegistry[category];
1364
+ const after = assetPlan.registry[category];
1365
+ for (const entry of after) {
1366
+ if (!before.includes(entry)) report.registryAdded.push({ entry, list: category });
1367
+ }
1368
+ for (const entry of before) {
1369
+ if (!after.includes(entry)) report.registryRemoved.push({ entry, list: category });
1370
+ }
1035
1371
  }
1372
+ report.managed.skippedTUI.push(
1373
+ ...assetPlan.disabledManaged,
1374
+ ...adapterAssets(
1375
+ AGENT_CLIENT_MANIFEST.filter((adapter) => !enabledTUIs.has(adapter.id)),
1376
+ 'merged'
1377
+ )
1378
+ );
1036
1379
 
1037
1380
  const templateSources = Array.isArray(cfg.templates?.sources) ? cfg.templates.sources : [];
1038
1381
  report.templateSources.configured = templateSources.length;
@@ -1056,6 +1399,97 @@ function syncTemplates(projectRoot, templateRootOverride) {
1056
1399
  : renderContent(fs.readFileSync(srcFull, 'utf8'), vars);
1057
1400
  }
1058
1401
 
1402
+ function selectedTargetsForEntry(entry) {
1403
+ let entryRels;
1404
+ if (entry.endsWith('/')) {
1405
+ const builtinDir = path.join(templateRoot, entry);
1406
+ const builtinRels = fs.existsSync(builtinDir)
1407
+ ? walkDir(builtinDir).map((filePath) => norm(path.relative(templateRoot, filePath)))
1408
+ : [];
1409
+ const prefix = norm(entry);
1410
+ const externalRels = allRels.filter((rel) =>
1411
+ rel.startsWith(prefix) && !builtinRels.includes(rel)
1412
+ );
1413
+ entryRels = [...builtinRels, ...externalRels];
1414
+ } else {
1415
+ entryRels = entryVariantRels(entry, allSet, platformType);
1416
+ }
1417
+ return platformSelect(
1418
+ langSelect(entryRels, lang, allSet, project),
1419
+ platformType,
1420
+ project
1421
+ );
1422
+ }
1423
+
1424
+ function writeProtectedManaged(target, content, bucket) {
1425
+ const dstFull = path.join(projectRoot, target);
1426
+ const exists = fs.existsSync(dstFull);
1427
+ const rawBaseline = managedBaselines[target];
1428
+ const baseline = trustedBaseline(rawBaseline);
1429
+ const templateHash = sha256(content);
1430
+ const localHash = exists ? sha256(fs.readFileSync(dstFull)) : null;
1431
+
1432
+ if (rawBaseline !== undefined && baseline === null) {
1433
+ delete managedBaselines[target];
1434
+ baselinesChanged = true;
1435
+ }
1436
+ if (baseline === null && localHash !== null && localHash !== templateHash) {
1437
+ report.managed.conflicts.push({
1438
+ target,
1439
+ reason: 'unknown-origin',
1440
+ baseline: null,
1441
+ local: localHash,
1442
+ template: templateHash
1443
+ });
1444
+ return false;
1445
+ }
1446
+ if (baseline !== null && templateHash === baseline && localHash !== baseline) {
1447
+ report.managed.protected.push({
1448
+ target,
1449
+ reason: localHash === null ? 'user-deleted' : 'user-modified',
1450
+ baseline,
1451
+ local: localHash,
1452
+ template: templateHash
1453
+ });
1454
+ return false;
1455
+ }
1456
+ if (
1457
+ baseline !== null
1458
+ && localHash !== baseline
1459
+ && templateHash !== baseline
1460
+ && localHash !== templateHash
1461
+ ) {
1462
+ report.managed.conflicts.push({
1463
+ target,
1464
+ reason: 'both-modified',
1465
+ baseline,
1466
+ local: localHash,
1467
+ template: templateHash
1468
+ });
1469
+ return false;
1470
+ }
1471
+ if (localHash === templateHash) {
1472
+ bucket.unchanged.push(target);
1473
+ if (managedBaselines[target] !== templateHash) {
1474
+ managedBaselines[target] = templateHash;
1475
+ baselinesChanged = true;
1476
+ }
1477
+ return false;
1478
+ }
1479
+
1480
+ const dir = path.dirname(dstFull);
1481
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1482
+ fs.writeFileSync(dstFull, content);
1483
+ managedBaselines[target] = templateHash;
1484
+ baselinesChanged = true;
1485
+ if (Object.prototype.hasOwnProperty.call(bucket, 'written')) {
1486
+ (exists ? bucket.written : bucket.created).push(target);
1487
+ } else {
1488
+ (exists ? bucket.updated : bucket.generated).push(target);
1489
+ }
1490
+ return true;
1491
+ }
1492
+
1059
1493
  for (const entry of [...managed, ...merged, ...ejected]) {
1060
1494
  if (!isPathOwnedByOtherPlatform(entry, platformType)) continue;
1061
1495
 
@@ -1109,39 +1543,81 @@ function syncTemplates(projectRoot, templateRootOverride) {
1109
1543
  }
1110
1544
  }
1111
1545
 
1112
- // Cleanup files owned by disabled built-in TUIs. Iterates managed + merged
1113
- // only (ejected entries are explicitly user-retained, see ejected loop below).
1114
- //
1115
- // Protection rule: only skip files registered as customTUI command targets
1116
- // (e.g. a customTUI configured with dir=.codex/commands/ when codex is
1117
- // disabled). Built-in TUI custom-skill commands like
1118
- // .gemini/commands/<project>/<dirName>.toml are intentionally NOT protected
1119
- // here so that disabling gemini-cli actually frees the gemini directory —
1120
- // they will be regenerated only for still-enabled TUIs (see
1121
- // generateCustomCommands).
1122
- for (const entry of [...managed, ...merged]) {
1123
- if (!isPathOwnedByDisabledTUI(entry, enabledTUIs)) continue;
1124
-
1125
- if (entry.endsWith('/')) {
1126
- const dir = path.join(projectRoot, entry);
1127
- if (!fs.existsSync(dir)) continue;
1128
-
1129
- for (const filePath of walkDir(dir)) {
1130
- const relProj = norm(path.relative(projectRoot, filePath));
1131
- if (customTUICommandTargets.has(relProj)) continue;
1132
- fs.unlinkSync(filePath);
1133
- report.managed.removed.push(relProj);
1546
+ // Disabled clients are converged conservatively: only exact manifest
1547
+ // managed targets or previously baselined generated targets are candidates.
1548
+ // Merged/ejected and content without trustworthy origin evidence are kept.
1549
+ for (const entry of assetPlan.disabledManaged) {
1550
+ const selectedTargets = selectedTargetsForEntry(entry);
1551
+ const candidates = new Set(
1552
+ Object.keys(managedBaselines).filter((target) => assetMatches(entry, target))
1553
+ );
1554
+ for (const target of selectedTargets.keys()) candidates.add(target);
1555
+
1556
+ for (const target of candidates) {
1557
+ const dstFull = path.join(projectRoot, target);
1558
+ const baseline = trustedBaseline(managedBaselines[target]);
1559
+ const selectedSource = selectedTargets.get(target);
1560
+ const sourceRoot = selectedSource ? (sourceMap.get(selectedSource) || templateRoot) : null;
1561
+ const templateContent = selectedSource && sourceRoot === templateRoot
1562
+ ? (
1563
+ isBinary(path.join(sourceRoot, selectedSource))
1564
+ ? fs.readFileSync(path.join(sourceRoot, selectedSource))
1565
+ : renderContent(fs.readFileSync(path.join(sourceRoot, selectedSource), 'utf8'), vars)
1566
+ )
1567
+ : null;
1568
+ const localHash = fs.existsSync(dstFull) ? sha256(fs.readFileSync(dstFull)) : null;
1569
+ const templateHash = templateContent === null ? null : sha256(templateContent);
1570
+ const protectedByEjection = ejected.some((pattern) =>
1571
+ assetMatches(pattern, target) || globMatch(pattern, target)
1572
+ );
1573
+ const protectedByCustom = customTUICommandTargets.has(target);
1574
+
1575
+ if (protectedByEjection || protectedByCustom || (sourceRoot && sourceRoot !== templateRoot)) {
1576
+ report.managed.protected.push({
1577
+ target,
1578
+ reason: protectedByEjection
1579
+ ? 'ejected'
1580
+ : protectedByCustom
1581
+ ? 'custom'
1582
+ : 'external-source',
1583
+ baseline,
1584
+ local: localHash,
1585
+ template: templateHash
1586
+ });
1587
+ continue;
1134
1588
  }
1135
- removeEmptyDirs(dir);
1136
- continue;
1589
+ if (localHash === null) {
1590
+ if (Object.prototype.hasOwnProperty.call(managedBaselines, target)) {
1591
+ delete managedBaselines[target];
1592
+ baselinesChanged = true;
1593
+ }
1594
+ continue;
1595
+ }
1596
+ const legacyManaged = currentRegistry.managed.includes(entry);
1597
+ if (
1598
+ (baseline !== null && localHash === baseline)
1599
+ || (baseline === null && legacyManaged && templateHash !== null && localHash === templateHash)
1600
+ ) {
1601
+ fs.unlinkSync(dstFull);
1602
+ report.managed.removed.push(target);
1603
+ if (Object.prototype.hasOwnProperty.call(managedBaselines, target)) {
1604
+ delete managedBaselines[target];
1605
+ baselinesChanged = true;
1606
+ }
1607
+ removeEmptyDirs(path.dirname(dstFull));
1608
+ continue;
1609
+ }
1610
+ report.managed.protected.push({
1611
+ target,
1612
+ reason: baseline === null ? 'unknown-origin' : 'user-modified',
1613
+ baseline,
1614
+ local: localHash,
1615
+ template: templateHash
1616
+ });
1617
+ }
1618
+ if (entry.endsWith('/')) {
1619
+ removeEmptyDirs(path.join(projectRoot, entry));
1137
1620
  }
1138
-
1139
- const target = path.join(projectRoot, renderPathname(entry, project));
1140
- if (!fs.existsSync(target)) continue;
1141
- const relProj = norm(path.relative(projectRoot, target));
1142
- if (customTUICommandTargets.has(relProj)) continue;
1143
- fs.unlinkSync(target);
1144
- report.managed.removed.push(relProj);
1145
1621
  }
1146
1622
 
1147
1623
  for (const entry of managed) {
@@ -1149,11 +1625,6 @@ function syncTemplates(projectRoot, templateRootOverride) {
1149
1625
  report.managed.skippedPlatform.push(entry);
1150
1626
  continue;
1151
1627
  }
1152
- if (isPathOwnedByDisabledTUI(entry, enabledTUIs)) {
1153
- report.managed.skippedTUI.push(entry);
1154
- continue;
1155
- }
1156
-
1157
1628
  const isDir = entry.endsWith('/');
1158
1629
  let entryRels;
1159
1630
  const expectedTargets = isDir ? new Set() : null;
@@ -1191,65 +1662,14 @@ function syncTemplates(projectRoot, templateRootOverride) {
1191
1662
  : renderContent(fs.readFileSync(srcFull, 'utf8'), vars);
1192
1663
 
1193
1664
  const exists = fs.existsSync(dstFull);
1194
- if (guardedManaged.has(tgt)) {
1195
- const rawBaseline = managedBaselines[tgt];
1196
- const baseline = trustedBaseline(rawBaseline);
1197
- const templateHash = sha256(content);
1198
- const localHash = exists ? sha256(fs.readFileSync(dstFull)) : null;
1199
-
1200
- if (rawBaseline !== undefined && baseline === null) {
1201
- delete managedBaselines[tgt];
1202
- baselinesChanged = true;
1203
- }
1204
-
1205
- if (baseline === null && localHash !== null && localHash !== templateHash) {
1206
- report.managed.conflicts.push({
1207
- target: tgt,
1208
- reason: 'unknown-origin',
1209
- baseline: null,
1210
- local: localHash,
1211
- template: templateHash
1212
- });
1213
- continue;
1214
- }
1215
-
1216
- if (baseline !== null && templateHash === baseline && localHash !== baseline) {
1217
- report.managed.protected.push({
1218
- target: tgt,
1219
- reason: localHash === null ? 'user-deleted' : 'user-modified',
1220
- baseline,
1221
- local: localHash,
1222
- template: templateHash
1223
- });
1224
- continue;
1225
- }
1226
-
1227
- if (baseline !== null && localHash !== baseline && templateHash !== baseline && localHash !== templateHash) {
1228
- report.managed.conflicts.push({
1229
- target: tgt,
1230
- reason: 'both-modified',
1231
- baseline,
1232
- local: localHash,
1233
- template: templateHash
1234
- });
1235
- continue;
1236
- }
1237
-
1238
- if (localHash === templateHash) {
1239
- report.managed.unchanged.push(tgt);
1240
- if (managedBaselines[tgt] !== templateHash) {
1241
- managedBaselines[tgt] = templateHash;
1242
- baselinesChanged = true;
1243
- }
1244
- continue;
1665
+ const clientManagedTarget = assetPlan.enabledManaged.some((asset) =>
1666
+ assetMatches(asset, tgt)
1667
+ );
1668
+ if (guardedManaged.has(tgt) || (clientManagedTarget && srcRoot === templateRoot)) {
1669
+ const written = writeProtectedManaged(tgt, content, report.managed);
1670
+ if (written && tgt.endsWith('.sh')) {
1671
+ try { fs.chmodSync(dstFull, 0o755); } catch { /* Windows */ }
1245
1672
  }
1246
-
1247
- const dir = path.dirname(dstFull);
1248
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1249
- fs.writeFileSync(dstFull, content);
1250
- managedBaselines[tgt] = templateHash;
1251
- baselinesChanged = true;
1252
- (exists ? report.managed.written : report.managed.created).push(tgt);
1253
1673
  continue;
1254
1674
  }
1255
1675
 
@@ -1281,7 +1701,25 @@ function syncTemplates(projectRoot, templateRootOverride) {
1281
1701
  if (projFile === configPathRel) continue;
1282
1702
  if (isCustomProtected(projFile, protectedCustomSkills, project, customTUICommandTargets)) continue;
1283
1703
  if (matchesAny(projFile, merged) || matchesAny(projFile, ejected)) continue;
1284
-
1704
+ if (assetPlan.enabledManaged.includes(entry)) {
1705
+ const baseline = trustedBaseline(managedBaselines[projFile]);
1706
+ const localHash = sha256(fs.readFileSync(path.join(projectRoot, projFile)));
1707
+ if (baseline !== null && localHash === baseline) {
1708
+ fs.unlinkSync(path.join(projectRoot, projFile));
1709
+ delete managedBaselines[projFile];
1710
+ baselinesChanged = true;
1711
+ report.managed.removed.push(projFile);
1712
+ } else {
1713
+ report.managed.protected.push({
1714
+ target: projFile,
1715
+ reason: baseline === null ? 'unknown-origin' : 'user-modified',
1716
+ baseline,
1717
+ local: localHash,
1718
+ template: null
1719
+ });
1720
+ }
1721
+ continue;
1722
+ }
1285
1723
  fs.unlinkSync(path.join(projectRoot, projFile));
1286
1724
  report.managed.removed.push(projFile);
1287
1725
  }
@@ -1300,7 +1738,17 @@ function syncTemplates(projectRoot, templateRootOverride) {
1300
1738
 
1301
1739
  const customSkills = detectCustomSkills(projectRoot, templateSkillNames);
1302
1740
  report.custom.detected = customSkills.map((skill) => skill.dirName);
1303
- generateCustomCommands(projectRoot, customSkills, project, lang, report, customTUIs, templateSkillNames, enabledTUIs);
1741
+ generateCustomCommands(
1742
+ projectRoot,
1743
+ customSkills,
1744
+ project,
1745
+ lang,
1746
+ report,
1747
+ customTUIs,
1748
+ templateSkillNames,
1749
+ enabledTUIs,
1750
+ writeProtectedManaged
1751
+ );
1304
1752
 
1305
1753
  for (const entry of ejected) {
1306
1754
  const dstFull = path.join(projectRoot, entry);
@@ -1310,7 +1758,11 @@ function syncTemplates(projectRoot, templateRootOverride) {
1310
1758
  }
1311
1759
  // Do not (re)create ejected files for disabled TUIs. Existing files are
1312
1760
  // never touched by sync (handled above); this guard only blocks creation.
1313
- if (isPathOwnedByDisabledTUI(entry, enabledTUIs)) continue;
1761
+ const disabledEjected = AGENT_CLIENT_MANIFEST.some((adapter) =>
1762
+ !enabledTUIs.has(adapter.id)
1763
+ && adapter.ejected.some((asset) => assetMatches(asset, entry))
1764
+ );
1765
+ if (disabledEjected) continue;
1314
1766
 
1315
1767
  const selected = platformSelect(langSelect(entryVariantRels(entry, allSet, platformType), lang, allSet, project), platformType, project);
1316
1768
  const target = norm(renderPathname(entry, project));
@@ -1331,11 +1783,6 @@ function syncTemplates(projectRoot, templateRootOverride) {
1331
1783
  report.managed.skippedPlatform.push(entry);
1332
1784
  continue;
1333
1785
  }
1334
- if (isPathOwnedByDisabledTUI(entry, enabledTUIs)) {
1335
- report.managed.skippedTUI.push(entry);
1336
- continue;
1337
- }
1338
-
1339
1786
  if (entry.includes('*')) {
1340
1787
  const hits = allRels.filter(r => {
1341
1788
  const t = norm(renderPathname(stripLangVariant(r), project));
@@ -1356,22 +1803,6 @@ function syncTemplates(projectRoot, templateRootOverride) {
1356
1803
  ([target, template]) => ({ target, template })
1357
1804
  );
1358
1805
 
1359
- const hasChanges = (
1360
- report.managed.written.length +
1361
- report.managed.created.length +
1362
- report.managed.removed.length +
1363
- report.custom.generated.length +
1364
- report.custom.updated.length +
1365
- report.custom.removed.length +
1366
- report.custom.commands.generated.length +
1367
- report.custom.commands.updated.length +
1368
- report.ejected.created.length +
1369
- report.registryAdded.length
1370
- ) > 0;
1371
-
1372
- const prevVersion = cfg.templateVersion;
1373
- const sandboxToolsMigrated = migrateSandboxTools(cfg);
1374
-
1375
1806
  cfg.files.managed = managed;
1376
1807
  cfg.files.merged = merged;
1377
1808
  cfg.files.ejected = ejected;
@@ -1384,9 +1815,22 @@ function syncTemplates(projectRoot, templateRootOverride) {
1384
1815
  cfg.templateVersion = version;
1385
1816
  delete cfg.templateSource;
1386
1817
 
1387
- report.configUpdated = hasChanges || baselinesChanged || prevVersion !== version || hadTemplateSource || sandboxToolsMigrated;
1388
-
1389
- fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
1818
+ const nextConfigText = JSON.stringify(cfg, null, 2) + '\n';
1819
+ report.configUpdated = originalConfigText !== nextConfigText;
1820
+ if (report.configUpdated) {
1821
+ const temporary = `${cfgPath}.tmp-${process.pid}-${Date.now()}`;
1822
+ try {
1823
+ fs.writeFileSync(temporary, nextConfigText, 'utf8');
1824
+ fs.renameSync(temporary, cfgPath);
1825
+ } catch (error) {
1826
+ try {
1827
+ if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
1828
+ } catch {
1829
+ // Preserve the original write/rename failure.
1830
+ }
1831
+ throw error;
1832
+ }
1833
+ }
1390
1834
 
1391
1835
  return report;
1392
1836
  }
@@ -1399,4 +1843,4 @@ if (entryPath === fileURLToPath(import.meta.url)) {
1399
1843
  if (result.error) process.exitCode = 1;
1400
1844
  }
1401
1845
 
1402
- export { syncTemplates };
1846
+ export { normalizeAgentClientConfig, syncTemplates };