helloagents 3.0.33 → 3.0.37

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 (66) hide show
  1. package/.claude-plugin/marketplace.json +1 -4
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +3 -4
  4. package/README.md +78 -74
  5. package/README_CN.md +78 -74
  6. package/bootstrap-lite.md +9 -11
  7. package/bootstrap.md +21 -23
  8. package/gemini-extension.json +1 -1
  9. package/install.ps1 +27 -4
  10. package/install.sh +27 -3
  11. package/package.json +2 -2
  12. package/scripts/capability-registry.mjs +5 -3
  13. package/scripts/cli-doctor-codex.mjs +153 -1
  14. package/scripts/cli-doctor-render.mjs +2 -1
  15. package/scripts/cli-doctor.mjs +3 -3
  16. package/scripts/cli-hosts.mjs +1 -1
  17. package/scripts/cli-lifecycle-hosts.mjs +124 -54
  18. package/scripts/cli-lifecycle.mjs +50 -15
  19. package/scripts/cli-messages.mjs +7 -7
  20. package/scripts/cli-runtime-root.mjs +9 -1
  21. package/scripts/delivery-gate-messages.mjs +5 -4
  22. package/scripts/delivery-gate.mjs +11 -22
  23. package/scripts/guard.mjs +1 -1
  24. package/scripts/notify-closeout.mjs +61 -22
  25. package/scripts/notify-context.mjs +5 -5
  26. package/scripts/notify-route.mjs +1 -1
  27. package/scripts/notify-sound.mjs +2 -1
  28. package/scripts/notify.mjs +2 -2
  29. package/scripts/plan-contract.mjs +10 -14
  30. package/scripts/project-session-cleanup.mjs +91 -31
  31. package/scripts/qa-review-state.mjs +313 -0
  32. package/scripts/ralph-loop.mjs +32 -13
  33. package/scripts/runtime-artifacts.mjs +2 -2
  34. package/scripts/runtime-scope.mjs +14 -13
  35. package/scripts/runtime-ttl.mjs +7 -4
  36. package/scripts/session-capsule.mjs +75 -13
  37. package/scripts/session-token.mjs +44 -9
  38. package/scripts/state-document.mjs +77 -0
  39. package/scripts/workflow-core.mjs +13 -19
  40. package/scripts/workflow-plan-files.mjs +1 -1
  41. package/scripts/workflow-recommendation.mjs +55 -67
  42. package/scripts/workflow-state.mjs +8 -8
  43. package/skills/commands/auto/SKILL.md +12 -12
  44. package/skills/commands/build/SKILL.md +9 -10
  45. package/skills/commands/commit/SKILL.md +1 -1
  46. package/skills/commands/help/SKILL.md +11 -13
  47. package/skills/commands/init/SKILL.md +18 -9
  48. package/skills/commands/loop/SKILL.md +70 -96
  49. package/skills/commands/plan/SKILL.md +7 -8
  50. package/skills/commands/prd/SKILL.md +3 -3
  51. package/skills/commands/qa/SKILL.md +49 -0
  52. package/skills/hello-ui/SKILL.md +3 -3
  53. package/skills/helloagents/SKILL.md +11 -14
  54. package/skills/qa-review/SKILL.md +92 -0
  55. package/templates/plans/contract.json +4 -7
  56. package/templates/plans/plan.md +1 -1
  57. package/templates/plans/tasks.md +1 -1
  58. package/templates/verify.yaml +1 -1
  59. package/scripts/review-state.mjs +0 -193
  60. package/scripts/verify-state.mjs +0 -175
  61. package/skills/commands/global/SKILL.md +0 -71
  62. package/skills/commands/verify/SKILL.md +0 -46
  63. package/skills/commands/wiki/SKILL.md +0 -57
  64. package/skills/hello-review/SKILL.md +0 -42
  65. package/skills/hello-verify/SKILL.md +0 -144
  66. /package/hooks/{hooks.json → hooks-gemini.json} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process'
2
+ import { platform } from 'node:os'
2
3
 
3
4
  import {
4
5
  installClaudeStandby,
@@ -17,23 +18,50 @@ import { getHostLabel } from './cli-host-detect.mjs'
17
18
 
18
19
  const CLAUDE_COMMAND = process.env.HELLOAGENTS_CLAUDE_CMD || 'claude'
19
20
  const GEMINI_COMMAND = process.env.HELLOAGENTS_GEMINI_CMD || 'gemini'
20
- const CLAUDE_MARKETPLACE = 'hellowind777/helloagents'
21
+ const CLAUDE_MARKETPLACE = 'https://github.com/hellowind777/helloagents.git'
21
22
  const CLAUDE_PLUGIN = 'helloagents@helloagents'
22
- const GEMINI_EXTENSION = 'https://github.com/hellowind777/helloagents'
23
+
24
+ function normalizeCommand(command = '') {
25
+ return String(command || '').trim()
26
+ }
27
+
28
+ function commandCandidates(command = '') {
29
+ const normalized = normalizeCommand(command)
30
+ if (!normalized) return []
31
+
32
+ if (platform() !== 'win32') return [normalized]
33
+
34
+ const candidates = new Set([normalized])
35
+ if (!/\.(cmd|bat|exe|ps1)$/i.test(normalized)) {
36
+ candidates.add(`${normalized}.cmd`)
37
+ candidates.add(`${normalized}.exe`)
38
+ }
39
+ return [...candidates]
40
+ }
23
41
 
24
42
  function runHostCommand(command, args) {
25
- const needsShell = process.platform === 'win32' && /\.cmd$/i.test(command)
26
- const result = spawnSync(command, args, {
27
- encoding: 'utf-8',
28
- errors: 'replace',
29
- shell: needsShell,
30
- windowsHide: true,
31
- })
32
- const errorMessage = result.error?.message || ''
43
+ const attempts = commandCandidates(command)
44
+ let lastResult = null
45
+
46
+ for (const candidate of attempts) {
47
+ const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(candidate)
48
+ const result = spawnSync(candidate, args, {
49
+ encoding: 'utf-8',
50
+ errors: 'replace',
51
+ shell: needsShell,
52
+ windowsHide: true,
53
+ })
54
+ lastResult = result
55
+ if (!result.error || !['ENOENT', 'EINVAL'].includes(result.error.code)) {
56
+ break
57
+ }
58
+ }
59
+
60
+ const errorMessage = lastResult?.error?.message || ''
33
61
  return {
34
- ok: result.status === 0,
35
- missing: result.error?.code === 'ENOENT',
36
- output: `${result.stdout || ''}${result.stderr || ''}${errorMessage}`.trim(),
62
+ ok: lastResult?.status === 0,
63
+ missing: ['ENOENT', 'EINVAL'].includes(lastResult?.error?.code || ''),
64
+ output: `${lastResult?.stdout || ''}${lastResult?.stderr || ''}${errorMessage}`.trim(),
37
65
  }
38
66
  }
39
67
 
@@ -46,6 +74,13 @@ function buildNativeResult(result, successCN, successEN, manualCN, manualEN) {
46
74
  }
47
75
  }
48
76
 
77
+ function preserveTrackedModeOnFailure(result = {}, trackedMode = '') {
78
+ if (result.ok === false && trackedMode) {
79
+ return { ...result, trackedModeOnFailure: trackedMode }
80
+ }
81
+ return result
82
+ }
83
+
49
84
  function installClaudeGlobalPlugin() {
50
85
  const add = runHostCommand(CLAUDE_COMMAND, ['plugin', 'marketplace', 'add', CLAUDE_MARKETPLACE])
51
86
  if (!add.ok && add.missing) return { ok: false, output: '未找到 claude 命令' }
@@ -53,8 +88,8 @@ function installClaudeGlobalPlugin() {
53
88
  return { ok: install.ok, output: install.output || add.output }
54
89
  }
55
90
 
56
- function installGeminiGlobalExtension() {
57
- return runHostCommand(GEMINI_COMMAND, ['extensions', 'install', GEMINI_EXTENSION])
91
+ function installGeminiGlobalExtension(runtimeRoot) {
92
+ return runHostCommand(GEMINI_COMMAND, ['extensions', 'link', runtimeRoot])
58
93
  }
59
94
 
60
95
  function removeClaudeGlobalPlugin() {
@@ -70,8 +105,11 @@ function reportHostAction(runtime, action, host, mode, result = {}) {
70
105
  const isCleanup = action === 'cleanup' || action === 'uninstall'
71
106
  if (result.skipped) {
72
107
  console.log(runtime.msg(` - ${label} 未检测到,跳过`, ` - ${label} not detected, skipped`))
73
- } else if (result.ok === false && !isCleanup) {
74
- console.log(runtime.msg(` - ${label} 自动配置未完成`, ` - ${label} automatic setup did not complete`))
108
+ } else if (result.ok === false) {
109
+ console.log(runtime.msg(
110
+ isCleanup ? ` - ${label} 自动清理未完成` : ` - ${label} 自动配置未完成`,
111
+ isCleanup ? ` - ${label} automatic cleanup did not complete` : ` - ${label} automatic setup did not complete`,
112
+ ))
75
113
  } else if (isCleanup) {
76
114
  runtime.ok(runtime.msg(`${label} 已清理(${mode} 模式)`, `${label} cleaned (${mode} mode)`))
77
115
  } else if (mode === 'standby') {
@@ -87,14 +125,46 @@ function reportHostAction(runtime, action, host, mode, result = {}) {
87
125
  }
88
126
  }
89
127
 
90
- function installHostStandby(runtime, host) {
128
+ function prepareClaudeStandby(previousMode) {
129
+ if (previousMode !== 'global') return {}
130
+ return preserveTrackedModeOnFailure(
131
+ buildNativeResult(
132
+ removeClaudeGlobalPlugin(),
133
+ '已自动移除 Claude Code 插件',
134
+ 'Claude Code plugin removed automatically',
135
+ '切到 standby 前无法自动移除 Claude Code 插件,请先在 Claude Code 中执行: /plugin remove helloagents',
136
+ 'Could not remove the Claude Code plugin before switching to standby. Run inside Claude Code: /plugin remove helloagents',
137
+ ),
138
+ 'global',
139
+ )
140
+ }
141
+
142
+ function prepareGeminiStandby(previousMode) {
143
+ if (previousMode !== 'global') return {}
144
+ return preserveTrackedModeOnFailure(
145
+ buildNativeResult(
146
+ removeGeminiGlobalExtension(),
147
+ '已自动移除 Gemini CLI 扩展',
148
+ 'Gemini CLI extension removed automatically',
149
+ '切到 standby 前无法自动移除 Gemini CLI 扩展,请先手动执行: gemini extensions uninstall helloagents',
150
+ 'Could not remove the Gemini CLI extension before switching to standby. Run manually: gemini extensions uninstall helloagents',
151
+ ),
152
+ 'global',
153
+ )
154
+ }
155
+
156
+ function installHostStandby(runtime, host, { previousMode = '' } = {}) {
91
157
  if (host === 'claude') {
158
+ const cleanupResult = prepareClaudeStandby(previousMode)
159
+ if (cleanupResult.ok === false) return cleanupResult
92
160
  installClaudeStandby(runtime.home, runtime.pkgRoot)
93
- return {}
161
+ return cleanupResult
94
162
  }
95
163
  if (host === 'gemini') {
164
+ const cleanupResult = prepareGeminiStandby(previousMode)
165
+ if (cleanupResult.ok === false) return cleanupResult
96
166
  installGeminiStandby(runtime.home, runtime.pkgRoot)
97
- return {}
167
+ return cleanupResult
98
168
  }
99
169
  if (!installCodexStandby(runtime.home, runtime.pkgRoot)) return { skipped: true }
100
170
  cleanupCodexGlobalResidueForStandby(runtime.home)
@@ -108,18 +178,18 @@ function installHostGlobal(runtime, host) {
108
178
  installClaudeGlobalPlugin(),
109
179
  '已自动安装 Claude Code 插件;重启 Claude Code 后生效',
110
180
  'Claude Code plugin installed automatically; restart Claude Code to apply',
111
- 'Claude Code 插件自动安装失败,请在 Claude Code 中执行: /plugin marketplace add hellowind777/helloagents;/plugin install helloagents@helloagents',
112
- 'Claude Code plugin auto-install failed. Run inside Claude Code: /plugin marketplace add hellowind777/helloagents; /plugin install helloagents@helloagents',
181
+ 'Claude Code 插件自动安装失败,请在 Claude Code 中执行: /plugin marketplace add https://github.com/hellowind777/helloagents.git;/plugin install helloagents@helloagents',
182
+ 'Claude Code plugin auto-install failed. Run inside Claude Code: /plugin marketplace add https://github.com/hellowind777/helloagents.git; /plugin install helloagents@helloagents',
113
183
  )
114
184
  }
115
185
  if (host === 'gemini') {
116
186
  uninstallGeminiStandby(runtime.home)
117
187
  return buildNativeResult(
118
- installGeminiGlobalExtension(),
188
+ installGeminiGlobalExtension(runtime.pkgRoot),
119
189
  '已自动安装 Gemini CLI 扩展;重启 Gemini CLI 后生效',
120
190
  'Gemini CLI extension installed automatically; restart Gemini CLI to apply',
121
- 'Gemini CLI 扩展自动安装失败,请手动执行: gemini extensions install https://github.com/hellowind777/helloagents',
122
- 'Gemini CLI extension auto-install failed. Run manually: gemini extensions install https://github.com/hellowind777/helloagents',
191
+ `Gemini CLI 扩展自动安装失败,请手动执行: gemini extensions link ${runtime.pkgRoot}`,
192
+ `Gemini CLI extension auto-install failed. Run manually: gemini extensions link ${runtime.pkgRoot}`,
123
193
  )
124
194
  }
125
195
  uninstallCodexStandby(runtime.home)
@@ -137,41 +207,41 @@ function cleanupHostStandby(runtime, host) {
137
207
  function cleanupHostGlobal(runtime, host) {
138
208
  if (host === 'claude') {
139
209
  uninstallClaudeStandby(runtime.home)
140
- return buildNativeResult(
141
- removeClaudeGlobalPlugin(),
142
- '已自动移除 Claude Code 插件',
143
- 'Claude Code plugin removed automatically',
144
- 'Claude Code 插件自动移除失败,请手动执行: /plugin remove helloagents',
145
- 'Claude Code plugin auto-remove failed. Run manually: /plugin remove helloagents',
210
+ return preserveTrackedModeOnFailure(
211
+ buildNativeResult(
212
+ removeClaudeGlobalPlugin(),
213
+ '已自动移除 Claude Code 插件',
214
+ 'Claude Code plugin removed automatically',
215
+ 'Claude Code 插件自动移除失败,请手动执行: /plugin remove helloagents',
216
+ 'Claude Code plugin auto-remove failed. Run manually: /plugin remove helloagents',
217
+ ),
218
+ 'global',
146
219
  )
147
220
  }
148
221
  if (host === 'gemini') {
149
222
  uninstallGeminiStandby(runtime.home)
150
- return buildNativeResult(
151
- removeGeminiGlobalExtension(),
152
- '已自动移除 Gemini CLI 扩展',
153
- 'Gemini CLI extension removed automatically',
154
- 'Gemini CLI 扩展自动移除失败,请手动执行: gemini extensions uninstall helloagents',
155
- 'Gemini CLI extension auto-remove failed. Run manually: gemini extensions uninstall helloagents',
223
+ return preserveTrackedModeOnFailure(
224
+ buildNativeResult(
225
+ removeGeminiGlobalExtension(),
226
+ '已自动移除 Gemini CLI 扩展',
227
+ 'Gemini CLI extension removed automatically',
228
+ 'Gemini CLI 扩展自动移除失败,请手动执行: gemini extensions uninstall helloagents',
229
+ 'Gemini CLI extension auto-remove failed. Run manually: gemini extensions uninstall helloagents',
230
+ ),
231
+ 'global',
156
232
  )
157
233
  }
158
234
  return { skipped: !uninstallCodexGlobal(runtime.home) }
159
235
  }
160
236
 
161
- function installStandby(runtime) {
237
+ function installStandby(runtime, previousModes = {}) {
162
238
  const results = {}
163
- if (installClaudeStandby(runtime.home, runtime.pkgRoot)) {
164
- runtime.ok(runtime.msg('Claude Code 已配置(standby 模式)', 'Claude Code configured (standby mode)'))
165
- results.claude = {}
166
- } else {
167
- results.claude = { skipped: true }
168
- }
169
- if (installGeminiStandby(runtime.home, runtime.pkgRoot)) {
170
- runtime.ok(runtime.msg('Gemini CLI 已配置(standby 模式)', 'Gemini CLI configured (standby mode)'))
171
- results.gemini = {}
172
- } else {
173
- results.gemini = { skipped: true }
174
- }
239
+ const claudeResult = installHostStandby(runtime, 'claude', { previousMode: previousModes.claude || '' })
240
+ reportHostAction(runtime, 'install', 'claude', 'standby', claudeResult)
241
+ results.claude = claudeResult.skipped ? { skipped: true } : claudeResult
242
+ const geminiResult = installHostStandby(runtime, 'gemini', { previousMode: previousModes.gemini || '' })
243
+ reportHostAction(runtime, 'install', 'gemini', 'standby', geminiResult)
244
+ results.gemini = geminiResult.skipped ? { skipped: true } : geminiResult
175
245
  if (installCodexStandby(runtime.home, runtime.pkgRoot)) {
176
246
  cleanupCodexGlobalResidueForStandby(runtime.home)
177
247
  runtime.ok(runtime.msg('Codex CLI 已配置(standby 模式)', 'Codex CLI configured (standby mode)'))
@@ -193,9 +263,9 @@ function installGlobal(runtime) {
193
263
  return results
194
264
  }
195
265
 
196
- export function installAllHosts(runtime, mode) {
266
+ export function installAllHosts(runtime, mode, { previousModes = {} } = {}) {
197
267
  if (mode === 'global') return installGlobal(runtime)
198
- return installStandby(runtime)
268
+ return installStandby(runtime, previousModes)
199
269
  }
200
270
 
201
271
  export function uninstallAllHosts(runtime) {
@@ -205,10 +275,10 @@ export function uninstallAllHosts(runtime) {
205
275
  uninstallCodexGlobal(runtime.home)
206
276
  }
207
277
 
208
- export function runHostLifecycle(runtime, action, host, mode) {
278
+ export function runHostLifecycle(runtime, action, host, mode, options = {}) {
209
279
  const result = (action === 'cleanup' || action === 'uninstall')
210
280
  ? (mode === 'global' ? cleanupHostGlobal(runtime, host) : cleanupHostStandby(runtime, host))
211
- : (mode === 'global' ? installHostGlobal(runtime, host) : installHostStandby(runtime, host))
281
+ : (mode === 'global' ? installHostGlobal(runtime, host) : installHostStandby(runtime, host, options))
212
282
 
213
283
  reportHostAction(runtime, action, host, mode, result)
214
284
  return result
@@ -7,7 +7,7 @@ import {
7
7
  getHostLabel as resolveHostLabel,
8
8
  normalizeHost as normalizeLifecycleHost,
9
9
  } from './cli-host-detect.mjs'
10
- import { installAllHosts, runHostLifecycle, uninstallAllHosts } from './cli-lifecycle-hosts.mjs'
10
+ import { installAllHosts, runHostLifecycle } from './cli-lifecycle-hosts.mjs'
11
11
  import { ensureDir, safeJson, safeWrite } from './cli-utils.mjs'
12
12
 
13
13
  export const HOSTS = ['claude', 'gemini', 'codex']
@@ -82,6 +82,18 @@ function syncTrackedHostMode(settings, host, result, mode) {
82
82
  setTrackedHostMode(settings, host, mode)
83
83
  return
84
84
  }
85
+ if (result?.trackedModeOnFailure) {
86
+ setTrackedHostMode(settings, host, result.trackedModeOnFailure)
87
+ return
88
+ }
89
+ clearTrackedHostMode(settings, host)
90
+ }
91
+
92
+ function syncCleanupTrackedHostMode(settings, host, result) {
93
+ if (result?.trackedModeOnFailure) {
94
+ setTrackedHostMode(settings, host, result.trackedModeOnFailure)
95
+ return
96
+ }
85
97
  clearTrackedHostMode(settings, host)
86
98
  }
87
99
 
@@ -129,6 +141,14 @@ export function getHostLabel(host) {
129
141
  return resolveHostLabel(host)
130
142
  }
131
143
 
144
+ function resolvePreviousHostMode(settings, host) {
145
+ return detectHostMode(host) || getTrackedHostMode(settings, host) || ''
146
+ }
147
+
148
+ function buildPreviousHostModes(settings) {
149
+ return Object.fromEntries(HOSTS.map((host) => [host, resolvePreviousHostMode(settings, host)]))
150
+ }
151
+
132
152
  function resolveHostMode(host, explicitMode, settings) {
133
153
  if (explicitMode) return explicitMode
134
154
  return detectHostMode(host)
@@ -167,6 +187,7 @@ export function switchMode(newMode) {
167
187
  const config = readSettings(true)
168
188
  const oldMode = config.install_mode || DEFAULTS.install_mode
169
189
  const isRefresh = oldMode === newMode
190
+ const previousModes = buildPreviousHostModes(config)
170
191
 
171
192
  if (!isRefresh) {
172
193
  config.install_mode = newMode
@@ -175,7 +196,7 @@ export function switchMode(newMode) {
175
196
  runtime.ok(runtime.msg(`当前已是 ${newMode} 模式,正在刷新安装`, `Already in ${newMode} mode, refreshing installation`))
176
197
  }
177
198
 
178
- const results = installAllHosts(runtime, newMode)
199
+ const results = installAllHosts(runtime, newMode, { previousModes })
179
200
  clearAllTrackedHostModes(config)
180
201
  for (const host of HOSTS) {
181
202
  syncTrackedHostMode(config, host, results?.[host], newMode)
@@ -187,13 +208,25 @@ export function switchMode(newMode) {
187
208
  function runAllHostsLifecycle(action, explicitMode) {
188
209
  if (action === 'cleanup' || action === 'uninstall') {
189
210
  console.log(`\n HelloAGENTS — ${runtime.msg('正在清理', 'Cleaning up')}\n`)
190
- uninstallAllHosts(runtime)
211
+ const settings = existsSync(runtime.configFile) ? readSettings() : {}
212
+ const results = {}
213
+ for (const host of HOSTS) {
214
+ const mode = explicitMode || resolveHostMode(host, '', settings)
215
+ results[host] = runHostLifecycle(runtime, action, host, mode, {
216
+ previousMode: resolvePreviousHostMode(settings, host),
217
+ })
218
+ }
191
219
  if (existsSync(runtime.configFile)) {
192
- const settings = readSettings()
193
- clearAllTrackedHostModes(settings)
220
+ for (const host of HOSTS) {
221
+ syncCleanupTrackedHostMode(settings, host, results[host])
222
+ }
194
223
  writeSettings(settings)
195
224
  }
196
- runtime.ok(runtime.msg('所有 CLI 配置已清理', 'All CLI configurations cleaned'))
225
+ const hasFailures = Object.values(results).some((result) => result?.ok === false)
226
+ runtime.ok(runtime.msg(
227
+ hasFailures ? '部分 CLI 仍需手动清理' : '所有 CLI 配置已清理',
228
+ hasFailures ? 'Some CLI cleanup still needs manual action' : 'All CLI configurations cleaned',
229
+ ))
197
230
  console.log(runtime.msg(
198
231
  ' ℹ ~/.helloagents/ 已保留(如需彻底清理请手动删除)\n ℹ 已自动尝试移除 Claude/Gemini 插件或扩展;如宿主命令不可用,请手动执行对应移除命令',
199
232
  ' ℹ ~/.helloagents/ preserved (delete manually if desired)\n ℹ Claude/Gemini plugin or extension removal was attempted automatically; if host commands are unavailable, remove them manually',
@@ -203,12 +236,14 @@ function runAllHostsLifecycle(action, explicitMode) {
203
236
  }
204
237
 
205
238
  const settings = readSettings(true)
239
+ const previousModes = buildPreviousHostModes(settings)
206
240
  if (!explicitMode) {
207
241
  for (const host of HOSTS) {
208
242
  const mode = resolveHostMode(host, '', settings)
209
- const result = runHostLifecycle(runtime, action, host, mode)
210
- if (!result.skipped && result.ok !== false) setTrackedHostMode(settings, host, mode)
211
- else clearTrackedHostMode(settings, host)
243
+ const result = runHostLifecycle(runtime, action, host, mode, {
244
+ previousMode: previousModes[host],
245
+ })
246
+ syncTrackedHostMode(settings, host, result, mode)
212
247
  }
213
248
  writeSettings(settings)
214
249
  const modes = Object.values(settings.host_install_modes || {})
@@ -221,12 +256,10 @@ function runAllHostsLifecycle(action, explicitMode) {
221
256
 
222
257
  const mode = resolveInstallMode(explicitMode, settings)
223
258
  if (explicitMode) settings.install_mode = explicitMode
224
- const results = installAllHosts(runtime, mode)
259
+ const results = installAllHosts(runtime, mode, { previousModes })
225
260
  settings.host_install_modes = {}
226
261
  for (const host of HOSTS) {
227
- if (!results?.[host]?.skipped && results?.[host]?.ok !== false) {
228
- settings.host_install_modes[host] = mode
229
- }
262
+ syncTrackedHostMode(settings, host, results?.[host], mode)
230
263
  }
231
264
  writeSettings(settings)
232
265
  runtime.printInstallMsg(mode, action === 'update' ? 'refresh' : 'install')
@@ -242,11 +275,13 @@ export function runScopedLifecycle(action, rawArgs) {
242
275
  const shouldEnsure = action === 'install' || action === 'update'
243
276
  const settings = readSettings(shouldEnsure)
244
277
  const mode = resolveHostMode(host, explicitMode, settings)
245
- const result = runHostLifecycle(runtime, action, host, mode)
278
+ const result = runHostLifecycle(runtime, action, host, mode, {
279
+ previousMode: resolvePreviousHostMode(settings, host),
280
+ })
246
281
 
247
282
  if (action === 'cleanup' || action === 'uninstall') {
248
283
  if (existsSync(runtime.configFile)) {
249
- clearTrackedHostMode(settings, host)
284
+ syncCleanupTrackedHostMode(settings, host, result)
250
285
  writeSettings(settings)
251
286
  }
252
287
  } else if (!result.skipped) {
@@ -21,9 +21,9 @@ function codexGlobalStatus({ home, msg }) {
21
21
 
22
22
  function pluginCommands() {
23
23
  return [
24
- ' Claude Code: /plugin marketplace add hellowind777/helloagents',
24
+ ' Claude Code: /plugin marketplace add https://github.com/hellowind777/helloagents.git',
25
25
  ' /plugin install helloagents@helloagents',
26
- ' Gemini CLI: gemini extensions install https://github.com/hellowind777/helloagents',
26
+ ' Gemini CLI: helloagents install gemini --global',
27
27
  ].join('\n')
28
28
  }
29
29
 
@@ -65,18 +65,18 @@ function renderInstallMessage(context, mode, state) {
65
65
 
66
66
  if (install) {
67
67
  return msg(
68
- `\n ✅ HelloAGENTS 已安装(standby 模式)!\n\n Claude Code: 已自动配置(~/.claude/CLAUDE.md + hooks)\n Gemini CLI: 已自动配置(~/.gemini/GEMINI.md)\n Codex: ${codexStandbyStatus(context)}\n\n ${restartHint(msg)}\n\n standby 模式下,hello-* 技能不会自动触发。\n 在项目中使用 ~wiki 或 ~init 仅创建/同步知识库;用 ~global 初始化项目级全局模式;也可用 ~command 按需调用。\n\n 切换模式:\n helloagents --global 项目级全局模式(自动尝试 Claude/Gemini 插件或扩展;Codex 自动装原生本地插件)`,
69
- `\n ✅ HelloAGENTS installed (standby mode)!\n\n Claude Code: Auto-configured (~/.claude/CLAUDE.md + hooks)\n Gemini CLI: Auto-configured (~/.gemini/GEMINI.md)\n Codex: ${codexStandbyStatus(context)}\n\n ${restartHint(msg)}\n\n In standby mode, hello-* skills won't auto-trigger.\n Use ~wiki or ~init to create or sync the KB only; use ~global to initialize project-level global mode; ~command stays available on demand.\n\n Switch modes:\n helloagents --global Project-level global mode (auto-attempts Claude/Gemini plugins or extensions; native local plugin auto-install for Codex)`,
68
+ `\n ✅ HelloAGENTS 已安装(standby 模式)!\n\n Claude Code: 已自动配置(~/.claude/CLAUDE.md + hooks)\n Gemini CLI: 已自动配置(~/.gemini/GEMINI.md)\n Codex: ${codexStandbyStatus(context)}\n\n ${restartHint(msg)}\n\n standby 模式下,hello-* 技能不会自动触发。\n 在项目中使用 ~init 初始化完整项目工作流;未初始化时也可继续用 ~command 按需调用。\n\n 切换模式:\n helloagents --global 宿主级全局部署(自动尝试 Claude/Gemini 插件或扩展;Codex 自动装原生本地插件)`,
69
+ `\n ✅ HelloAGENTS installed (standby mode)!\n\n Claude Code: Auto-configured (~/.claude/CLAUDE.md + hooks)\n Gemini CLI: Auto-configured (~/.gemini/GEMINI.md)\n Codex: ${codexStandbyStatus(context)}\n\n ${restartHint(msg)}\n\n In standby mode, hello-* skills won't auto-trigger.\n Use ~init to initialize the full project workflow; uninitialized repos can still use ~command on demand.\n\n Switch modes:\n helloagents --global Host-wide global deployment (auto-attempts Claude/Gemini plugins or extensions; native local plugin auto-install for Codex)`,
70
70
  )
71
71
  }
72
72
 
73
73
  return msg(
74
74
  refresh
75
75
  ? ` standby 模式已刷新,CLI 注入与链接已同步最新文件。\n ${restartHint(msg)}\n ${removeHint(msg)}`
76
- : ` 项目可通过 ~wiki 或 ~init 创建/同步知识库;用 ~global 初始化项目级全局模式;未初始化时仅注入轻量规则。\n ${restartHint(msg)}\n ${removeHint(msg)}`,
76
+ : ` 项目可通过 ~init 初始化完整工作流;未初始化时仅注入轻量规则。\n ${restartHint(msg)}\n ${removeHint(msg)}`,
77
77
  refresh
78
78
  ? ` Standby mode refreshed; injected files and links were synchronized.\n ${restartHint(msg)}\n ${removeHint(msg)}`
79
- : ` Projects can use ~wiki or ~init to create/sync the KB; use ~global to initialize project-level global mode. Projects that are not initialized get lite rules only.\n ${restartHint(msg)}\n ${removeHint(msg)}`,
79
+ : ` Projects can use ~init to initialize the full workflow; projects that are not initialized get lite rules only.\n ${restartHint(msg)}\n ${removeHint(msg)}`,
80
80
  )
81
81
  }
82
82
 
@@ -90,7 +90,7 @@ HelloAGENTS v${pkgVersion} — The orchestration kernel for AI CLIs
90
90
  helloagents-js ${msg('(受管宿主配置的跨平台稳定入口)', '(cross-platform stable entrypoint for managed host configs)')}
91
91
 
92
92
  ${msg('模式切换', 'Mode switching')}:
93
- helloagents --global ${msg('项目级全局模式(自动尝试 Claude/Gemini 插件或扩展;Codex 自动装原生本地插件)', 'Project-level global mode (auto-attempts Claude/Gemini plugins or extensions; native local plugin auto-install for Codex)')}
93
+ helloagents --global ${msg('宿主级全局部署(自动尝试 Claude/Gemini 插件或扩展;Codex 自动装原生本地插件)', 'Host-wide global deployment (auto-attempts Claude/Gemini plugins or extensions; native local plugin auto-install for Codex)')}
94
94
  helloagents --standby ${msg('标准模式(非插件安装,hello-* 不自动触发,默认)', "Standby mode (non-plugin install, hello-* won't auto-trigger, default)")}
95
95
 
96
96
  ${msg('单 CLI 管理', 'Scoped CLI management')}:
@@ -1,4 +1,4 @@
1
- import { mkdtempSync, realpathSync, renameSync } from 'node:fs'
1
+ import { copyFileSync, existsSync, mkdtempSync, realpathSync, renameSync } from 'node:fs'
2
2
  import { dirname, join, resolve } from 'node:path'
3
3
 
4
4
  import { copyEntries, ensureDir, removeIfExists } from './cli-utils.mjs'
@@ -63,6 +63,13 @@ function retryTransientFs(operation) {
63
63
  throw lastError
64
64
  }
65
65
 
66
+ function materializeGeminiHooks(root) {
67
+ const source = join(root, 'hooks', 'hooks-gemini.json')
68
+ const target = join(root, 'hooks', 'hooks.json')
69
+ if (!existsSync(source)) return
70
+ copyFileSync(source, target)
71
+ }
72
+
66
73
  /** Sync package runtime files into the stable root without copying repo-only files. */
67
74
  export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
68
75
  const source = resolve(sourceRoot)
@@ -77,6 +84,7 @@ export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
77
84
 
78
85
  try {
79
86
  copyEntries(source, staging, RUNTIME_ROOT_ENTRIES)
87
+ materializeGeminiHooks(staging)
80
88
  retryTransientFs(() => {
81
89
  removeIfExists(target)
82
90
  renameSync(staging, target)
@@ -24,10 +24,8 @@ function issueHeading(issue) {
24
24
  return '任务缺少可交付元数据'
25
25
  case 'missing-contract':
26
26
  return '方案包缺少可信的结构化契约'
27
- case 'missing-verify-evidence':
28
- return '当前工作流缺少最新验证证据'
29
- case 'missing-review-evidence':
30
- return '当前工作流缺少最新审查证据'
27
+ case 'missing-qa-review-evidence':
28
+ return '当前工作流缺少最新 qa-review 证据'
31
29
  case 'missing-advisor-evidence':
32
30
  return '当前工作流缺少最新 advisor 证据'
33
31
  case 'missing-visual-evidence':
@@ -62,6 +60,9 @@ export function buildDeliveryBlockReason(issues, recommendation, gateHint) {
62
60
  if (issues.some((issue) => issue.type === 'missing-visual-evidence')) {
63
61
  lines.push('视觉验收动作:先写入当前会话 `artifacts/visual.json`,记录 `tooling`、`screensChecked`、`statesChecked`、`status` 和 `summary`,再报告完成。')
64
62
  }
63
+ if (issues.some((issue) => issue.type === 'missing-qa-review-evidence')) {
64
+ lines.push('质量闭环动作:先完成 `~qa` 或写入当前会话 `artifacts/qa-review.json`,记录结论、问题定位、验证命令与最新结果,再报告完成。')
65
+ }
65
66
  if (gateHint) {
66
67
  lines.push(gateHint)
67
68
  }
@@ -9,10 +9,9 @@ import { fileURLToPath } from 'node:url'
9
9
  import { getAdvisorEvidenceStatus } from './advisor-state.mjs'
10
10
  import { getCloseoutEvidenceStatus } from './closeout-state.mjs'
11
11
  import { getAdvisorRequirement, getVisualValidationRequirement } from './plan-contract.mjs'
12
+ import { getQaReviewEvidenceStatus } from './qa-review-state.mjs'
12
13
  import { getVisualEvidenceStatus } from './visual-state.mjs'
13
14
  import { buildDeliveryGateHint, getDeliveryAction, getWorkflowRecommendation, getWorkflowSnapshot } from './workflow-state.mjs'
14
- import { getReviewEvidenceStatus } from './review-state.mjs'
15
- import { getVerifyEvidenceStatus } from './verify-state.mjs'
16
15
  import { buildDeliveryBlockReason, buildUnderSpecifiedDetails } from './delivery-gate-messages.mjs'
17
16
 
18
17
  function selectGatePlans(snapshot) {
@@ -84,20 +83,12 @@ function collectPlanIssues(planEntries) {
84
83
  return issues
85
84
  }
86
85
 
87
- function collectEvidenceIssues(issues, verificationStatus, reviewStatus, advisorStatus, visualStatus, closeoutStatus) {
88
- if (verificationStatus?.required && verificationStatus.status !== 'valid') {
86
+ function collectEvidenceIssues(issues, qaStatus, advisorStatus, visualStatus, closeoutStatus) {
87
+ if (qaStatus?.required && qaStatus.status !== 'valid') {
89
88
  issues.push({
90
- type: 'missing-verify-evidence',
89
+ type: 'missing-qa-review-evidence',
91
90
  planName: 'delivery',
92
- details: verificationStatus.details,
93
- })
94
- }
95
-
96
- if (reviewStatus?.required && reviewStatus.status !== 'valid') {
97
- issues.push({
98
- type: 'missing-review-evidence',
99
- planName: 'delivery',
100
- details: reviewStatus.details,
91
+ details: qaStatus.details,
101
92
  })
102
93
  }
103
94
  if (advisorStatus?.required && advisorStatus.status !== 'valid') {
@@ -124,9 +115,9 @@ function collectEvidenceIssues(issues, verificationStatus, reviewStatus, advisor
124
115
  }
125
116
  }
126
117
 
127
- function collectGateIssues(planEntries, verificationStatus, reviewStatus, advisorStatus, visualStatus, closeoutStatus) {
118
+ function collectGateIssues(planEntries, qaStatus, advisorStatus, visualStatus, closeoutStatus) {
128
119
  const issues = collectPlanIssues(planEntries)
129
- collectEvidenceIssues(issues, verificationStatus, reviewStatus, advisorStatus, visualStatus, closeoutStatus)
120
+ collectEvidenceIssues(issues, qaStatus, advisorStatus, visualStatus, closeoutStatus)
130
121
  return issues
131
122
  }
132
123
 
@@ -143,11 +134,10 @@ export function evaluateDeliveryGate(data = {}) {
143
134
  const workflowOptions = { payload: data }
144
135
  const snapshot = getWorkflowSnapshot(cwd, workflowOptions)
145
136
  const recommendation = getWorkflowRecommendation(cwd, workflowOptions)
146
- const verificationStatus = getVerifyEvidenceStatus(cwd, workflowOptions)
147
137
  const deliveryAction = getDeliveryAction(cwd, workflowOptions)
148
138
  const gatePlans = selectGatePlans(snapshot)
149
- const reviewStatus = getReviewEvidenceStatus(cwd, {
150
- required: deliveryAction?.phase === 'verify' && deliveryAction?.mode === 'review-first',
139
+ const qaStatus = getQaReviewEvidenceStatus(cwd, {
140
+ required: deliveryAction?.phase === 'qa' || deliveryAction?.phase === 'consolidate',
151
141
  ...workflowOptions,
152
142
  })
153
143
  if (gatePlans.length === 0) {
@@ -169,8 +159,7 @@ export function evaluateDeliveryGate(data = {}) {
169
159
  })
170
160
  const closeoutRequired = (
171
161
  gatePlans.every((entry) => entry.missingFiles.length === 0 && entry.templateIssues.length === 0 && entry.taskSummary.total > 0 && entry.taskSummary.open === 0 && entry.taskSummary.underSpecifiedCount === 0)
172
- && (!verificationStatus.required || verificationStatus.status === 'valid')
173
- && (!reviewStatus.required || reviewStatus.status === 'valid')
162
+ && (!qaStatus.required || qaStatus.status === 'valid')
174
163
  && (!advisorStatus.required || advisorStatus.status === 'valid')
175
164
  && (!visualStatus.required || visualStatus.status === 'valid')
176
165
  )
@@ -179,7 +168,7 @@ export function evaluateDeliveryGate(data = {}) {
179
168
  ...workflowOptions,
180
169
  })
181
170
 
182
- const issues = collectGateIssues(gatePlans, verificationStatus, reviewStatus, advisorStatus, visualStatus, closeoutStatus)
171
+ const issues = collectGateIssues(gatePlans, qaStatus, advisorStatus, visualStatus, closeoutStatus)
183
172
  if (issues.length === 0) {
184
173
  return { suppressOutput: true }
185
174
  }
package/scripts/guard.mjs CHANGED
@@ -76,7 +76,7 @@ function buildHighRiskGate(matches, cwd, payload = {}) {
76
76
  if (!recommendation) return null
77
77
  if (matches.some((match) => match.gate === 'post-verify')) {
78
78
  return {
79
- reason: `[HelloAGENTS Guard] 已阻止 T3 命令:当前工作流尚未进入 VERIFY / CONSOLIDATE。\n当前工作流:${recommendation.summary}\n处理路径:${recommendation.nextPath}\n${recommendation.guidance}`,
79
+ reason: `[HelloAGENTS Guard] 已阻止 T3 命令:当前工作流尚未进入 QA / CONSOLIDATE。\n当前工作流:${recommendation.summary}\n处理路径:${recommendation.nextPath}\n${recommendation.guidance}`,
80
80
  }
81
81
  }
82
82
  if (matches.some((match) => match.gate === 'plan-first') && recommendation.nextCommand === 'plan') {