helloagents 3.0.12 → 3.0.16-beta.1

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 (73) hide show
  1. package/.claude-plugin/marketplace.json +6 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +182 -35
  5. package/README_CN.md +184 -37
  6. package/bootstrap-lite.md +32 -26
  7. package/bootstrap.md +35 -29
  8. package/cli.mjs +119 -11
  9. package/gemini-extension.json +1 -1
  10. package/install.ps1 +128 -0
  11. package/install.sh +121 -0
  12. package/package.json +23 -4
  13. package/scripts/advisor-state.mjs +36 -63
  14. package/scripts/capability-registry.mjs +4 -4
  15. package/scripts/cli-branch.mjs +84 -0
  16. package/scripts/cli-codex-config.mjs +14 -20
  17. package/scripts/cli-codex.mjs +32 -38
  18. package/scripts/cli-doctor-render.mjs +4 -0
  19. package/scripts/cli-doctor.mjs +40 -30
  20. package/scripts/cli-host-detect.mjs +0 -1
  21. package/scripts/cli-hosts.mjs +16 -8
  22. package/scripts/cli-lifecycle-hosts.mjs +119 -32
  23. package/scripts/cli-lifecycle.mjs +24 -13
  24. package/scripts/cli-messages.mjs +34 -16
  25. package/scripts/cli-runtime-carrier.mjs +15 -0
  26. package/scripts/cli-runtime-root.mjs +72 -0
  27. package/scripts/cli-toml.mjs +0 -79
  28. package/scripts/cli-utils.mjs +30 -4
  29. package/scripts/closeout-state.mjs +35 -62
  30. package/scripts/delivery-gate-messages.mjs +70 -0
  31. package/scripts/delivery-gate.mjs +9 -75
  32. package/scripts/guard-rules.mjs +42 -42
  33. package/scripts/guard.mjs +44 -24
  34. package/scripts/notify-context.mjs +19 -28
  35. package/scripts/notify-events.mjs +3 -1
  36. package/scripts/notify-gates.mjs +2 -0
  37. package/scripts/notify-route.mjs +9 -7
  38. package/scripts/notify-ui.mjs +42 -32
  39. package/scripts/notify.mjs +72 -36
  40. package/scripts/project-storage.mjs +35 -66
  41. package/scripts/ralph-loop.mjs +36 -31
  42. package/scripts/replay-state.mjs +31 -128
  43. package/scripts/review-state.mjs +34 -61
  44. package/scripts/runtime-artifacts.mjs +95 -0
  45. package/scripts/runtime-context.mjs +35 -29
  46. package/scripts/runtime-scope.mjs +313 -0
  47. package/scripts/session-capsule.mjs +202 -0
  48. package/scripts/turn-state-cli.mjs +17 -0
  49. package/scripts/turn-state.mjs +185 -66
  50. package/scripts/turn-stop-gate.mjs +24 -6
  51. package/scripts/verify-state.mjs +34 -85
  52. package/scripts/visual-state.mjs +38 -65
  53. package/scripts/workflow-core.mjs +3 -3
  54. package/scripts/workflow-plan-files.mjs +1 -1
  55. package/scripts/workflow-recommendation.mjs +17 -13
  56. package/scripts/workflow-state.mjs +5 -5
  57. package/skills/commands/build/SKILL.md +1 -1
  58. package/skills/commands/commit/SKILL.md +1 -1
  59. package/skills/commands/help/SKILL.md +5 -3
  60. package/skills/commands/loop/SKILL.md +1 -1
  61. package/skills/commands/plan/SKILL.md +8 -6
  62. package/skills/commands/prd/SKILL.md +5 -3
  63. package/skills/commands/verify/SKILL.md +5 -5
  64. package/skills/hello-debug/SKILL.md +20 -3
  65. package/skills/hello-review/SKILL.md +2 -2
  66. package/skills/hello-subagent/SKILL.md +2 -2
  67. package/skills/hello-test/SKILL.md +6 -2
  68. package/skills/hello-ui/SKILL.md +7 -7
  69. package/skills/hello-verify/SKILL.md +10 -7
  70. package/skills/helloagents/SKILL.md +14 -9
  71. package/templates/context.md +6 -0
  72. package/templates/plans/plan.md +3 -0
  73. package/templates/plans/tasks.md +8 -3
@@ -20,7 +20,11 @@ function codexGlobalStatus({ home, msg }) {
20
20
  }
21
21
 
22
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'
23
+ return [
24
+ ' Claude Code: /plugin marketplace add hellowind777/helloagents',
25
+ ' /plugin install helloagents@helloagents',
26
+ ' Gemini CLI: gemini extensions install https://github.com/hellowind777/helloagents',
27
+ ].join('\n')
24
28
  }
25
29
 
26
30
  function removeHint(msg) {
@@ -30,6 +34,13 @@ function removeHint(msg) {
30
34
  )
31
35
  }
32
36
 
37
+ function restartHint(msg) {
38
+ return msg(
39
+ '重装、刷新或切换模式后,请重启对应 AI CLI 或新开会话;已运行会话不会自动重载注入规则。',
40
+ 'After reinstalling, refreshing, or switching modes, restart the target AI CLI or open a new session; already running sessions do not reload injected rules automatically.',
41
+ )
42
+ }
43
+
33
44
  function renderInstallMessage(context, mode, state) {
34
45
  const { msg } = context
35
46
  const install = state === 'install'
@@ -38,34 +49,34 @@ function renderInstallMessage(context, mode, state) {
38
49
  if (mode === 'global') {
39
50
  if (install) {
40
51
  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)`,
52
+ `\n ✅ HelloAGENTS 已安装(global 模式)!\n\n Claude Code / Gemini CLI: 已自动尝试宿主原生插件/扩展安装\n Codex: ${codexGlobalStatus(context)}(~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n ${restartHint(msg)}\n\n 若宿主命令不可用,请手动执行:\n${pluginCommands()}\n\n 切换模式:\n helloagents --standby 标准模式(默认,非插件安装)`,
53
+ `\n ✅ HelloAGENTS installed (global mode)!\n\n Claude Code / Gemini CLI: native plugin/extension install attempted automatically\n Codex: ${codexGlobalStatus(context)} (~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n ${restartHint(msg)}\n\n If a host command is unavailable, run manually:\n${pluginCommands()}\n\n Switch modes:\n helloagents --standby Standby mode (default, non-plugin install)`,
43
54
  )
44
55
  }
45
56
  return msg(
46
57
  refresh
47
- ? ' global 模式已刷新。\n Claude Code / Gemini 请保持插件已安装;Codex 原生本地插件已重装并同步最新文件。'
48
- : ' 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 请手动安装插件;Codex 已自动安装原生本地插件。',
58
+ ? ` global 模式已刷新。\n Claude Code / Gemini 已自动尝试刷新宿主插件/扩展;Codex 原生本地插件已重装并同步最新文件。\n ${restartHint(msg)}`
59
+ : ` 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 已自动尝试安装宿主插件/扩展;Codex 已自动安装原生本地插件。\n ${restartHint(msg)}\n\n若宿主命令不可用,请手动执行:\n${pluginCommands()}`,
49
60
  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.',
61
+ ? ` Global mode refreshed.\n Claude Code / Gemini native plugin/extension refresh was attempted automatically; Codex native local-plugin files were reinstalled and synced.\n ${restartHint(msg)}`
62
+ : ` All projects will use full HelloAGENTS rules.\n Claude Code / Gemini native plugin/extension install was attempted automatically; Codex now uses the native local-plugin path automatically.\n ${restartHint(msg)}\n\nIf a host command is unavailable, run manually:\n${pluginCommands()}`,
52
63
  )
53
64
  }
54
65
 
55
66
  if (install) {
56
67
  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)`,
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 完整初始化项目;也可用 ~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 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 (auto-attempts Claude/Gemini plugins or extensions; native local plugin auto-install for Codex)`,
59
70
  )
60
71
  }
61
72
 
62
73
  return msg(
63
74
  refresh
64
- ? ` standby 模式已刷新,CLI 注入与链接已同步最新文件。\n ${removeHint(msg)}`
65
- : ` 项目可通过 ~wiki 创建/同步知识库,或通过 ~init 完整初始化;未激活项目仅注入通用规则。\n ${removeHint(msg)}`,
75
+ ? ` standby 模式已刷新,CLI 注入与链接已同步最新文件。\n ${restartHint(msg)}\n ${removeHint(msg)}`
76
+ : ` 项目可通过 ~wiki 创建/同步知识库,或通过 ~init 完整初始化;未激活项目仅注入通用规则。\n ${restartHint(msg)}\n ${removeHint(msg)}`,
66
77
  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)}`,
78
+ ? ` Standby mode refreshed; injected files and links were synchronized.\n ${restartHint(msg)}\n ${removeHint(msg)}`
79
+ : ` Projects can use ~wiki for KB-only activation or ~init for the full bootstrap. Unactivated projects get lite rules only.\n ${restartHint(msg)}\n ${removeHint(msg)}`,
69
80
  )
70
81
  }
71
82
 
@@ -74,11 +85,12 @@ function renderHelp({ pkgVersion, msg }) {
74
85
  HelloAGENTS v${pkgVersion} — The orchestration kernel for AI CLIs
75
86
 
76
87
  ${msg('安装', 'Install')}:
77
- npm install -g helloagents ${msg('(只安装包与命令;CLI 部署需显式执行 helloagents install ...)', '(installs the package/command only; deploy to CLIs explicitly with helloagents install ...)')}
88
+ npm install -g helloagents ${msg('(安装命令并同步稳定运行根目录;CLI 部署需显式执行 helloagents install ...)', '(installs the command and syncs the stable runtime root; deploy to CLIs explicitly with helloagents install ...)')}
89
+ HELLOAGENTS=codex:global npm install -g helloagents
78
90
  helloagents-js ${msg('(稳定别名,避免与系统中同名可执行文件冲突)', '(stable alias to avoid conflicts with system executables of the same name)')}
79
91
 
80
92
  ${msg('模式切换', 'Mode switching')}:
81
- helloagents --global ${msg('全局模式(Claude/Gemini 装插件;Codex 自动装原生本地插件)', 'Global mode (manual plugins for Claude/Gemini; native local plugin auto-install for Codex)')}
93
+ helloagents --global ${msg('全局模式(自动尝试 Claude/Gemini 插件或扩展;Codex 自动装原生本地插件)', 'Global mode (auto-attempts Claude/Gemini plugins or extensions; native local plugin auto-install for Codex)')}
82
94
  helloagents --standby ${msg('标准模式(非插件安装,hello-* 不自动触发,默认)', "Standby mode (non-plugin install, hello-* won't auto-trigger, default)")}
83
95
 
84
96
  ${msg('单 CLI 管理', 'Scoped CLI management')}:
@@ -89,6 +101,12 @@ ${msg('单 CLI 管理', 'Scoped CLI management')}:
89
101
  helloagents uninstall gemini
90
102
  ${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')}
91
103
 
104
+ ${msg('分支切换', 'Branch switching')}:
105
+ helloagents switch-branch beta
106
+ helloagents switch-branch beta claude --global
107
+ helloagents branch github:hellowind777/helloagents#beta --all --standby
108
+ ${msg('先通过 npm 安装指定 ref,再通过 npm 脚本同步宿主 CLI', 'Installs the requested ref with npm first, then syncs host CLIs through npm scripts')}
109
+
92
110
  ${msg('诊断', 'Diagnostics')}:
93
111
  helloagents doctor
94
112
  helloagents doctor codex --json
@@ -97,7 +115,7 @@ ${msg('诊断', 'Diagnostics')}:
97
115
  ${msg('卸载', 'Uninstall')}:
98
116
  helloagents cleanup ${msg('(推荐先执行,显式清理所有 CLI 注入/链接)', '(recommended first, explicitly cleans CLI injections/links)')}
99
117
  npm uninstall -g helloagents
100
- ${msg('如已安装插件,另需手动移除:', 'If plugins installed, also remove manually:')}
118
+ ${msg('如宿主命令不可用,另需手动移除:', 'If host commands are unavailable, also remove manually:')}
101
119
  Claude Code: /plugin remove helloagents
102
120
  Gemini CLI: gemini extensions uninstall helloagents
103
121
  `.trim()
@@ -0,0 +1,15 @@
1
+ import { join } from 'node:path'
2
+
3
+ import { safeJson } from './cli-utils.mjs'
4
+
5
+ export function readCarrierSettings(home) {
6
+ return safeJson(join(home, '.helloagents', 'helloagents.json')) || {}
7
+ }
8
+
9
+ export function buildRuntimeCarrier(bootstrapContent, settings = {}) {
10
+ void settings
11
+ const normalized = String(bootstrapContent || '').trim()
12
+ if (!normalized) return ''
13
+
14
+ return `${normalized}\n`
15
+ }
@@ -0,0 +1,72 @@
1
+ import { mkdtempSync, realpathSync, renameSync } from 'node:fs'
2
+ import { dirname, join, resolve } from 'node:path'
3
+
4
+ import { copyEntries, ensureDir, removeIfExists } from './cli-utils.mjs'
5
+
6
+ export const RUNTIME_ROOT_ENTRIES = [
7
+ '.claude-plugin',
8
+ '.codex-plugin',
9
+ 'assets',
10
+ 'bootstrap-lite.md',
11
+ 'bootstrap.md',
12
+ 'cli.mjs',
13
+ 'gemini-extension.json',
14
+ 'hooks',
15
+ 'install.ps1',
16
+ 'install.sh',
17
+ 'LICENSE.md',
18
+ 'package.json',
19
+ 'README.md',
20
+ 'README_CN.md',
21
+ 'scripts',
22
+ 'skills',
23
+ 'templates',
24
+ ]
25
+
26
+ /** Return the stable per-user runtime copy used by host integrations. */
27
+ export function getStableRuntimeRoot(home) {
28
+ return join(home, '.helloagents', 'helloagents')
29
+ }
30
+
31
+ function normalizePath(path) {
32
+ const resolved = resolve(path)
33
+ try {
34
+ return realpathSync(resolved)
35
+ } catch {
36
+ return resolved
37
+ }
38
+ }
39
+
40
+ function samePath(left, right) {
41
+ const a = normalizePath(left)
42
+ const b = normalizePath(right)
43
+ return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b
44
+ }
45
+
46
+ /** Sync package runtime files into the stable root without copying repo-only files. */
47
+ export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
48
+ const source = resolve(sourceRoot)
49
+ const target = resolve(runtimeRoot)
50
+ if (samePath(source, target)) {
51
+ return { synced: false, root: target }
52
+ }
53
+
54
+ const parent = dirname(target)
55
+ ensureDir(parent)
56
+ const staging = mkdtempSync(join(parent, '.helloagents-runtime-'))
57
+
58
+ try {
59
+ copyEntries(source, staging, RUNTIME_ROOT_ENTRIES)
60
+ removeIfExists(target)
61
+ renameSync(staging, target)
62
+ return { synced: true, root: target }
63
+ } catch (error) {
64
+ removeIfExists(staging)
65
+ throw error
66
+ }
67
+ }
68
+
69
+ /** Remove the stable runtime copy while leaving user settings under ~/.helloagents intact. */
70
+ export function removeRuntimeRoot(runtimeRoot) {
71
+ removeIfExists(runtimeRoot)
72
+ }
@@ -164,85 +164,6 @@ export function ensureTopLevelTomlLine(text, key, line) {
164
164
  return upsertTopLevelTomlKey(text, key, value);
165
165
  }
166
166
 
167
- export function readTomlKeyInSection(text, headerLine, key) {
168
- const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
169
- const headerIndex = lines.findIndex((line) => line.trim() === headerLine);
170
- if (headerIndex < 0) return '';
171
-
172
- const keyRe = new RegExp(`^\\s*${key}\\s*=.*$`);
173
- for (let index = headerIndex + 1; index < lines.length; index += 1) {
174
- const line = lines[index];
175
- if (isTomlTableHeader(line)) break;
176
- if (keyRe.test(line)) return line.trim();
177
- }
178
- return '';
179
- }
180
-
181
- export function removeTomlKeyInSection(text, headerLine, key) {
182
- const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
183
- const headerIndex = lines.findIndex((line) => line.trim() === headerLine);
184
- if (headerIndex < 0) return normalizeToml(text);
185
-
186
- const keyRe = new RegExp(`^\\s*${key}\\s*=`);
187
- const nextLines = [];
188
- let removed = false;
189
- for (let index = 0; index < lines.length; index += 1) {
190
- const line = lines[index];
191
- if (index > headerIndex && isTomlTableHeader(line)) {
192
- nextLines.push(...lines.slice(index));
193
- break;
194
- }
195
- if (index > headerIndex && keyRe.test(line)) {
196
- removed = true;
197
- continue;
198
- }
199
- nextLines.push(line);
200
- }
201
-
202
- if (!removed) return normalizeToml(text);
203
- return normalizeToml(nextLines.join('\n'));
204
- }
205
-
206
- export function upsertTomlKeyInSection(text, headerLine, key, value) {
207
- const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
208
- const headerIndex = lines.findIndex((line) => line.trim() === headerLine);
209
-
210
- if (headerIndex < 0) {
211
- const base = normalizeToml(text).trimEnd();
212
- return base
213
- ? `${base}\n\n${headerLine}\n${key} = ${value}\n`
214
- : `${headerLine}\n${key} = ${value}\n`;
215
- }
216
-
217
- let endIndex = headerIndex + 1;
218
- while (endIndex < lines.length && !isTomlTableHeader(lines[endIndex])) {
219
- endIndex += 1;
220
- }
221
-
222
- const keyRe = new RegExp(`^\\s*${key}\\s*=`);
223
- let updated = false;
224
- for (let index = headerIndex + 1; index < endIndex; index += 1) {
225
- if (keyRe.test(lines[index])) {
226
- lines[index] = `${key} = ${value}`;
227
- updated = true;
228
- break;
229
- }
230
- }
231
-
232
- if (!updated) {
233
- lines.splice(endIndex, 0, `${key} = ${value}`);
234
- }
235
-
236
- return normalizeToml(lines.join('\n'));
237
- }
238
-
239
- export function ensureTomlKeyInSection(text, headerLine, key, line) {
240
- const normalized = String(line || '').trim();
241
- if (!normalized) return normalizeToml(text);
242
- const value = normalized.slice(normalized.indexOf('=') + 1).trim();
243
- return upsertTomlKeyInSection(text, headerLine, key, value);
244
- }
245
-
246
167
  export function stripTomlSection(text, headerLine) {
247
168
  const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
248
169
  const kept = [];
@@ -130,10 +130,36 @@ export function cleanSettingsHooks(settingsPath, cleanPermissions = false) {
130
130
  }
131
131
  }
132
132
 
133
- /** Read hooks source file and replace path variable with absolute PKG_ROOT. */
134
- export function loadHooksWithAbsPath(pkgRoot, hooksFile, pathVar) {
133
+ function rewriteHookCommandToCli(command = '', pathVar = '') {
134
+ const replacements = new Map([
135
+ [`node "${pathVar}/scripts/notify.mjs"`, 'helloagents-js notify'],
136
+ [`node "${pathVar}/scripts/guard.mjs"`, 'helloagents-js guard'],
137
+ [`node "${pathVar}/scripts/ralph-loop.mjs"`, 'helloagents-js ralph-loop'],
138
+ ]);
139
+
140
+ let next = command;
141
+ for (const [from, to] of replacements) {
142
+ next = next.replaceAll(from, to);
143
+ }
144
+ return next;
145
+ }
146
+
147
+ function rewriteHookCommands(value, pathVar) {
148
+ if (Array.isArray(value)) return value.map((item) => rewriteHookCommands(item, pathVar));
149
+ if (value && typeof value === 'object') {
150
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
151
+ key,
152
+ key === 'command' && typeof entry === 'string'
153
+ ? rewriteHookCommandToCli(entry, pathVar)
154
+ : rewriteHookCommands(entry, pathVar),
155
+ ]));
156
+ }
157
+ return value;
158
+ }
159
+
160
+ /** Read hooks source file and rewrite standby hooks to the stable CLI entrypoint. */
161
+ export function loadHooksWithCliEntry(pkgRoot, hooksFile, pathVar) {
135
162
  const src = safeRead(join(pkgRoot, 'hooks', hooksFile));
136
163
  if (!src) return null;
137
- const absRoot = pkgRoot.replace(/\\/g, '/');
138
- return JSON.parse(src.replace(new RegExp(pathVar.replace(/[{}$]/g, '\\$&'), 'g'), absRoot));
164
+ return rewriteHookCommands(JSON.parse(src), pathVar);
139
165
  }
@@ -1,11 +1,18 @@
1
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
1
+ import { readFileSync } from 'node:fs'
2
2
  import { fileURLToPath } from 'node:url'
3
- import { join } from 'node:path'
4
- import { captureWorkspaceFingerprint } from './verify-state.mjs'
5
3
  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
4
+ import {
5
+ captureWorkspaceFingerprint,
6
+ clearRuntimeEvidence,
7
+ getRuntimeEvidencePath,
8
+ getRuntimeEvidenceRelativePath,
9
+ readRuntimeEvidence,
10
+ validateEvidenceFingerprint,
11
+ validateEvidenceTimestamp,
12
+ writeRuntimeEvidence,
13
+ } from './runtime-artifacts.mjs'
14
+
15
+ export const CLOSEOUT_EVIDENCE_FILE_NAME = 'closeout.json'
9
16
  const ALLOWED_STATUSES = new Set(['PASS', 'BLOCKED'])
10
17
 
11
18
  function normalizeEntry(entry = {}) {
@@ -15,20 +22,16 @@ function normalizeEntry(entry = {}) {
15
22
  }
16
23
  }
17
24
 
18
- export function getCloseoutEvidencePath(cwd) {
19
- return join(cwd, '.helloagents', CLOSEOUT_EVIDENCE_FILE_NAME)
25
+ export function getCloseoutEvidencePath(cwd, options = {}) {
26
+ return getRuntimeEvidencePath(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, options)
20
27
  }
21
28
 
22
- export function readCloseoutEvidence(cwd) {
23
- try {
24
- return JSON.parse(readFileSync(getCloseoutEvidencePath(cwd), 'utf-8'))
25
- } catch {
26
- return null
27
- }
29
+ export function readCloseoutEvidence(cwd, options = {}) {
30
+ return readRuntimeEvidence(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, options)
28
31
  }
29
32
 
30
- export function clearCloseoutEvidence(cwd) {
31
- rmSync(getCloseoutEvidencePath(cwd), { force: true })
33
+ export function clearCloseoutEvidence(cwd, options = {}) {
34
+ clearRuntimeEvidence(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, options)
32
35
  }
33
36
 
34
37
  export function normalizeCloseoutEvidence(input = {}) {
@@ -40,8 +43,7 @@ export function normalizeCloseoutEvidence(input = {}) {
40
43
  }
41
44
  }
42
45
 
43
- export function writeCloseoutEvidence(cwd, input = {}) {
44
- mkdirSync(join(cwd, '.helloagents'), { recursive: true })
46
+ export function writeCloseoutEvidence(cwd, input = {}, options = {}) {
45
47
  const normalized = normalizeCloseoutEvidence(input)
46
48
  const payload = {
47
49
  updatedAt: new Date().toISOString(),
@@ -51,51 +53,35 @@ export function writeCloseoutEvidence(cwd, input = {}) {
51
53
  deliveryChecklist: normalized.deliveryChecklist,
52
54
  fingerprint: captureWorkspaceFingerprint(cwd),
53
55
  }
54
- writeFileSync(getCloseoutEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
56
+ writeRuntimeEvidence(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, payload, options)
55
57
  appendReplayEvent(cwd, {
56
58
  event: 'closeout_evidence_written',
57
59
  source: normalized.source || 'manual',
58
60
  skillName: normalized.originCommand,
61
+ payload: options.payload || {},
59
62
  details: {
60
63
  requirementsCoverage: normalized.requirementsCoverage,
61
64
  deliveryChecklist: normalized.deliveryChecklist,
62
65
  },
63
- artifacts: ['.helloagents/.ralph-closeout.json'],
66
+ artifacts: [getRuntimeEvidenceRelativePath(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, options)],
64
67
  })
65
68
  return payload
66
69
  }
67
70
 
68
- function readRequiredCloseoutEvidence(cwd) {
69
- const evidence = readCloseoutEvidence(cwd)
71
+ function readRequiredCloseoutEvidence(cwd, options = {}) {
72
+ const evidence = readCloseoutEvidence(cwd, options)
70
73
  if (evidence) return { evidence }
71
74
  return {
72
75
  error: {
73
76
  required: true,
74
77
  status: 'missing',
75
- details: ['missing closeout evidence for requirements coverage and delivery checklist'],
78
+ details: ['缺少需求覆盖和交付清单的收尾证据'],
76
79
  },
77
80
  }
78
81
  }
79
82
 
80
83
  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
84
+ return validateEvidenceTimestamp(evidence, now, '收尾证据')
99
85
  }
100
86
 
101
87
  function validateCloseoutEntries(evidence) {
@@ -112,7 +98,7 @@ function validateCloseoutEntries(evidence) {
112
98
  required: true,
113
99
  status: 'invalid',
114
100
  evidence,
115
- details: ['closeout evidence must record requirements coverage and delivery checklist with explicit PASS/BLOCKED status plus summary'],
101
+ details: ['收尾证据必须记录需求覆盖和交付清单,并包含明确的 PASS/BLOCKED 状态和 summary'],
116
102
  }
117
103
  }
118
104
  if (requirementsCoverage.status !== 'PASS') {
@@ -120,7 +106,7 @@ function validateCloseoutEntries(evidence) {
120
106
  required: true,
121
107
  status: 'blocked',
122
108
  evidence,
123
- details: ['requirements coverage is not marked as PASS in the latest closeout evidence'],
109
+ details: ['最新收尾证据中的需求覆盖未标记为 PASS'],
124
110
  }
125
111
  }
126
112
  if (deliveryChecklist.status !== 'PASS') {
@@ -128,7 +114,7 @@ function validateCloseoutEntries(evidence) {
128
114
  required: true,
129
115
  status: 'blocked',
130
116
  evidence,
131
- details: ['delivery checklist is not marked as PASS in the latest closeout evidence'],
117
+ details: ['最新收尾证据中的交付清单未标记为 PASS'],
132
118
  }
133
119
  }
134
120
  return {
@@ -138,23 +124,10 @@ function validateCloseoutEntries(evidence) {
138
124
  }
139
125
 
140
126
  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
127
+ return validateEvidenceFingerprint(cwd, evidence, '成功收尾证据')
155
128
  }
156
129
 
157
- export function getCloseoutEvidenceStatus(cwd, { required = false, now = Date.now() } = {}) {
130
+ export function getCloseoutEvidenceStatus(cwd, { required = false, now = Date.now(), ...options } = {}) {
158
131
  if (!required) {
159
132
  return {
160
133
  required: false,
@@ -162,7 +135,7 @@ export function getCloseoutEvidenceStatus(cwd, { required = false, now = Date.no
162
135
  }
163
136
  }
164
137
 
165
- const requiredEvidence = readRequiredCloseoutEvidence(cwd)
138
+ const requiredEvidence = readRequiredCloseoutEvidence(cwd, options)
166
139
  if (requiredEvidence.error) return requiredEvidence.error
167
140
 
168
141
  const { evidence } = requiredEvidence
@@ -200,10 +173,10 @@ function main() {
200
173
 
201
174
  const input = readStdinJson()
202
175
  const cwd = input.cwd || process.cwd()
203
- const payload = writeCloseoutEvidence(cwd, input)
176
+ const payload = writeCloseoutEvidence(cwd, input, { payload: input })
204
177
  process.stdout.write(JSON.stringify({
205
178
  suppressOutput: true,
206
- path: getCloseoutEvidencePath(cwd),
179
+ path: getCloseoutEvidencePath(cwd, { payload: input }),
207
180
  payload,
208
181
  }))
209
182
  }
@@ -0,0 +1,70 @@
1
+ export function buildUnderSpecifiedDetails(entry) {
2
+ return entry.taskSummary.underSpecifiedItems
3
+ .slice(0, 3)
4
+ .map((item) => {
5
+ const missing = []
6
+ if (item.files.length === 0) missing.push('缺少涉及文件')
7
+ if (!item.acceptance) missing.push('缺少完成标准')
8
+ if (!item.validation) missing.push('缺少验证方式')
9
+ return `${item.text}(${missing.join('、')})`
10
+ })
11
+ }
12
+
13
+ function issueHeading(issue) {
14
+ switch (issue.type) {
15
+ case 'missing-files':
16
+ return '方案包缺少必需文件'
17
+ case 'template-placeholders':
18
+ return '方案包仍包含模板占位内容'
19
+ case 'missing-task-checklist':
20
+ return '方案包没有可执行任务'
21
+ case 'unfinished-tasks':
22
+ return '方案包仍有未完成任务'
23
+ case 'under-specified-tasks':
24
+ return '任务缺少可交付元数据'
25
+ case 'missing-contract':
26
+ return '方案包缺少可信的结构化契约'
27
+ case 'missing-verify-evidence':
28
+ return '当前工作流缺少最新验证证据'
29
+ case 'missing-review-evidence':
30
+ return '当前工作流缺少最新审查证据'
31
+ case 'missing-advisor-evidence':
32
+ return '当前工作流缺少最新 advisor 证据'
33
+ case 'missing-visual-evidence':
34
+ return '当前工作流缺少最新视觉验收证据'
35
+ case 'missing-closeout-evidence':
36
+ return '当前工作流缺少最新收尾证据'
37
+ default:
38
+ return '方案包尚未达到交付条件'
39
+ }
40
+ }
41
+
42
+ export function buildDeliveryBlockReason(issues, recommendation, gateHint) {
43
+ const lines = ['[Delivery Gate] 当前工作流尚未闭合,暂不能交付:']
44
+
45
+ for (const issue of issues) {
46
+ lines.push(`- ${issue.planName}: ${issueHeading(issue)}`)
47
+ for (const detail of issue.details) {
48
+ lines.push(` - ${detail}`)
49
+ }
50
+ if (issue.extraCount) {
51
+ lines.push(` - 另有 ${issue.extraCount} 项`)
52
+ }
53
+ }
54
+
55
+ lines.push('')
56
+ if (recommendation?.nextPath) {
57
+ lines.push(`建议路径:${recommendation.nextPath}`)
58
+ }
59
+ if (issues.some((issue) => issue.type === 'missing-closeout-evidence')) {
60
+ lines.push('下一步收尾:先写入当前会话 `artifacts/closeout.json`,记录 `requirementsCoverage` 和 `deliveryChecklist`,再报告完成。')
61
+ }
62
+ if (issues.some((issue) => issue.type === 'missing-visual-evidence')) {
63
+ lines.push('下一步视觉验收:先写入当前会话 `artifacts/visual.json`,记录 `tooling`、`screensChecked`、`statesChecked`、`status` 和 `summary`,再报告完成。')
64
+ }
65
+ if (gateHint) {
66
+ lines.push(gateHint)
67
+ }
68
+ lines.push('暂不要报告完成。先完成剩余任务、明确关闭任务,或修复方案包,使其成为可信的交付记录。')
69
+ return lines.join('\n')
70
+ }