helloagents 3.0.2-beta.1 → 3.0.7

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 (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +147 -45
  5. package/README_CN.md +148 -46
  6. package/bootstrap-lite.md +104 -46
  7. package/bootstrap.md +143 -112
  8. package/cli.mjs +80 -427
  9. package/gemini-extension.json +1 -1
  10. package/hooks/hooks-claude.json +10 -0
  11. package/hooks/hooks.json +10 -0
  12. package/package.json +2 -12
  13. package/scripts/advisor-state.mjs +222 -0
  14. package/scripts/capability-registry.mjs +59 -0
  15. package/scripts/cli-codex-backup.mjs +59 -0
  16. package/scripts/cli-codex-config.mjs +100 -0
  17. package/scripts/cli-codex.mjs +34 -156
  18. package/scripts/cli-config.mjs +1 -0
  19. package/scripts/cli-doctor-render.mjs +28 -0
  20. package/scripts/cli-doctor.mjs +367 -0
  21. package/scripts/cli-host-detect.mjs +94 -0
  22. package/scripts/cli-lifecycle-hosts.mjs +123 -0
  23. package/scripts/cli-lifecycle.mjs +213 -0
  24. package/scripts/cli-messages.mjs +76 -52
  25. package/scripts/closeout-state.mjs +213 -0
  26. package/scripts/delivery-gate.mjs +256 -0
  27. package/scripts/guard-rules.mjs +122 -0
  28. package/scripts/guard.mjs +190 -168
  29. package/scripts/notify-context.mjs +77 -17
  30. package/scripts/notify-events.mjs +5 -1
  31. package/scripts/notify-route.mjs +111 -0
  32. package/scripts/notify-shared.mjs +0 -2
  33. package/scripts/notify-source.mjs +113 -0
  34. package/scripts/notify-ui.mjs +40 -6
  35. package/scripts/notify.mjs +120 -59
  36. package/scripts/plan-contract.mjs +210 -0
  37. package/scripts/project-storage.mjs +235 -0
  38. package/scripts/ralph-loop.mjs +9 -58
  39. package/scripts/replay-state.mjs +210 -0
  40. package/scripts/review-state.mjs +220 -0
  41. package/scripts/runtime-context.mjs +74 -0
  42. package/scripts/verify-state.mjs +226 -0
  43. package/scripts/visual-state.mjs +244 -0
  44. package/scripts/workflow-core.mjs +165 -0
  45. package/scripts/workflow-plan-files.mjs +249 -0
  46. package/scripts/workflow-recommendation.mjs +335 -0
  47. package/scripts/workflow-state.mjs +113 -0
  48. package/skills/commands/auto/SKILL.md +37 -71
  49. package/skills/commands/build/SKILL.md +67 -0
  50. package/skills/commands/clean/SKILL.md +10 -8
  51. package/skills/commands/commit/SKILL.md +8 -4
  52. package/skills/commands/help/SKILL.md +19 -11
  53. package/skills/commands/idea/SKILL.md +55 -0
  54. package/skills/commands/init/SKILL.md +6 -3
  55. package/skills/commands/loop/SKILL.md +6 -5
  56. package/skills/commands/plan/SKILL.md +116 -0
  57. package/skills/commands/prd/SKILL.md +20 -15
  58. package/skills/commands/verify/SKILL.md +32 -9
  59. package/skills/commands/wiki/SKILL.md +59 -0
  60. package/skills/hello-review/SKILL.md +9 -0
  61. package/skills/hello-subagent/SKILL.md +4 -3
  62. package/skills/hello-ui/SKILL.md +36 -8
  63. package/skills/hello-verify/SKILL.md +10 -2
  64. package/skills/helloagents/SKILL.md +24 -13
  65. package/templates/DESIGN.md +25 -4
  66. package/templates/STATE.md +3 -0
  67. package/templates/plans/contract.json +48 -0
  68. package/templates/plans/plan.md +23 -0
  69. package/templates/plans/tasks.md +3 -3
  70. package/skills/commands/design/SKILL.md +0 -108
  71. package/skills/commands/review/SKILL.md +0 -16
  72. package/templates/plans/design.md +0 -14
@@ -0,0 +1,213 @@
1
+ import { existsSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import { DEFAULTS, ensureConfig } from './cli-config.mjs'
5
+ import {
6
+ detectHostMode as detectRuntimeHostMode,
7
+ getHostLabel as resolveHostLabel,
8
+ normalizeHost as normalizeLifecycleHost,
9
+ } from './cli-host-detect.mjs'
10
+ import { installAllHosts, runHostLifecycle, uninstallAllHosts } from './cli-lifecycle-hosts.mjs'
11
+ import { ensureDir, safeJson, safeWrite } from './cli-utils.mjs'
12
+
13
+ export const HOSTS = ['claude', 'gemini', 'codex']
14
+
15
+ const runtime = {
16
+ home: '',
17
+ pkgRoot: '',
18
+ helloagentsHome: '',
19
+ configFile: '',
20
+ pkgVersion: '',
21
+ msg: (cn, en) => en || cn,
22
+ ok: console.log,
23
+ printInstallMsg: () => {},
24
+ }
25
+
26
+ export function initCliLifecycle(options) {
27
+ Object.assign(runtime, options)
28
+ }
29
+
30
+ export function readSettings(shouldEnsure = false) {
31
+ if (shouldEnsure) ensureConfig(runtime.helloagentsHome, runtime.configFile, safeJson, ensureDir)
32
+ return safeJson(runtime.configFile) || {}
33
+ }
34
+
35
+ function writeSettings(settings) {
36
+ ensureDir(runtime.helloagentsHome)
37
+ writeFileSync(runtime.configFile, JSON.stringify(settings, null, 2), 'utf-8')
38
+ }
39
+
40
+ function hasTrackedHostModes(settings) {
41
+ return !!settings && typeof settings.host_install_modes === 'object' && !Array.isArray(settings.host_install_modes)
42
+ }
43
+
44
+ export function getTrackedHostMode(settings, host) {
45
+ return hasTrackedHostModes(settings) ? settings.host_install_modes[host] || '' : ''
46
+ }
47
+
48
+ function setTrackedHostMode(settings, host, mode) {
49
+ if (!hasTrackedHostModes(settings)) settings.host_install_modes = {}
50
+ settings.host_install_modes[host] = mode
51
+ }
52
+
53
+ function clearTrackedHostMode(settings, host) {
54
+ if (!hasTrackedHostModes(settings)) {
55
+ settings.host_install_modes = {}
56
+ return
57
+ }
58
+ delete settings.host_install_modes[host]
59
+ }
60
+
61
+ function setAllTrackedHostModes(settings, mode) {
62
+ settings.host_install_modes = Object.fromEntries(HOSTS.map((host) => [host, mode]))
63
+ }
64
+
65
+ function clearAllTrackedHostModes(settings) {
66
+ settings.host_install_modes = {}
67
+ }
68
+
69
+ export function normalizeHost(value = '') {
70
+ return normalizeLifecycleHost(value)
71
+ }
72
+
73
+ function parseModeFlag(args) {
74
+ const hasGlobal = args.includes('--global')
75
+ const hasStandby = args.includes('--standby')
76
+ if (hasGlobal && hasStandby) {
77
+ throw new Error(runtime.msg('不能同时指定 --global 和 --standby', 'Cannot use --global and --standby together'))
78
+ }
79
+ if (hasGlobal) return 'global'
80
+ if (hasStandby) return 'standby'
81
+ return ''
82
+ }
83
+
84
+ function parseLifecycleArgs(args) {
85
+ const explicitMode = parseModeFlag(args)
86
+ const wantsAll = args.includes('--all')
87
+ const positionals = args.filter((arg) => !arg.startsWith('--'))
88
+ const unknownFlags = args.filter((arg) => arg.startsWith('--') && !['--global', '--standby', '--all'].includes(arg))
89
+ if (unknownFlags.length) {
90
+ throw new Error(runtime.msg(`未知参数: ${unknownFlags.join(', ')}`, `Unknown flags: ${unknownFlags.join(', ')}`))
91
+ }
92
+ if (wantsAll && positionals.length) {
93
+ throw new Error(runtime.msg('`--all` 不能与具体 CLI 同时使用', '`--all` cannot be combined with a specific CLI'))
94
+ }
95
+ if (positionals.length > 1) {
96
+ throw new Error(runtime.msg(`参数过多: ${positionals.join(' ')}`, `Too many arguments: ${positionals.join(' ')}`))
97
+ }
98
+ const host = normalizeLifecycleHost(wantsAll ? 'all' : (positionals[0] || 'all'))
99
+ if (!host) {
100
+ throw new Error(runtime.msg(`不支持的 CLI: ${positionals[0]}`, `Unsupported CLI: ${positionals[0]}`))
101
+ }
102
+ return { host, explicitMode }
103
+ }
104
+
105
+ export function detectHostMode(host) {
106
+ return detectRuntimeHostMode(host, runtime)
107
+ }
108
+
109
+ export function getHostLabel(host) {
110
+ return resolveHostLabel(host)
111
+ }
112
+
113
+ function resolveHostMode(host, explicitMode, settings) {
114
+ if (explicitMode) return explicitMode
115
+ return detectHostMode(host)
116
+ || getTrackedHostMode(settings, host)
117
+ || (!hasTrackedHostModes(settings) ? (settings.install_mode || '') : '')
118
+ || DEFAULTS.install_mode
119
+ }
120
+
121
+ function resolveInstallMode(explicitMode, settings) {
122
+ return explicitMode || settings.install_mode || DEFAULTS.install_mode
123
+ }
124
+
125
+
126
+ export function syncVersion() {
127
+ const targets = [
128
+ join(runtime.pkgRoot, '.claude-plugin', 'plugin.json'),
129
+ join(runtime.pkgRoot, '.codex-plugin', 'plugin.json'),
130
+ join(runtime.pkgRoot, 'gemini-extension.json'),
131
+ ]
132
+ for (const path of targets) {
133
+ const obj = safeJson(path)
134
+ if (!obj) continue
135
+ obj.version = runtime.pkgVersion
136
+ safeWrite(path, JSON.stringify(obj, null, 2) + '\n')
137
+ }
138
+ const marketPath = join(runtime.pkgRoot, '.claude-plugin', 'marketplace.json')
139
+ const market = safeJson(marketPath)
140
+ if (market?.plugins?.[0]) {
141
+ market.plugins[0].version = runtime.pkgVersion
142
+ safeWrite(marketPath, JSON.stringify(market, null, 2) + '\n')
143
+ }
144
+ runtime.ok(`Version synced to ${runtime.pkgVersion}`)
145
+ }
146
+
147
+ export function switchMode(newMode) {
148
+ const config = readSettings(true)
149
+ const oldMode = config.install_mode || DEFAULTS.install_mode
150
+ const isRefresh = oldMode === newMode
151
+
152
+ if (!isRefresh) {
153
+ config.install_mode = newMode
154
+ runtime.ok(runtime.msg(`模式已切换为: ${newMode}`, `Mode switched to: ${newMode}`))
155
+ } else {
156
+ runtime.ok(runtime.msg(`当前已是 ${newMode} 模式,正在刷新安装`, `Already in ${newMode} mode, refreshing installation`))
157
+ }
158
+
159
+ installAllHosts(runtime, newMode)
160
+ setAllTrackedHostModes(config, newMode)
161
+ writeSettings(config)
162
+ runtime.printInstallMsg(newMode, isRefresh ? 'refresh' : 'switch')
163
+ }
164
+
165
+ function runAllHostsLifecycle(action, explicitMode) {
166
+ if (action === 'cleanup' || action === 'uninstall') {
167
+ console.log(`\n HelloAGENTS — ${runtime.msg('正在清理', 'Cleaning up')}\n`)
168
+ uninstallAllHosts(runtime)
169
+ if (existsSync(runtime.configFile)) {
170
+ const settings = readSettings()
171
+ clearAllTrackedHostModes(settings)
172
+ writeSettings(settings)
173
+ }
174
+ runtime.ok(runtime.msg('所有 CLI 配置已清理', 'All CLI configurations cleaned'))
175
+ console.log(runtime.msg(
176
+ ' ℹ ~/.helloagents/ 已保留(如需彻底清理请手动删除)\n ℹ 如已安装 Claude Code 插件,请手动执行: /plugin remove helloagents\n ℹ 如已安装 Gemini CLI 扩展,请手动执行: gemini extensions uninstall helloagents',
177
+ ' ℹ ~/.helloagents/ preserved (delete manually if desired)\n ℹ If Claude Code plugin installed, run: /plugin remove helloagents\n ℹ If Gemini CLI extension installed, run: gemini extensions uninstall helloagents',
178
+ ))
179
+ console.log()
180
+ return
181
+ }
182
+
183
+ const settings = readSettings(true)
184
+ const mode = resolveInstallMode(explicitMode, settings)
185
+ if (explicitMode) settings.install_mode = explicitMode
186
+ installAllHosts(runtime, mode)
187
+ setAllTrackedHostModes(settings, mode)
188
+ writeSettings(settings)
189
+ runtime.printInstallMsg(mode, action === 'update' ? 'refresh' : 'install')
190
+ }
191
+
192
+ export function runScopedLifecycle(action, rawArgs) {
193
+ const { host, explicitMode } = parseLifecycleArgs(rawArgs)
194
+ if (host === 'all') {
195
+ runAllHostsLifecycle(action, explicitMode)
196
+ return
197
+ }
198
+
199
+ const shouldEnsure = action === 'install' || action === 'update'
200
+ const settings = readSettings(shouldEnsure)
201
+ const mode = resolveHostMode(host, explicitMode, settings)
202
+ const result = runHostLifecycle(runtime, action, host, mode)
203
+
204
+ if (action === 'cleanup' || action === 'uninstall') {
205
+ if (existsSync(runtime.configFile)) {
206
+ clearTrackedHostMode(settings, host)
207
+ writeSettings(settings)
208
+ }
209
+ } else if (!result.skipped) {
210
+ setTrackedHostMode(settings, host, mode)
211
+ writeSettings(settings)
212
+ }
213
+ }
@@ -1,63 +1,76 @@
1
- import { existsSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
3
 
4
4
  export function createMessageHelpers(isCN) {
5
- const msg = (cn, en) => (isCN ? cn : en);
6
- const ok = (message) => console.log(` ✓ ${message}`);
7
- return { msg, ok };
5
+ const msg = (cn, en) => (isCN ? cn : en)
6
+ const ok = (message) => console.log(` ✓ ${message}`)
7
+ return { msg, ok }
8
8
  }
9
9
 
10
- export function createInstallMessagePrinter({ home, pkgVersion, msg }) {
11
- const codexStandbyStatus = () => existsSync(join(home, '.codex'))
10
+ function codexStandbyStatus({ home, msg }) {
11
+ return existsSync(join(home, '.codex'))
12
12
  ? msg('已自动配置', 'Auto-configured')
13
- : msg('安装 Codex CLI 后重新运行 npm install -g helloagents', 'Install Codex CLI then re-run npm install -g helloagents');
13
+ : msg('安装 Codex CLI 后重新运行 npm install -g helloagents', 'Install Codex CLI then re-run npm install -g helloagents')
14
+ }
14
15
 
15
- const codexGlobalStatus = () => existsSync(join(home, '.codex'))
16
+ function codexGlobalStatus({ home, msg }) {
17
+ return existsSync(join(home, '.codex'))
16
18
  ? msg('已自动安装原生本地插件', 'Native local plugin auto-installed')
17
- : msg('安装 Codex CLI 后重新运行 npm install -g helloagents', 'Install Codex CLI then re-run npm install -g helloagents');
19
+ : msg('安装 Codex CLI 后重新运行 npm install -g helloagents', 'Install Codex CLI then re-run npm install -g helloagents')
20
+ }
21
+
22
+ function pluginCommands() {
23
+ return ' Claude Code: /plugin marketplace add hellowind777/helloagents\n /plugin install helloagents@helloagents\n Gemini CLI: gemini extensions install https://github.com/hellowind777/helloagents'
24
+ }
18
25
 
19
- const PLUGIN_CMDS = ' Claude Code: /plugin marketplace add hellowind777/helloagents\n /plugin install helloagents@helloagents\n Gemini CLI: gemini extensions install https://github.com/hellowind777/helloagents';
20
- const REMOVE_HINT = msg(
26
+ function removeHint(msg) {
27
+ return msg(
21
28
  '如已安装 Claude Code 插件,建议手动移除: /plugin remove helloagents\n 如已安装 Gemini CLI 扩展,建议手动移除: gemini extensions uninstall helloagents',
22
29
  'If Claude Code plugin installed, consider removing: /plugin remove helloagents\n If Gemini CLI extension installed, consider removing: gemini extensions uninstall helloagents',
23
- );
24
-
25
- function printInstallMsg(mode, context) {
26
- const isSwitch = context === 'switch';
27
- const isRefresh = context === 'refresh';
28
- const isInstall = !isSwitch && !isRefresh;
29
- if (mode === 'global') {
30
- if (isInstall) console.log(msg(
31
- `\n HelloAGENTS 已安装(global 模式)!\n\n${PLUGIN_CMDS}\n Codex: ${codexGlobalStatus()}(~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n 切换模式:\n helloagents --standby 标准模式(默认,非插件安装)`,
32
- `\n ✅ HelloAGENTS installed (global mode)!\n\n${PLUGIN_CMDS}\n Codex: ${codexGlobalStatus()} (~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n Switch modes:\n helloagents --standby Standby mode (default, non-plugin install)`,
33
- ));
34
- else console.log(msg(
35
- isRefresh
36
- ? ' global 模式已刷新。\n Claude Code / Gemini 请保持插件已安装;Codex 原生本地插件链路已重装并同步最新文件。'
37
- : ' 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 请手动安装插件;Codex 已自动走原生本地插件链路。',
38
- isRefresh
39
- ? ' Global mode refreshed.\n Keep Claude Code / Gemini plugins installed; Codex native local-plugin files were reinstalled and synced.'
40
- : ' All projects will use full HelloAGENTS rules.\n Install Claude Code / Gemini plugins manually; Codex now uses the native local-plugin path automatically.',
41
- ));
42
- } else {
43
- if (isInstall) console.log(msg(
44
- `\n ✅ HelloAGENTS 已安装(standby 模式)!\n\n Claude Code: 已自动配置(~/.claude/CLAUDE.md + hooks)\n Gemini CLI: 已自动配置(~/.gemini/GEMINI.md)\n Codex: ${codexStandbyStatus()}\n\n standby 模式下,hello-* 技能不会自动触发。\n 在项目中使用 ~init 激活完整功能,或使用 ~command 按需调用。\n\n 切换模式:\n helloagents --global 全局模式(Claude/Gemini 装插件;Codex 自动装原生本地插件)`,
45
- `\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()}\n\n In standby mode, hello-* skills won't auto-trigger.\n Use ~init in a project to activate full features, or use ~command on demand.\n\n Switch modes:\n helloagents --global Global mode (manual plugins for Claude/Gemini; native local plugin auto-install for Codex)`,
46
- ));
47
- else console.log(msg(
48
- isRefresh
49
- ? ` standby 模式已刷新,CLI 注入与链接已同步最新文件。\n ${REMOVE_HINT}`
50
- : ` 项目需通过 ~init 激活完整功能,未激活项目仅注入通用规则。\n ${REMOVE_HINT}`,
51
- isRefresh
52
- ? ` Standby mode refreshed; injected files and links were synchronized.\n ${REMOVE_HINT}`
53
- : ` Projects need ~init to activate full features. Unactivated projects get lite rules only.\n ${REMOVE_HINT}`,
54
- ));
30
+ )
31
+ }
32
+
33
+ function renderInstallMessage(context, mode, state) {
34
+ const { msg } = context
35
+ const install = state === 'install'
36
+ const refresh = state === 'refresh'
37
+
38
+ if (mode === 'global') {
39
+ if (install) {
40
+ return msg(
41
+ `\n ✅ HelloAGENTS 已安装(global 模式)!\n\n${pluginCommands()}\n Codex: ${codexGlobalStatus(context)}(~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n 切换模式:\n helloagents --standby 标准模式(默认,非插件安装)`,
42
+ `\n ✅ HelloAGENTS installed (global mode)!\n\n${pluginCommands()}\n Codex: ${codexGlobalStatus(context)} (~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n Switch modes:\n helloagents --standby Standby mode (default, non-plugin install)`,
43
+ )
55
44
  }
56
- if (isInstall || isRefresh) console.log();
45
+ return msg(
46
+ refresh
47
+ ? ' global 模式已刷新。\n Claude Code / Gemini 请保持插件已安装;Codex 原生本地插件链路已重装并同步最新文件。'
48
+ : ' 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 请手动安装插件;Codex 已自动走原生本地插件链路。',
49
+ refresh
50
+ ? ' Global mode refreshed.\n Keep Claude Code / Gemini plugins installed; Codex native local-plugin files were reinstalled and synced.'
51
+ : ' All projects will use full HelloAGENTS rules.\n Install Claude Code / Gemini plugins manually; Codex now uses the native local-plugin path automatically.',
52
+ )
57
53
  }
58
54
 
59
- function printHelp() {
60
- console.log(`
55
+ if (install) {
56
+ return msg(
57
+ `\n ✅ HelloAGENTS 已安装(standby 模式)!\n\n Claude Code: 已自动配置(~/.claude/CLAUDE.md + hooks)\n Gemini CLI: 已自动配置(~/.gemini/GEMINI.md)\n Codex: ${codexStandbyStatus(context)}\n\n standby 模式下,hello-* 技能不会自动触发。\n 在项目中使用 ~wiki 仅创建/同步知识库,或用 ~init 完整初始化项目;也可用 ~command 按需调用。\n\n 切换模式:\n helloagents --global 全局模式(Claude/Gemini 装插件;Codex 自动装原生本地插件)`,
58
+ `\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 In standby mode, hello-* skills won't auto-trigger.\n Use ~wiki to create or sync the KB only, or ~init for the full project bootstrap; ~command stays available on demand.\n\n Switch modes:\n helloagents --global Global mode (manual plugins for Claude/Gemini; native local plugin auto-install for Codex)`,
59
+ )
60
+ }
61
+
62
+ return msg(
63
+ refresh
64
+ ? ` standby 模式已刷新,CLI 注入与链接已同步最新文件。\n ${removeHint(msg)}`
65
+ : ` 项目可通过 ~wiki 创建/同步知识库,或通过 ~init 完整初始化;未激活项目仅注入通用规则。\n ${removeHint(msg)}`,
66
+ refresh
67
+ ? ` Standby mode refreshed; injected files and links were synchronized.\n ${removeHint(msg)}`
68
+ : ` Projects can use ~wiki for KB-only activation or ~init for the full bootstrap. Unactivated projects get lite rules only.\n ${removeHint(msg)}`,
69
+ )
70
+ }
71
+
72
+ function renderHelp({ pkgVersion, msg }) {
73
+ return `
61
74
  HelloAGENTS v${pkgVersion} — The orchestration kernel for AI CLIs
62
75
 
63
76
  ${msg('安装', 'Install')}:
@@ -76,17 +89,28 @@ ${msg('单 CLI 管理', 'Scoped CLI management')}:
76
89
  helloagents uninstall gemini
77
90
  ${msg('支持: claude | gemini | codex | --all;省略模式时优先沿用该 CLI 已记录/已检测的模式,否则回退 standby', 'Hosts: claude | gemini | codex | --all; omit mode to reuse the tracked/detected mode for that CLI, then fall back to standby')}
78
91
 
92
+ ${msg('诊断', 'Diagnostics')}:
93
+ helloagents doctor
94
+ helloagents doctor codex --json
95
+ ${msg('检查 carrier、链接、hooks、配置注入、Codex 插件链路、model_instructions_file 遮蔽风险与版本漂移', 'Checks carriers, links, hooks, config injections, the Codex plugin chain, model_instructions_file shadowing risks, and version drift')}
96
+
79
97
  ${msg('卸载', 'Uninstall')}:
80
98
  helloagents cleanup ${msg('(推荐先执行,显式清理所有 CLI 注入/链接)', '(recommended first, explicitly cleans CLI injections/links)')}
81
99
  npm uninstall -g helloagents
82
100
  ${msg('如已安装插件,另需手动移除:', 'If plugins installed, also remove manually:')}
83
101
  Claude Code: /plugin remove helloagents
84
102
  Gemini CLI: gemini extensions uninstall helloagents
85
- `.trim());
86
- }
103
+ `.trim()
104
+ }
87
105
 
106
+ export function createInstallMessagePrinter(context) {
88
107
  return {
89
- printHelp,
90
- printInstallMsg,
91
- };
108
+ printHelp() {
109
+ console.log(renderHelp(context))
110
+ },
111
+ printInstallMsg(mode, state) {
112
+ console.log(renderInstallMessage(context, mode, state))
113
+ if (state === 'install' || state === 'refresh') console.log()
114
+ },
115
+ }
92
116
  }
@@ -0,0 +1,213 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { join } from 'node:path'
4
+ import { captureWorkspaceFingerprint } from './verify-state.mjs'
5
+ import { appendReplayEvent } from './replay-state.mjs'
6
+
7
+ export const CLOSEOUT_EVIDENCE_FILE_NAME = '.ralph-closeout.json'
8
+ const CLOSEOUT_EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
9
+ const ALLOWED_STATUSES = new Set(['PASS', 'BLOCKED'])
10
+
11
+ function normalizeEntry(entry = {}) {
12
+ return {
13
+ status: typeof entry.status === 'string' ? entry.status.trim().toUpperCase() : '',
14
+ summary: typeof entry.summary === 'string' ? entry.summary.trim() : '',
15
+ }
16
+ }
17
+
18
+ export function getCloseoutEvidencePath(cwd) {
19
+ return join(cwd, '.helloagents', CLOSEOUT_EVIDENCE_FILE_NAME)
20
+ }
21
+
22
+ export function readCloseoutEvidence(cwd) {
23
+ try {
24
+ return JSON.parse(readFileSync(getCloseoutEvidencePath(cwd), 'utf-8'))
25
+ } catch {
26
+ return null
27
+ }
28
+ }
29
+
30
+ export function clearCloseoutEvidence(cwd) {
31
+ rmSync(getCloseoutEvidencePath(cwd), { force: true })
32
+ }
33
+
34
+ export function normalizeCloseoutEvidence(input = {}) {
35
+ return {
36
+ source: typeof input.source === 'string' ? input.source.trim() : 'manual',
37
+ originCommand: typeof input.originCommand === 'string' ? input.originCommand.trim() : '',
38
+ requirementsCoverage: normalizeEntry(input.requirementsCoverage),
39
+ deliveryChecklist: normalizeEntry(input.deliveryChecklist),
40
+ }
41
+ }
42
+
43
+ export function writeCloseoutEvidence(cwd, input = {}) {
44
+ mkdirSync(join(cwd, '.helloagents'), { recursive: true })
45
+ const normalized = normalizeCloseoutEvidence(input)
46
+ const payload = {
47
+ updatedAt: new Date().toISOString(),
48
+ source: normalized.source || 'manual',
49
+ originCommand: normalized.originCommand,
50
+ requirementsCoverage: normalized.requirementsCoverage,
51
+ deliveryChecklist: normalized.deliveryChecklist,
52
+ fingerprint: captureWorkspaceFingerprint(cwd),
53
+ }
54
+ writeFileSync(getCloseoutEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
55
+ appendReplayEvent(cwd, {
56
+ event: 'closeout_evidence_written',
57
+ source: normalized.source || 'manual',
58
+ skillName: normalized.originCommand,
59
+ details: {
60
+ requirementsCoverage: normalized.requirementsCoverage,
61
+ deliveryChecklist: normalized.deliveryChecklist,
62
+ },
63
+ artifacts: ['.helloagents/.ralph-closeout.json'],
64
+ })
65
+ return payload
66
+ }
67
+
68
+ function readRequiredCloseoutEvidence(cwd) {
69
+ const evidence = readCloseoutEvidence(cwd)
70
+ if (evidence) return { evidence }
71
+ return {
72
+ error: {
73
+ required: true,
74
+ status: 'missing',
75
+ details: ['missing closeout evidence for requirements coverage and delivery checklist'],
76
+ },
77
+ }
78
+ }
79
+
80
+ function validateCloseoutTimestamp(evidence, now) {
81
+ const updatedAt = Date.parse(evidence.updatedAt || '')
82
+ if (!Number.isFinite(updatedAt)) {
83
+ return {
84
+ required: true,
85
+ status: 'invalid',
86
+ evidence,
87
+ details: ['closeout evidence timestamp is invalid'],
88
+ }
89
+ }
90
+ if (now - updatedAt > CLOSEOUT_EVIDENCE_MAX_AGE_MS) {
91
+ return {
92
+ required: true,
93
+ status: 'stale-time',
94
+ evidence,
95
+ details: ['closeout evidence is older than 30 minutes'],
96
+ }
97
+ }
98
+ return null
99
+ }
100
+
101
+ function validateCloseoutEntries(evidence) {
102
+ const requirementsCoverage = normalizeEntry(evidence.requirementsCoverage)
103
+ const deliveryChecklist = normalizeEntry(evidence.deliveryChecklist)
104
+
105
+ if (
106
+ !ALLOWED_STATUSES.has(requirementsCoverage.status)
107
+ || !requirementsCoverage.summary
108
+ || !ALLOWED_STATUSES.has(deliveryChecklist.status)
109
+ || !deliveryChecklist.summary
110
+ ) {
111
+ return {
112
+ required: true,
113
+ status: 'invalid',
114
+ evidence,
115
+ details: ['closeout evidence must record requirements coverage and delivery checklist with explicit PASS/BLOCKED status plus summary'],
116
+ }
117
+ }
118
+ if (requirementsCoverage.status !== 'PASS') {
119
+ return {
120
+ required: true,
121
+ status: 'blocked',
122
+ evidence,
123
+ details: ['requirements coverage is not marked as PASS in the latest closeout evidence'],
124
+ }
125
+ }
126
+ if (deliveryChecklist.status !== 'PASS') {
127
+ return {
128
+ required: true,
129
+ status: 'blocked',
130
+ evidence,
131
+ details: ['delivery checklist is not marked as PASS in the latest closeout evidence'],
132
+ }
133
+ }
134
+ return {
135
+ requirementsCoverage,
136
+ deliveryChecklist,
137
+ }
138
+ }
139
+
140
+ function validateCloseoutFingerprint(cwd, evidence) {
141
+ const currentFingerprint = captureWorkspaceFingerprint(cwd)
142
+ if (
143
+ currentFingerprint.available
144
+ && evidence.fingerprint?.available
145
+ && currentFingerprint.combined !== evidence.fingerprint.combined
146
+ ) {
147
+ return {
148
+ required: true,
149
+ status: 'stale-diff',
150
+ evidence,
151
+ details: ['workspace diff changed after the last successful closeout evidence'],
152
+ }
153
+ }
154
+ return null
155
+ }
156
+
157
+ export function getCloseoutEvidenceStatus(cwd, { required = false, now = Date.now() } = {}) {
158
+ if (!required) {
159
+ return {
160
+ required: false,
161
+ status: 'not-applicable',
162
+ }
163
+ }
164
+
165
+ const requiredEvidence = readRequiredCloseoutEvidence(cwd)
166
+ if (requiredEvidence.error) return requiredEvidence.error
167
+
168
+ const { evidence } = requiredEvidence
169
+ const timestampError = validateCloseoutTimestamp(evidence, now)
170
+ if (timestampError) return timestampError
171
+
172
+ const normalizedEntries = validateCloseoutEntries(evidence)
173
+ if (!('requirementsCoverage' in normalizedEntries)) return normalizedEntries
174
+
175
+ const fingerprintError = validateCloseoutFingerprint(cwd, evidence)
176
+ if (fingerprintError) return fingerprintError
177
+
178
+ return {
179
+ required: true,
180
+ status: 'valid',
181
+ evidence: {
182
+ ...evidence,
183
+ requirementsCoverage: normalizedEntries.requirementsCoverage,
184
+ deliveryChecklist: normalizedEntries.deliveryChecklist,
185
+ },
186
+ }
187
+ }
188
+
189
+ function readStdinJson() {
190
+ try {
191
+ return JSON.parse(readFileSync(0, 'utf-8'))
192
+ } catch {
193
+ return {}
194
+ }
195
+ }
196
+
197
+ function main() {
198
+ const command = process.argv[2] || ''
199
+ if (command !== 'write') return
200
+
201
+ const input = readStdinJson()
202
+ const cwd = input.cwd || process.cwd()
203
+ const payload = writeCloseoutEvidence(cwd, input)
204
+ process.stdout.write(JSON.stringify({
205
+ suppressOutput: true,
206
+ path: getCloseoutEvidencePath(cwd),
207
+ payload,
208
+ }))
209
+ }
210
+
211
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
212
+ main()
213
+ }