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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +147 -45
- package/README_CN.md +148 -46
- package/bootstrap-lite.md +104 -46
- package/bootstrap.md +143 -112
- package/cli.mjs +80 -427
- package/gemini-extension.json +1 -1
- package/hooks/hooks-claude.json +10 -0
- package/hooks/hooks.json +10 -0
- package/package.json +2 -12
- package/scripts/advisor-state.mjs +222 -0
- package/scripts/capability-registry.mjs +59 -0
- package/scripts/cli-codex-backup.mjs +59 -0
- package/scripts/cli-codex-config.mjs +100 -0
- package/scripts/cli-codex.mjs +34 -156
- package/scripts/cli-config.mjs +1 -0
- package/scripts/cli-doctor-render.mjs +28 -0
- package/scripts/cli-doctor.mjs +367 -0
- package/scripts/cli-host-detect.mjs +94 -0
- package/scripts/cli-lifecycle-hosts.mjs +123 -0
- package/scripts/cli-lifecycle.mjs +213 -0
- package/scripts/cli-messages.mjs +76 -52
- package/scripts/closeout-state.mjs +213 -0
- package/scripts/delivery-gate.mjs +256 -0
- package/scripts/guard-rules.mjs +122 -0
- package/scripts/guard.mjs +190 -168
- package/scripts/notify-context.mjs +77 -17
- package/scripts/notify-events.mjs +5 -1
- package/scripts/notify-route.mjs +111 -0
- package/scripts/notify-shared.mjs +0 -2
- package/scripts/notify-source.mjs +113 -0
- package/scripts/notify-ui.mjs +40 -6
- package/scripts/notify.mjs +120 -59
- package/scripts/plan-contract.mjs +210 -0
- package/scripts/project-storage.mjs +235 -0
- package/scripts/ralph-loop.mjs +9 -58
- package/scripts/replay-state.mjs +210 -0
- package/scripts/review-state.mjs +220 -0
- package/scripts/runtime-context.mjs +74 -0
- package/scripts/verify-state.mjs +226 -0
- package/scripts/visual-state.mjs +244 -0
- package/scripts/workflow-core.mjs +165 -0
- package/scripts/workflow-plan-files.mjs +249 -0
- package/scripts/workflow-recommendation.mjs +335 -0
- package/scripts/workflow-state.mjs +113 -0
- package/skills/commands/auto/SKILL.md +37 -71
- package/skills/commands/build/SKILL.md +67 -0
- package/skills/commands/clean/SKILL.md +10 -8
- package/skills/commands/commit/SKILL.md +8 -4
- package/skills/commands/help/SKILL.md +19 -11
- package/skills/commands/idea/SKILL.md +55 -0
- package/skills/commands/init/SKILL.md +6 -3
- package/skills/commands/loop/SKILL.md +6 -5
- package/skills/commands/plan/SKILL.md +116 -0
- package/skills/commands/prd/SKILL.md +20 -15
- package/skills/commands/verify/SKILL.md +32 -9
- package/skills/commands/wiki/SKILL.md +59 -0
- package/skills/hello-review/SKILL.md +9 -0
- package/skills/hello-subagent/SKILL.md +4 -3
- package/skills/hello-ui/SKILL.md +36 -8
- package/skills/hello-verify/SKILL.md +10 -2
- package/skills/helloagents/SKILL.md +24 -13
- package/templates/DESIGN.md +25 -4
- package/templates/STATE.md +3 -0
- package/templates/plans/contract.json +48 -0
- package/templates/plans/plan.md +23 -0
- package/templates/plans/tasks.md +3 -3
- package/skills/commands/design/SKILL.md +0 -108
- package/skills/commands/review/SKILL.md +0 -16
- 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
|
+
}
|
package/scripts/cli-messages.mjs
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
+
}
|