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.
- package/.claude-plugin/marketplace.json +6 -4
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +182 -35
- package/README_CN.md +184 -37
- package/bootstrap-lite.md +32 -26
- package/bootstrap.md +35 -29
- package/cli.mjs +119 -11
- package/gemini-extension.json +1 -1
- package/install.ps1 +128 -0
- package/install.sh +121 -0
- package/package.json +23 -4
- package/scripts/advisor-state.mjs +36 -63
- package/scripts/capability-registry.mjs +4 -4
- package/scripts/cli-branch.mjs +84 -0
- package/scripts/cli-codex-config.mjs +14 -20
- package/scripts/cli-codex.mjs +32 -38
- package/scripts/cli-doctor-render.mjs +4 -0
- package/scripts/cli-doctor.mjs +40 -30
- package/scripts/cli-host-detect.mjs +0 -1
- package/scripts/cli-hosts.mjs +16 -8
- package/scripts/cli-lifecycle-hosts.mjs +119 -32
- package/scripts/cli-lifecycle.mjs +24 -13
- package/scripts/cli-messages.mjs +34 -16
- package/scripts/cli-runtime-carrier.mjs +15 -0
- package/scripts/cli-runtime-root.mjs +72 -0
- package/scripts/cli-toml.mjs +0 -79
- package/scripts/cli-utils.mjs +30 -4
- package/scripts/closeout-state.mjs +35 -62
- package/scripts/delivery-gate-messages.mjs +70 -0
- package/scripts/delivery-gate.mjs +9 -75
- package/scripts/guard-rules.mjs +42 -42
- package/scripts/guard.mjs +44 -24
- package/scripts/notify-context.mjs +19 -28
- package/scripts/notify-events.mjs +3 -1
- package/scripts/notify-gates.mjs +2 -0
- package/scripts/notify-route.mjs +9 -7
- package/scripts/notify-ui.mjs +42 -32
- package/scripts/notify.mjs +72 -36
- package/scripts/project-storage.mjs +35 -66
- package/scripts/ralph-loop.mjs +36 -31
- package/scripts/replay-state.mjs +31 -128
- package/scripts/review-state.mjs +34 -61
- package/scripts/runtime-artifacts.mjs +95 -0
- package/scripts/runtime-context.mjs +35 -29
- package/scripts/runtime-scope.mjs +313 -0
- package/scripts/session-capsule.mjs +202 -0
- package/scripts/turn-state-cli.mjs +17 -0
- package/scripts/turn-state.mjs +185 -66
- package/scripts/turn-stop-gate.mjs +24 -6
- package/scripts/verify-state.mjs +34 -85
- package/scripts/visual-state.mjs +38 -65
- package/scripts/workflow-core.mjs +3 -3
- package/scripts/workflow-plan-files.mjs +1 -1
- package/scripts/workflow-recommendation.mjs +17 -13
- package/scripts/workflow-state.mjs +5 -5
- package/skills/commands/build/SKILL.md +1 -1
- package/skills/commands/commit/SKILL.md +1 -1
- package/skills/commands/help/SKILL.md +5 -3
- package/skills/commands/loop/SKILL.md +1 -1
- package/skills/commands/plan/SKILL.md +8 -6
- package/skills/commands/prd/SKILL.md +5 -3
- package/skills/commands/verify/SKILL.md +5 -5
- package/skills/hello-debug/SKILL.md +20 -3
- package/skills/hello-review/SKILL.md +2 -2
- package/skills/hello-subagent/SKILL.md +2 -2
- package/skills/hello-test/SKILL.md +6 -2
- package/skills/hello-ui/SKILL.md +7 -7
- package/skills/hello-verify/SKILL.md +10 -7
- package/skills/helloagents/SKILL.md +14 -9
- package/templates/context.md +6 -0
- package/templates/plans/plan.md +3 -0
- package/templates/plans/tasks.md +8 -3
package/scripts/cli-messages.mjs
CHANGED
|
@@ -20,7 +20,11 @@ function codexGlobalStatus({ home, msg }) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function pluginCommands() {
|
|
23
|
-
return
|
|
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
|
|
42
|
-
`\n ✅ HelloAGENTS installed (global mode)!\n\n
|
|
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
|
-
?
|
|
48
|
-
:
|
|
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
|
-
?
|
|
51
|
-
:
|
|
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
|
|
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 (
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
+
}
|
package/scripts/cli-toml.mjs
CHANGED
|
@@ -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 = [];
|
package/scripts/cli-utils.mjs
CHANGED
|
@@ -130,10 +130,36 @@ export function cleanSettingsHooks(settingsPath, cleanPermissions = false) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|
|
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: ['
|
|
78
|
+
details: ['缺少需求覆盖和交付清单的收尾证据'],
|
|
76
79
|
},
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
function validateCloseoutTimestamp(evidence, now) {
|
|
81
|
-
|
|
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: ['
|
|
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: ['
|
|
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: ['
|
|
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
|
-
|
|
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
|
+
}
|