helloagents 3.0.33 → 3.0.35

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 (57) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.codex-plugin/plugin.json +3 -4
  3. package/README.md +70 -71
  4. package/README_CN.md +70 -71
  5. package/bootstrap-lite.md +9 -11
  6. package/bootstrap.md +21 -23
  7. package/gemini-extension.json +1 -1
  8. package/install.ps1 +21 -3
  9. package/install.sh +19 -2
  10. package/package.json +2 -2
  11. package/scripts/capability-registry.mjs +5 -3
  12. package/scripts/cli-doctor-codex.mjs +150 -1
  13. package/scripts/cli-doctor-render.mjs +2 -1
  14. package/scripts/cli-lifecycle-hosts.mjs +76 -34
  15. package/scripts/cli-lifecycle.mjs +50 -15
  16. package/scripts/cli-messages.mjs +5 -5
  17. package/scripts/delivery-gate-messages.mjs +5 -4
  18. package/scripts/delivery-gate.mjs +11 -22
  19. package/scripts/guard.mjs +1 -1
  20. package/scripts/notify-closeout.mjs +61 -22
  21. package/scripts/notify-context.mjs +5 -5
  22. package/scripts/notify-route.mjs +1 -1
  23. package/scripts/notify.mjs +2 -2
  24. package/scripts/plan-contract.mjs +10 -14
  25. package/scripts/project-session-cleanup.mjs +45 -31
  26. package/scripts/qa-review-state.mjs +313 -0
  27. package/scripts/ralph-loop.mjs +32 -13
  28. package/scripts/runtime-scope.mjs +1 -3
  29. package/scripts/session-capsule.mjs +51 -13
  30. package/scripts/state-document.mjs +77 -0
  31. package/scripts/workflow-core.mjs +13 -19
  32. package/scripts/workflow-plan-files.mjs +1 -1
  33. package/scripts/workflow-recommendation.mjs +55 -67
  34. package/scripts/workflow-state.mjs +8 -8
  35. package/skills/commands/auto/SKILL.md +12 -12
  36. package/skills/commands/build/SKILL.md +9 -10
  37. package/skills/commands/commit/SKILL.md +1 -1
  38. package/skills/commands/help/SKILL.md +11 -13
  39. package/skills/commands/init/SKILL.md +18 -9
  40. package/skills/commands/loop/SKILL.md +70 -96
  41. package/skills/commands/plan/SKILL.md +7 -8
  42. package/skills/commands/prd/SKILL.md +3 -3
  43. package/skills/commands/qa/SKILL.md +49 -0
  44. package/skills/hello-ui/SKILL.md +3 -3
  45. package/skills/helloagents/SKILL.md +11 -14
  46. package/skills/qa-review/SKILL.md +92 -0
  47. package/templates/plans/contract.json +4 -7
  48. package/templates/plans/plan.md +1 -1
  49. package/templates/plans/tasks.md +1 -1
  50. package/templates/verify.yaml +1 -1
  51. package/scripts/review-state.mjs +0 -193
  52. package/scripts/verify-state.mjs +0 -175
  53. package/skills/commands/global/SKILL.md +0 -71
  54. package/skills/commands/verify/SKILL.md +0 -46
  55. package/skills/commands/wiki/SKILL.md +0 -57
  56. package/skills/hello-review/SKILL.md +0 -42
  57. package/skills/hello-verify/SKILL.md +0 -144
package/install.ps1 CHANGED
@@ -15,6 +15,7 @@ $Target = if ($env:HELLOAGENTS_TARGET) { $env:HELLOAGENTS_TARGET } else { "" }
15
15
  $Mode = if ($env:HELLOAGENTS_MODE) { $env:HELLOAGENTS_MODE } else { "" }
16
16
  $Branch = if ($env:HELLOAGENTS_BRANCH) { $env:HELLOAGENTS_BRANCH } else { "" }
17
17
  $Package = if ($env:HELLOAGENTS_PACKAGE) { $env:HELLOAGENTS_PACKAGE } else { "" }
18
+ $HasExplicitPackage = [bool]$Package
18
19
 
19
20
  if ($env:HELLOAGENTS) {
20
21
  $Parts = $env:HELLOAGENTS.Split(":", 2)
@@ -53,6 +54,23 @@ function Invoke-Npm {
53
54
  }
54
55
  }
55
56
 
57
+ function Clear-HelloagentsEnv {
58
+ foreach ($name in @(
59
+ "HELLOAGENTS",
60
+ "HELLOAGENTS_ACTION",
61
+ "HELLOAGENTS_TARGET",
62
+ "HELLOAGENTS_HOST",
63
+ "HELLOAGENTS_MODE",
64
+ "HELLOAGENTS_BRANCH",
65
+ "HELLOAGENTS_PACKAGE",
66
+ "HELLOAGENTS_DEPLOY"
67
+ )) {
68
+ Remove-Item "Env:$name" -ErrorAction SilentlyContinue
69
+ }
70
+ }
71
+
72
+ Clear-HelloagentsEnv
73
+
56
74
  function Enable-PostinstallDeploy {
57
75
  $env:HELLOAGENTS_DEPLOY = "1"
58
76
  $env:HELLOAGENTS_TARGET = $Target
@@ -93,7 +111,7 @@ switch ($Action) {
93
111
  Invoke-Npm -NpmArgs @("install", "-g", $Package)
94
112
  }
95
113
  "update" {
96
- if ($Branch -or $env:HELLOAGENTS_PACKAGE) {
114
+ if ($Branch -or $HasExplicitPackage) {
97
115
  Invoke-Npm -NpmArgs @("install", "-g", $Package)
98
116
  } else {
99
117
  & npm update -g helloagents
@@ -107,14 +125,14 @@ switch ($Action) {
107
125
  Cleanup-Hosts
108
126
  }
109
127
  "switch-branch" {
110
- if (-not $Branch -and -not $env:HELLOAGENTS_PACKAGE) {
128
+ if (-not $Branch -and -not $HasExplicitPackage) {
111
129
  throw "HELLOAGENTS_BRANCH or HELLOAGENTS_PACKAGE is required for switch-branch"
112
130
  }
113
131
  Invoke-Npm -NpmArgs @("install", "-g", $Package)
114
132
  Sync-Hosts
115
133
  }
116
134
  "branch" {
117
- if (-not $Branch -and -not $env:HELLOAGENTS_PACKAGE) {
135
+ if (-not $Branch -and -not $HasExplicitPackage) {
118
136
  throw "HELLOAGENTS_BRANCH or HELLOAGENTS_PACKAGE is required for branch"
119
137
  }
120
138
  Invoke-Npm -NpmArgs @("install", "-g", $Package)
package/install.sh CHANGED
@@ -16,6 +16,10 @@ TARGET="${HELLOAGENTS_TARGET:-}"
16
16
  MODE="${HELLOAGENTS_MODE:-}"
17
17
  BRANCH="${HELLOAGENTS_BRANCH:-}"
18
18
  PACKAGE="${HELLOAGENTS_PACKAGE:-}"
19
+ HAS_EXPLICIT_PACKAGE=0
20
+ if [ -n "$PACKAGE" ]; then
21
+ HAS_EXPLICIT_PACKAGE=1
22
+ fi
19
23
 
20
24
  if [ -n "${HELLOAGENTS:-}" ]; then
21
25
  SPEC_TARGET="${HELLOAGENTS%%:*}"
@@ -55,6 +59,19 @@ if [ -z "$PACKAGE" ]; then
55
59
  fi
56
60
  fi
57
61
 
62
+ clear_lifecycle_env() {
63
+ unset HELLOAGENTS
64
+ unset HELLOAGENTS_ACTION
65
+ unset HELLOAGENTS_TARGET
66
+ unset HELLOAGENTS_HOST
67
+ unset HELLOAGENTS_MODE
68
+ unset HELLOAGENTS_BRANCH
69
+ unset HELLOAGENTS_PACKAGE
70
+ unset HELLOAGENTS_DEPLOY
71
+ }
72
+
73
+ clear_lifecycle_env
74
+
58
75
  sync_hosts() {
59
76
  if [ "$TARGET" = "all" ]; then
60
77
  if [ -n "$MODE" ]; then
@@ -115,7 +132,7 @@ case "$ACTION" in
115
132
  npm install -g "$PACKAGE"
116
133
  ;;
117
134
  update)
118
- if [ -n "$BRANCH" ] || [ -n "${HELLOAGENTS_PACKAGE:-}" ]; then
135
+ if [ -n "$BRANCH" ] || [ "$HAS_EXPLICIT_PACKAGE" -eq 1 ]; then
119
136
  npm install -g "$PACKAGE"
120
137
  else
121
138
  npm update -g helloagents || npm install -g helloagents
@@ -126,7 +143,7 @@ case "$ACTION" in
126
143
  cleanup_hosts
127
144
  ;;
128
145
  switch-branch|branch)
129
- if [ -z "$BRANCH" ] && [ -z "${HELLOAGENTS_PACKAGE:-}" ]; then
146
+ if [ -z "$BRANCH" ] && [ "$HAS_EXPLICIT_PACKAGE" -ne 1 ]; then
130
147
  echo "HELLOAGENTS_BRANCH or HELLOAGENTS_PACKAGE is required for switch-branch" >&2
131
148
  exit 1
132
149
  fi
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "helloagents",
3
- "version": "3.0.33",
3
+ "version": "3.0.35",
4
4
  "type": "module",
5
- "description": "HelloAGENTS — The orchestration kernel that makes any AI CLI smarter. Adds intelligent routing, quality verification (Ralph Loop), safety guards, and notifications.",
5
+ "description": "HelloAGENTS — The orchestration kernel that makes any AI CLI smarter. Adds intelligent routing, unified QA gates, safety guards, and notifications.",
6
6
  "author": "HelloWind",
7
7
  "license": "Apache-2.0",
8
8
  "homepage": "https://github.com/hellowind777/helloagents",
@@ -30,10 +30,12 @@ export function selectCapabilities({ cwd, skillName = '', options = {} }) {
30
30
  : '独立 advisor:当前契约要求进入收尾前写当前会话 `artifacts/advisor.json`,记录 advisor reason、focus、consultedSources 与结论。',
31
31
  })
32
32
  }
33
- if (plan?.contract?.verifyMode === 'review-first') {
33
+ if ((plan?.contract?.qaFocus || []).length > 0) {
34
34
  capabilities.push({
35
- id: 'review-evaluator',
36
- description: '审查优先:当前验证主路径是 review-first,先做 hello-review,再做 hello-verify。',
35
+ id: 'qa-evaluator',
36
+ description: plan?.contract?.qaMode === 'deep'
37
+ ? `深度 qa-review:当前收尾主路径是 ~qa,按阻断性质量闭环执行;重点:${plan.contract.qaFocus.join(';')}。`
38
+ : `统一 qa-review:当前收尾主路径是 ~qa,重点:${plan.contract.qaFocus.join(';')}。`,
37
39
  })
38
40
  }
39
41
  if (plan?.contract?.ui?.required || existsSync(getProjectDesignContractPath(cwd))) {
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from 'node:child_process'
1
2
  import { existsSync, realpathSync } from 'node:fs'
2
3
  import { join } from 'node:path'
3
4
 
@@ -76,6 +77,141 @@ function buildDoctorIssue(runtime, code, cn, en) {
76
77
  }
77
78
  }
78
79
 
80
+ function normalizeDoctorText(value = '') {
81
+ return String(value || '').replace(/\s+/g, ' ').trim()
82
+ }
83
+
84
+ function readFirstInteger(value = '') {
85
+ const match = String(value || '').match(/-?\d+/)
86
+ return match ? Number.parseInt(match[0], 10) : null
87
+ }
88
+
89
+ function readNativeDoctorDetail(checks, checkId, detailKey) {
90
+ return String(checks?.[checkId]?.details?.[detailKey] || '').trim()
91
+ }
92
+
93
+ function readNativeDoctorList(value = '') {
94
+ const normalized = normalizeDoctorText(value)
95
+ if (!normalized || normalized === '(none)') return []
96
+ return normalized.split(/\s*,\s*/).map((entry) => entry.trim()).filter(Boolean)
97
+ }
98
+
99
+ function summarizeNativeCodexDoctor(payload = {}) {
100
+ const checks = payload?.checks || {}
101
+ const configCheck = checks['config.load'] || {}
102
+ const sandboxCheck = checks['sandbox.helpers'] || {}
103
+ const mcpCount = readFirstInteger(readNativeDoctorDetail(checks, 'config.load', 'mcp servers'))
104
+ const fsSandbox = readNativeDoctorDetail(checks, 'sandbox.helpers', 'filesystem sandbox').toLowerCase()
105
+ const linuxHelper = readNativeDoctorDetail(checks, 'sandbox.helpers', 'codex-linux-sandbox helper').toLowerCase()
106
+ || readNativeDoctorDetail(checks, 'sandbox.helpers', 'linux helper').toLowerCase()
107
+ const execveHelper = readNativeDoctorDetail(checks, 'sandbox.helpers', 'execve wrapper helper').toLowerCase()
108
+
109
+ let sandboxAvailable = null
110
+ if (sandboxCheck && Object.keys(sandboxCheck).length > 0) {
111
+ sandboxAvailable = Boolean(
112
+ (fsSandbox && !fsSandbox.includes('unrestricted'))
113
+ || (linuxHelper && linuxHelper !== 'none')
114
+ || (execveHelper && execveHelper !== 'none')
115
+ )
116
+ }
117
+
118
+ return {
119
+ version: String(payload?.codexVersion || '').trim(),
120
+ configPath: readNativeDoctorDetail(checks, 'config.load', 'config.toml'),
121
+ resolvedProvider: readNativeDoctorDetail(checks, 'config.load', 'model provider'),
122
+ resolvedModel: readNativeDoctorDetail(checks, 'config.load', 'model'),
123
+ sandboxAvailable,
124
+ mcpPresent: typeof mcpCount === 'number' ? mcpCount > 0 : false,
125
+ skillsSelected: readNativeDoctorList(
126
+ readNativeDoctorDetail(checks, 'config.load', 'selected skills')
127
+ || readNativeDoctorDetail(checks, 'config.load', 'skills selected')
128
+ ),
129
+ }
130
+ }
131
+
132
+ function summarizeNativeCodexDoctorOutput(payload = {}) {
133
+ const checks = Object.values(payload?.checks || {})
134
+ const failedCheck = checks.find((check) => check?.status === 'fail')
135
+ if (failedCheck?.issues?.length) {
136
+ return normalizeDoctorText(failedCheck.issues.map((issue) => issue?.cause || issue?.measured || '').filter(Boolean).join(' | '))
137
+ }
138
+ if (failedCheck?.summary) return normalizeDoctorText(failedCheck.summary)
139
+
140
+ const warningCheck = checks.find((check) => check?.status === 'warn')
141
+ if (warningCheck?.issues?.length) {
142
+ return normalizeDoctorText(warningCheck.issues.map((issue) => issue?.cause || issue?.measured || '').filter(Boolean).join(' | '))
143
+ }
144
+ if (warningCheck?.summary) return normalizeDoctorText(warningCheck.summary)
145
+
146
+ return ''
147
+ }
148
+
149
+ function inspectNativeCodexDoctor(runtime) {
150
+ try {
151
+ const result = spawnSync('codex', ['doctor', '--json'], {
152
+ cwd: process.cwd(),
153
+ env: {
154
+ ...process.env,
155
+ HOME: runtime.home || process.env.HOME,
156
+ USERPROFILE: runtime.home || process.env.USERPROFILE,
157
+ NO_COLOR: process.env.NO_COLOR || '1',
158
+ },
159
+ encoding: 'utf-8',
160
+ timeout: 20_000,
161
+ windowsHide: true,
162
+ })
163
+
164
+ if (result.error) {
165
+ return {
166
+ available: false,
167
+ ok: false,
168
+ status: '',
169
+ summary: null,
170
+ output: normalizeDoctorText(result.error.message || ''),
171
+ }
172
+ }
173
+
174
+ const stdout = String(result.stdout || '').trim()
175
+ if (!stdout) {
176
+ return {
177
+ available: true,
178
+ ok: result.status === 0,
179
+ status: '',
180
+ summary: null,
181
+ output: normalizeDoctorText(result.stderr || ''),
182
+ }
183
+ }
184
+
185
+ try {
186
+ const payload = JSON.parse(stdout)
187
+ const status = String(payload?.overallStatus || '').trim().toLowerCase()
188
+ return {
189
+ available: true,
190
+ ok: status ? status !== 'fail' : result.status === 0,
191
+ status,
192
+ summary: summarizeNativeCodexDoctor(payload),
193
+ output: summarizeNativeCodexDoctorOutput(payload),
194
+ }
195
+ } catch {
196
+ return {
197
+ available: true,
198
+ ok: result.status === 0,
199
+ status: '',
200
+ summary: null,
201
+ output: normalizeDoctorText(stdout || result.stderr || ''),
202
+ }
203
+ }
204
+ } catch (error) {
205
+ return {
206
+ available: false,
207
+ ok: false,
208
+ status: '',
209
+ summary: null,
210
+ output: normalizeDoctorText(error?.message || ''),
211
+ }
212
+ }
213
+ }
214
+
79
215
  function normalizeDoctorMode(mode = '') {
80
216
  return mode || 'none'
81
217
  }
@@ -209,6 +345,7 @@ export function inspectCodexDoctor(runtime, settings) {
209
345
  const host = 'codex'
210
346
  const trackedMode = normalizeDoctorMode(runtime.getTrackedHostMode(settings, host))
211
347
  const detectedMode = normalizeDoctorMode(runtime.detectHostMode(host))
348
+ const nativeDoctor = inspectNativeCodexDoctor(runtime)
212
349
  const { checks, pluginVersion, cacheVersion } = buildCodexChecks(runtime, settings, trackedMode, detectedMode)
213
350
  checks.pluginVersionMatch = pluginVersion ? pluginVersion === runtime.pkgVersion : false
214
351
  checks.pluginCacheVersionMatch = cacheVersion ? cacheVersion === runtime.pkgVersion : false
@@ -230,7 +367,19 @@ export function inspectCodexDoctor(runtime, settings) {
230
367
  if (!checks.pluginCacheVersionMatch && !cacheVersion && detectedMode === 'global') notes.push(runtime.msg('未读到 global 插件缓存版本信息', 'Global plugin cache version was not readable'))
231
368
  if (detectedMode !== 'none' && !checks.codexGoalsFeature) notes.push(runtime.msg('Codex /goal 未启用;如需长程执行,可运行 `helloagents codex goals enable`。', 'Codex /goal is not enabled; run `helloagents codex goals enable` if you need long-running goals.'))
232
369
  if (detectedMode !== 'none' && checks.legacyCodexHooksFeature) notes.push(runtime.msg('检测到旧版 `codex_hooks`;HelloAGENTS 只兼容 Codex 最新版,请移除旧 key。', 'Legacy `codex_hooks` was detected; HelloAGENTS targets latest Codex only, so remove the old key.'))
370
+ if (!nativeDoctor.available) notes.push(runtime.msg('未检测到原生 `codex doctor`;当前仅检查 HelloAGENTS 受管覆盖层。', 'Native `codex doctor` was not available; only the HelloAGENTS managed overlay was checked.'))
233
371
 
234
372
  const status = summarizeDoctorStatus(issues, { trackedMode, detectedMode })
235
- return { host, label: runtime.getHostLabel(host), trackedMode, detectedMode, status, checks, issues, notes, suggestedFix: suggestCodexDoctorFix(status, trackedMode) }
373
+ return {
374
+ host,
375
+ label: runtime.getHostLabel(host),
376
+ trackedMode,
377
+ detectedMode,
378
+ status,
379
+ checks,
380
+ nativeDoctor,
381
+ issues,
382
+ notes,
383
+ suggestedFix: suggestCodexDoctorFix(status, trackedMode),
384
+ }
236
385
  }
@@ -20,7 +20,8 @@ export function printDoctorText(runtime, report) {
20
20
  if (entry.nativeDoctor) {
21
21
  console.log(` native_doctor.available: ${entry.nativeDoctor.available ? 'ok' : 'missing'}`)
22
22
  if (entry.nativeDoctor.available) {
23
- console.log(` native_doctor.ok: ${entry.nativeDoctor.ok ? 'ok' : 'missing'}`)
23
+ console.log(` native_doctor.ok: ${entry.nativeDoctor.ok ? 'ok' : 'fail'}`)
24
+ if (entry.nativeDoctor.status) console.log(` native_doctor.status: ${entry.nativeDoctor.status}`)
24
25
  }
25
26
  if (entry.nativeDoctor.summary) {
26
27
  if (entry.nativeDoctor.summary.version) console.log(` native_doctor.version: ${entry.nativeDoctor.summary.version}`)
@@ -46,6 +46,13 @@ function buildNativeResult(result, successCN, successEN, manualCN, manualEN) {
46
46
  }
47
47
  }
48
48
 
49
+ function preserveTrackedModeOnFailure(result = {}, trackedMode = '') {
50
+ if (result.ok === false && trackedMode) {
51
+ return { ...result, trackedModeOnFailure: trackedMode }
52
+ }
53
+ return result
54
+ }
55
+
49
56
  function installClaudeGlobalPlugin() {
50
57
  const add = runHostCommand(CLAUDE_COMMAND, ['plugin', 'marketplace', 'add', CLAUDE_MARKETPLACE])
51
58
  if (!add.ok && add.missing) return { ok: false, output: '未找到 claude 命令' }
@@ -70,8 +77,11 @@ function reportHostAction(runtime, action, host, mode, result = {}) {
70
77
  const isCleanup = action === 'cleanup' || action === 'uninstall'
71
78
  if (result.skipped) {
72
79
  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`))
80
+ } else if (result.ok === false) {
81
+ console.log(runtime.msg(
82
+ isCleanup ? ` - ${label} 自动清理未完成` : ` - ${label} 自动配置未完成`,
83
+ isCleanup ? ` - ${label} automatic cleanup did not complete` : ` - ${label} automatic setup did not complete`,
84
+ ))
75
85
  } else if (isCleanup) {
76
86
  runtime.ok(runtime.msg(`${label} 已清理(${mode} 模式)`, `${label} cleaned (${mode} mode)`))
77
87
  } else if (mode === 'standby') {
@@ -87,14 +97,46 @@ function reportHostAction(runtime, action, host, mode, result = {}) {
87
97
  }
88
98
  }
89
99
 
90
- function installHostStandby(runtime, host) {
100
+ function prepareClaudeStandby(previousMode) {
101
+ if (previousMode !== 'global') return {}
102
+ return preserveTrackedModeOnFailure(
103
+ buildNativeResult(
104
+ removeClaudeGlobalPlugin(),
105
+ '已自动移除 Claude Code 插件',
106
+ 'Claude Code plugin removed automatically',
107
+ '切到 standby 前无法自动移除 Claude Code 插件,请先在 Claude Code 中执行: /plugin remove helloagents',
108
+ 'Could not remove the Claude Code plugin before switching to standby. Run inside Claude Code: /plugin remove helloagents',
109
+ ),
110
+ 'global',
111
+ )
112
+ }
113
+
114
+ function prepareGeminiStandby(previousMode) {
115
+ if (previousMode !== 'global') return {}
116
+ return preserveTrackedModeOnFailure(
117
+ buildNativeResult(
118
+ removeGeminiGlobalExtension(),
119
+ '已自动移除 Gemini CLI 扩展',
120
+ 'Gemini CLI extension removed automatically',
121
+ '切到 standby 前无法自动移除 Gemini CLI 扩展,请先手动执行: gemini extensions uninstall helloagents',
122
+ 'Could not remove the Gemini CLI extension before switching to standby. Run manually: gemini extensions uninstall helloagents',
123
+ ),
124
+ 'global',
125
+ )
126
+ }
127
+
128
+ function installHostStandby(runtime, host, { previousMode = '' } = {}) {
91
129
  if (host === 'claude') {
130
+ const cleanupResult = prepareClaudeStandby(previousMode)
131
+ if (cleanupResult.ok === false) return cleanupResult
92
132
  installClaudeStandby(runtime.home, runtime.pkgRoot)
93
- return {}
133
+ return cleanupResult
94
134
  }
95
135
  if (host === 'gemini') {
136
+ const cleanupResult = prepareGeminiStandby(previousMode)
137
+ if (cleanupResult.ok === false) return cleanupResult
96
138
  installGeminiStandby(runtime.home, runtime.pkgRoot)
97
- return {}
139
+ return cleanupResult
98
140
  }
99
141
  if (!installCodexStandby(runtime.home, runtime.pkgRoot)) return { skipped: true }
100
142
  cleanupCodexGlobalResidueForStandby(runtime.home)
@@ -137,41 +179,41 @@ function cleanupHostStandby(runtime, host) {
137
179
  function cleanupHostGlobal(runtime, host) {
138
180
  if (host === 'claude') {
139
181
  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',
182
+ return preserveTrackedModeOnFailure(
183
+ buildNativeResult(
184
+ removeClaudeGlobalPlugin(),
185
+ '已自动移除 Claude Code 插件',
186
+ 'Claude Code plugin removed automatically',
187
+ 'Claude Code 插件自动移除失败,请手动执行: /plugin remove helloagents',
188
+ 'Claude Code plugin auto-remove failed. Run manually: /plugin remove helloagents',
189
+ ),
190
+ 'global',
146
191
  )
147
192
  }
148
193
  if (host === 'gemini') {
149
194
  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',
195
+ return preserveTrackedModeOnFailure(
196
+ buildNativeResult(
197
+ removeGeminiGlobalExtension(),
198
+ '已自动移除 Gemini CLI 扩展',
199
+ 'Gemini CLI extension removed automatically',
200
+ 'Gemini CLI 扩展自动移除失败,请手动执行: gemini extensions uninstall helloagents',
201
+ 'Gemini CLI extension auto-remove failed. Run manually: gemini extensions uninstall helloagents',
202
+ ),
203
+ 'global',
156
204
  )
157
205
  }
158
206
  return { skipped: !uninstallCodexGlobal(runtime.home) }
159
207
  }
160
208
 
161
- function installStandby(runtime) {
209
+ function installStandby(runtime, previousModes = {}) {
162
210
  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
- }
211
+ const claudeResult = installHostStandby(runtime, 'claude', { previousMode: previousModes.claude || '' })
212
+ reportHostAction(runtime, 'install', 'claude', 'standby', claudeResult)
213
+ results.claude = claudeResult.skipped ? { skipped: true } : claudeResult
214
+ const geminiResult = installHostStandby(runtime, 'gemini', { previousMode: previousModes.gemini || '' })
215
+ reportHostAction(runtime, 'install', 'gemini', 'standby', geminiResult)
216
+ results.gemini = geminiResult.skipped ? { skipped: true } : geminiResult
175
217
  if (installCodexStandby(runtime.home, runtime.pkgRoot)) {
176
218
  cleanupCodexGlobalResidueForStandby(runtime.home)
177
219
  runtime.ok(runtime.msg('Codex CLI 已配置(standby 模式)', 'Codex CLI configured (standby mode)'))
@@ -193,9 +235,9 @@ function installGlobal(runtime) {
193
235
  return results
194
236
  }
195
237
 
196
- export function installAllHosts(runtime, mode) {
238
+ export function installAllHosts(runtime, mode, { previousModes = {} } = {}) {
197
239
  if (mode === 'global') return installGlobal(runtime)
198
- return installStandby(runtime)
240
+ return installStandby(runtime, previousModes)
199
241
  }
200
242
 
201
243
  export function uninstallAllHosts(runtime) {
@@ -205,10 +247,10 @@ export function uninstallAllHosts(runtime) {
205
247
  uninstallCodexGlobal(runtime.home)
206
248
  }
207
249
 
208
- export function runHostLifecycle(runtime, action, host, mode) {
250
+ export function runHostLifecycle(runtime, action, host, mode, options = {}) {
209
251
  const result = (action === 'cleanup' || action === 'uninstall')
210
252
  ? (mode === 'global' ? cleanupHostGlobal(runtime, host) : cleanupHostStandby(runtime, host))
211
- : (mode === 'global' ? installHostGlobal(runtime, host) : installHostStandby(runtime, host))
253
+ : (mode === 'global' ? installHostGlobal(runtime, host) : installHostStandby(runtime, host, options))
212
254
 
213
255
  reportHostAction(runtime, action, host, mode, result)
214
256
  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) {
@@ -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')}: