helloagents 3.0.9-beta.1 → 3.0.10-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 (41) 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 +16 -7
  5. package/README_CN.md +41 -32
  6. package/bootstrap-lite.md +18 -18
  7. package/bootstrap.md +22 -22
  8. package/gemini-extension.json +1 -1
  9. package/package.json +1 -1
  10. package/scripts/cli-codex-config.mjs +8 -2
  11. package/scripts/cli-codex.mjs +5 -3
  12. package/scripts/cli-lifecycle.mjs +11 -0
  13. package/scripts/cli-messages.mjs +3 -3
  14. package/scripts/cli-toml-values.mjs +25 -0
  15. package/scripts/cli-toml.mjs +15 -0
  16. package/scripts/guard.mjs +2 -2
  17. package/scripts/notify-context.mjs +7 -7
  18. package/scripts/notify-events.mjs +0 -8
  19. package/scripts/notify-gates.mjs +128 -0
  20. package/scripts/notify-ui.mjs +3 -0
  21. package/scripts/notify.mjs +44 -76
  22. package/scripts/project-storage.mjs +5 -5
  23. package/scripts/workflow-core.mjs +5 -5
  24. package/scripts/workflow-recommendation.mjs +4 -4
  25. package/scripts/workflow-state.mjs +1 -1
  26. package/skills/commands/auto/SKILL.md +13 -13
  27. package/skills/commands/build/SKILL.md +5 -5
  28. package/skills/commands/clean/SKILL.md +6 -6
  29. package/skills/commands/commit/SKILL.md +2 -2
  30. package/skills/commands/help/SKILL.md +2 -2
  31. package/skills/commands/idea/SKILL.md +5 -5
  32. package/skills/commands/init/SKILL.md +4 -4
  33. package/skills/commands/loop/SKILL.md +4 -4
  34. package/skills/commands/plan/SKILL.md +13 -13
  35. package/skills/commands/prd/SKILL.md +13 -13
  36. package/skills/commands/verify/SKILL.md +3 -3
  37. package/skills/commands/wiki/SKILL.md +5 -5
  38. package/skills/hello-subagent/SKILL.md +1 -1
  39. package/skills/hello-ui/SKILL.md +2 -2
  40. package/skills/helloagents/SKILL.md +3 -2
  41. package/templates/plans/contract.json +2 -2
@@ -181,6 +181,17 @@ function runAllHostsLifecycle(action, explicitMode) {
181
181
  }
182
182
 
183
183
  const settings = readSettings(true)
184
+ if (action === 'update' && !explicitMode) {
185
+ for (const host of HOSTS) {
186
+ const mode = resolveHostMode(host, '', settings)
187
+ const result = runHostLifecycle(runtime, action, host, mode)
188
+ if (!result.skipped) setTrackedHostMode(settings, host, mode)
189
+ }
190
+ writeSettings(settings)
191
+ runtime.printInstallMsg(settings.install_mode || DEFAULTS.install_mode, 'refresh')
192
+ return
193
+ }
194
+
184
195
  const mode = resolveInstallMode(explicitMode, settings)
185
196
  if (explicitMode) settings.install_mode = explicitMode
186
197
  installAllHosts(runtime, mode)
@@ -44,8 +44,8 @@ function renderInstallMessage(context, mode, state) {
44
44
  }
45
45
  return msg(
46
46
  refresh
47
- ? ' global 模式已刷新。\n Claude Code / Gemini 请保持插件已安装;Codex 原生本地插件链路已重装并同步最新文件。'
48
- : ' 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 请手动安装插件;Codex 已自动走原生本地插件链路。',
47
+ ? ' global 模式已刷新。\n Claude Code / Gemini 请保持插件已安装;Codex 原生本地插件已重装并同步最新文件。'
48
+ : ' 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 请手动安装插件;Codex 已自动安装原生本地插件。',
49
49
  refresh
50
50
  ? ' Global mode refreshed.\n Keep Claude Code / Gemini plugins installed; Codex native local-plugin files were reinstalled and synced.'
51
51
  : ' All projects will use full HelloAGENTS rules.\n Install Claude Code / Gemini plugins manually; Codex now uses the native local-plugin path automatically.',
@@ -92,7 +92,7 @@ ${msg('单 CLI 管理', 'Scoped CLI management')}:
92
92
  ${msg('诊断', 'Diagnostics')}:
93
93
  helloagents doctor
94
94
  helloagents doctor codex --json
95
- ${msg('检查 carrier、链接、hooks、配置注入、Codex 插件链路、受管 model_instructions_file 指向与版本漂移', 'Checks carriers, links, hooks, config injections, the Codex plugin chain, managed model_instructions_file targeting, and version drift')}
95
+ ${msg('检查 carrier、链接、hooks、配置注入、Codex 插件安装、受管 model_instructions_file 指向与版本漂移', 'Checks carriers, links, hooks, config injections, Codex plugin installation, managed model_instructions_file targeting, and version drift')}
96
96
 
97
97
  ${msg('卸载', 'Uninstall')}:
98
98
  helloagents cleanup ${msg('(推荐先执行,显式清理所有 CLI 注入/链接)', '(recommended first, explicitly cleans CLI injections/links)')}
@@ -0,0 +1,25 @@
1
+ export function getTomlArrayDepthDelta(text) {
2
+ let depth = 0
3
+ let quoted = false
4
+ let escaped = false
5
+
6
+ for (const char of String(text || '')) {
7
+ if (escaped) {
8
+ escaped = false
9
+ continue
10
+ }
11
+ if (char === '\\' && quoted) {
12
+ escaped = true
13
+ continue
14
+ }
15
+ if (char === '"') {
16
+ quoted = !quoted
17
+ continue
18
+ }
19
+ if (quoted) continue
20
+ if (char === '[') depth += 1
21
+ if (char === ']') depth -= 1
22
+ }
23
+
24
+ return depth
25
+ }
@@ -3,6 +3,8 @@
3
3
  * Targets the small subset of TOML structures used by Codex CLI config.
4
4
  */
5
5
 
6
+ import { getTomlArrayDepthDelta } from './cli-toml-values.mjs'
7
+
6
8
  export function isTomlTableHeader(line) {
7
9
  const trimmed = String(line || '').trim();
8
10
  return trimmed.startsWith('[') && trimmed.endsWith(']');
@@ -62,6 +64,19 @@ function findTopLevelTomlBlock(text, key) {
62
64
  const closeIndex = normalized.indexOf('"""', openIndex + 3);
63
65
  end = closeIndex >= 0 ? closeIndex + 3 : normalized.length;
64
66
  }
67
+ if (value.startsWith('[')) {
68
+ let depth = getTomlArrayDepthDelta(firstLine.slice(firstLine.indexOf('=') + 1));
69
+ let lineStart = firstLineEnd + (normalized[firstLineEnd] === '\n' ? 1 : 0);
70
+
71
+ while (depth > 0 && lineStart < normalized.length) {
72
+ const lineEndIndex = normalized.indexOf('\n', lineStart);
73
+ const nextLineEnd = lineEndIndex >= 0 ? lineEndIndex : normalized.length;
74
+ const nextLine = normalized.slice(lineStart, nextLineEnd);
75
+ depth += getTomlArrayDepthDelta(nextLine);
76
+ end = nextLineEnd;
77
+ lineStart = nextLineEnd + 1;
78
+ }
79
+ }
65
80
 
66
81
  while (end < normalized.length && normalized[end] === '\n') {
67
82
  end += 1;
package/scripts/guard.mjs CHANGED
@@ -124,7 +124,7 @@ function buildPostWriteWarnings(data) {
124
124
  const filePath = data.tool_input?.file_path || ''
125
125
  return [
126
126
  ...(detectIdeaBoundaryContext(data)?.zeroSideEffect
127
- ? ['~idea 本轮要求只读探索;检测到写入工具落地,请回退到探索输出或升级到 ~plan / ~build / ~prd / ~auto 后再修改文件']
127
+ ? ['~idea 本轮要求只读探索;检测到写入文件的工具调用,请回到探索输出,或升级到 ~plan / ~build / ~prd / ~auto 后再修改文件']
128
128
  : []),
129
129
  ...scanUnrequestedFiles(filePath, data.tool_name),
130
130
  ...(content ? [...scanForSecrets(content), ...scanDangerousPackages(content, filePath)] : []),
@@ -195,7 +195,7 @@ function handleHighRiskCommand(data, command) {
195
195
  function emitShellWarnings(data, command, highRiskWarnings, shellSafetyWarnings) {
196
196
  const sections = []
197
197
  if (highRiskWarnings.length > 0) {
198
- sections.push(`⚠️ [HelloAGENTS 高风险链路提醒] 检测到高风险命令:\n${highRiskWarnings.map((warning) => ` - ${warning}`).join('\n')}\n请确认已完成相应规划/审查并获得必要授权。`)
198
+ sections.push(`⚠️ [HelloAGENTS 高风险操作提醒] 检测到高风险命令:\n${highRiskWarnings.map((warning) => ` - ${warning}`).join('\n')}\n请确认已完成相应规划/审查并获得必要授权。`)
199
199
  }
200
200
  if (shellSafetyWarnings.length > 0) {
201
201
  sections.push(`⚠️ [HelloAGENTS Shell 安全提醒] 检测到建议调整的命令写法:\n${shellSafetyWarnings.map((warning) => ` - ${warning}`).join('\n')}\n当前仅提示,不中断执行。`)
@@ -74,8 +74,8 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
74
74
  const stateSyncHint = buildStateSyncHint(cwd, workflowOptions);
75
75
  if (stateSnapshot.exists && stateSnapshot.content) {
76
76
  summaryParts.push('');
77
- summaryParts.push(`## 恢复快照(从 ${stateSnapshot.statePath.replace(/\\/g, '/')} 读取,只用于找回上次停在哪)`);
78
- summaryParts.push('恢复时先看当前用户消息,确认仍是同一任务再按 STATE.md 接续。');
77
+ summaryParts.push(`## 状态文件(从 ${stateSnapshot.statePath.replace(/\\/g, '/')} 读取,只用于找回上次停在哪)`);
78
+ summaryParts.push('恢复时先看当前用户消息;如果仍是同一任务,再参考状态文件。');
79
79
  summaryParts.push(stateSnapshot.content);
80
80
  }
81
81
 
@@ -109,7 +109,7 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
109
109
 
110
110
  if (stateSyncHint) {
111
111
  summaryParts.push('');
112
- summaryParts.push('## STATE.md 提醒');
112
+ summaryParts.push('## 状态文件提醒');
113
113
  summaryParts.push(stateSyncHint);
114
114
  }
115
115
 
@@ -140,10 +140,10 @@ export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host,
140
140
  if (projectStorageBlock) context += `\n\n${projectStorageBlock}`;
141
141
  if (workflowHint) context += `\n\n## 当前工作流提示\n${workflowHint}`;
142
142
  if (capabilityHint) context += `\n\n## 当前按需能力\n${capabilityHint}`;
143
- if (stateSyncHint) context += `\n\n## STATE.md 提醒\n${stateSyncHint}`;
143
+ if (stateSyncHint) context += `\n\n## 状态文件提醒\n${stateSyncHint}`;
144
144
  context += settingsBlock;
145
145
  if (source === 'resume' || source === 'compact') {
146
- context += `\n\n> ⚠️ 会话已恢复/压缩,请先读取当前 \`state_path\` 指向的 \`${stateSnapshot.statePath.replace(/\\/g, '/')}\` 恢复工作状态;先看当前用户消息确认仍是同一任务,再按 STATE.md 接续。`;
146
+ context += `\n\n> ⚠️ 会话已恢复/压缩,请先读取 \`state_path\` 指向的 \`${stateSnapshot.statePath.replace(/\\/g, '/')}\`;先看当前用户消息,如果仍是同一任务,再参考状态文件。`;
147
147
  }
148
148
  return context;
149
149
  }
@@ -168,8 +168,8 @@ export function buildSemanticRouteInstruction(cwd, payload = {}) {
168
168
  return [
169
169
  '当前消息未使用 ~command。',
170
170
  '请根据用户请求的真实意图选路,不依赖关键词表。',
171
- 'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆链路。',
172
- '路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动编排并自动衔接后续阶段。',
171
+ 'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆操作。',
172
+ '路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动选择并继续执行后续阶段。',
173
173
  '若判定为 T3,默认先走 ~plan / ~prd;纯审查/验证请求才优先 ~verify。',
174
174
  `涉及 UI 任务时,设计决策优先级:当前活跃 plan / PRD → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → 通用 UI 规则。`,
175
175
  projectStorageHint,
@@ -5,11 +5,3 @@ export function shouldIgnoreCodexNotifyClient(client) {
5
5
  export function shouldIgnoreFormattedSubagent(lastMsg, outputFormatEnabled) {
6
6
  return outputFormatEnabled && !lastMsg.includes('【HelloAGENTS】');
7
7
  }
8
-
9
- export function claimsTaskComplete(lastMsg) {
10
- if (!lastMsg) return false;
11
- if (/^✅【HelloAGENTS】- .*(当前任务已完成|任务已完成|已修复|完成交付|done|fixed|completed|finished)/im.test(lastMsg)) {
12
- return true;
13
- }
14
- return /(当前任务已完成|任务已完成|已全部完成|已修复|修复完成|\b(done|fixed|completed|finished)\b)/i.test(lastMsg);
15
- }
@@ -0,0 +1,128 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { spawnSync } from 'node:child_process'
3
+
4
+ function truncateText(value = '') {
5
+ const text = String(value || '').trim()
6
+ return text.length > 1000 ? `${text.slice(0, 1000)}\n...(truncated)` : text
7
+ }
8
+
9
+ function buildGateErrorReason(source, detail = '') {
10
+ return [
11
+ `[HelloAGENTS Runtime] ${source} 执行失败,已暂停完成通知。`,
12
+ detail ? `原因:${detail}` : '',
13
+ '请修复脚本或重新运行验证后再报告完成。',
14
+ ].filter(Boolean).join('\n')
15
+ }
16
+
17
+ function emitGateError({
18
+ payload,
19
+ host,
20
+ source,
21
+ reason,
22
+ appendReplayEvent,
23
+ output,
24
+ }) {
25
+ appendReplayEvent(payload.cwd || process.cwd(), {
26
+ host,
27
+ event: 'runtime_gate_error',
28
+ source,
29
+ reason,
30
+ })
31
+ output({
32
+ decision: 'block',
33
+ reason,
34
+ suppressOutput: true,
35
+ })
36
+ return true
37
+ }
38
+
39
+ export function runGateScript({
40
+ payload,
41
+ host,
42
+ scriptPath,
43
+ args = [],
44
+ source,
45
+ blockEvent,
46
+ timeout,
47
+ appendReplayEvent,
48
+ output,
49
+ }) {
50
+ if (!existsSync(scriptPath)) {
51
+ return emitGateError({
52
+ payload,
53
+ host,
54
+ source,
55
+ reason: buildGateErrorReason(source, `脚本不存在:${scriptPath}`),
56
+ appendReplayEvent,
57
+ output,
58
+ })
59
+ }
60
+
61
+ const result = spawnSync(process.execPath, [scriptPath, ...args], {
62
+ input: JSON.stringify(payload),
63
+ encoding: 'utf-8',
64
+ timeout,
65
+ })
66
+
67
+ if (result.error) {
68
+ return emitGateError({
69
+ payload,
70
+ host,
71
+ source,
72
+ reason: buildGateErrorReason(source, result.error.message),
73
+ appendReplayEvent,
74
+ output,
75
+ })
76
+ }
77
+
78
+ if (result.status !== 0) {
79
+ const detail = truncateText(`${result.stderr || ''}\n${result.stdout || ''}`) || `退出码 ${result.status}`
80
+ return emitGateError({
81
+ payload,
82
+ host,
83
+ source,
84
+ reason: buildGateErrorReason(source, detail),
85
+ appendReplayEvent,
86
+ output,
87
+ })
88
+ }
89
+
90
+ const stdout = String(result.stdout || '').trim()
91
+ if (!stdout) {
92
+ return emitGateError({
93
+ payload,
94
+ host,
95
+ source,
96
+ reason: buildGateErrorReason(source, '脚本未返回有效结果'),
97
+ appendReplayEvent,
98
+ output,
99
+ })
100
+ }
101
+
102
+ let gateOutput
103
+ try {
104
+ gateOutput = JSON.parse(stdout)
105
+ } catch {
106
+ return emitGateError({
107
+ payload,
108
+ host,
109
+ source,
110
+ reason: buildGateErrorReason(source, `脚本返回了无法解析的 JSON:${truncateText(stdout)}`),
111
+ appendReplayEvent,
112
+ output,
113
+ })
114
+ }
115
+
116
+ if (gateOutput.decision === 'block') {
117
+ appendReplayEvent(payload.cwd || process.cwd(), {
118
+ host,
119
+ event: blockEvent,
120
+ source,
121
+ reason: gateOutput.reason || '',
122
+ })
123
+ output(gateOutput)
124
+ return true
125
+ }
126
+
127
+ return false
128
+ }
@@ -18,6 +18,7 @@ const NOTIFY_MESSAGES = {
18
18
  };
19
19
 
20
20
  const WIN_APPID = 'HelloAgents.Notification';
21
+ const DISABLE_OS_NOTIFICATIONS = process.env.HELLOAGENTS_DISABLE_OS_NOTIFICATIONS === '1';
21
22
 
22
23
  function escapeToastText(value = '') {
23
24
  return String(value)
@@ -55,6 +56,7 @@ function resolveWav(pkgRoot, event) {
55
56
  }
56
57
 
57
58
  export function playSound(pkgRoot, event) {
59
+ if (DISABLE_OS_NOTIFICATIONS) return;
58
60
  const wav = resolveWav(pkgRoot, event);
59
61
  if (!wav) { process.stderr.write('\x07'); return; }
60
62
  try {
@@ -83,6 +85,7 @@ function ensureWinAppId(pkgRoot) {
83
85
  }
84
86
 
85
87
  export function desktopNotify(pkgRoot, event, extra) {
88
+ if (DISABLE_OS_NOTIFICATIONS) return;
86
89
  const notification = buildDesktopNotificationContent(event, extra);
87
90
  try {
88
91
  if (PLAT === 'win32') {
@@ -4,13 +4,13 @@
4
4
 
5
5
  import { join, dirname } from 'node:path';
6
6
  import { existsSync, readFileSync } from 'node:fs';
7
- import { spawnSync } from 'node:child_process';
8
7
  import { fileURLToPath } from 'node:url';
9
8
  import { homedir } from 'node:os';
10
9
  import { playSound as _playSound, desktopNotify as _desktopNotify } from './notify-ui.mjs';
11
10
  import { resolveNotificationSource } from './notify-source.mjs';
12
11
  import { buildCompactionContext, buildInjectContext, buildRouteInstruction, buildSemanticRouteInstruction, resolveCanonicalCommandSkill } from './notify-context.mjs';
13
- import { claimsTaskComplete, shouldIgnoreCodexNotifyClient } from './notify-events.mjs';
12
+ import { shouldIgnoreCodexNotifyClient } from './notify-events.mjs';
13
+ import { runGateScript } from './notify-gates.mjs';
14
14
  import { handleRouteCommand, resolveBootstrapFile } from './notify-route.mjs';
15
15
  import { readSettings, readStdinJson, output, suppressedOutput, emptySuppress } from './notify-shared.mjs';
16
16
  import { clearRouteContext, writeRouteContext } from './runtime-context.mjs';
@@ -37,6 +37,17 @@ const EVENT_NAME = {
37
37
  const playSound = (event) => _playSound(PKG_ROOT, event);
38
38
  const desktopNotify = (event, extra) => _desktopNotify(PKG_ROOT, event, extra);
39
39
 
40
+ function normalizeNotifyLevel(value) {
41
+ const level = Number(value);
42
+ return [0, 1, 2, 3].includes(level) ? level : 0;
43
+ }
44
+
45
+ function notifyByLevel(event, extra, settings = getSettings()) {
46
+ const level = normalizeNotifyLevel(settings.notify_level ?? 0);
47
+ if (level === 2 || level === 3) playSound(event);
48
+ if (level === 1 || level === 3) desktopNotify(event, extra);
49
+ }
50
+
40
51
  function buildNotifyExtra(payload = {}, options = {}) {
41
52
  const source = resolveNotificationSource({
42
53
  host: HOST,
@@ -56,63 +67,30 @@ function getSettings() {
56
67
  function runRalphLoop(payload) {
57
68
  const settings = getSettings();
58
69
  if (settings.ralph_loop_enabled === false) return false;
59
- try {
60
- const rlPath = join(__dirname, 'ralph-loop.mjs');
61
- if (!existsSync(rlPath)) return false;
62
- const hostFlag = IS_GEMINI ? ['--gemini'] : HOST === 'codex' ? ['--codex'] : [];
63
- const result = spawnSync(process.execPath, [rlPath, ...hostFlag], {
64
- input: JSON.stringify(payload),
65
- encoding: 'utf-8',
66
- timeout: 120_000,
67
- });
68
- if (result.stdout) {
69
- const rlOut = JSON.parse(result.stdout);
70
- if (rlOut.decision === 'block') {
71
- appendReplayEvent(payload.cwd || process.cwd(), {
72
- host: HOST,
73
- event: 'verify_gate_blocked',
74
- source: 'ralph-loop',
75
- reason: rlOut.reason || '',
76
- });
77
- output(rlOut);
78
- return true;
79
- }
80
- }
81
- } catch {}
82
- return false;
70
+ return runGateScript({
71
+ payload,
72
+ host: HOST,
73
+ scriptPath: join(__dirname, 'ralph-loop.mjs'),
74
+ args: IS_GEMINI ? ['--gemini'] : HOST === 'codex' ? ['--codex'] : [],
75
+ source: 'ralph-loop',
76
+ blockEvent: 'verify_gate_blocked',
77
+ timeout: 120_000,
78
+ appendReplayEvent,
79
+ output,
80
+ });
83
81
  }
84
82
 
85
83
  function runDeliveryGate(payload) {
86
- try {
87
- const gatePath = join(__dirname, 'delivery-gate.mjs');
88
- if (!existsSync(gatePath)) return false;
89
- const result = spawnSync(process.execPath, [gatePath], {
90
- input: JSON.stringify(payload),
91
- encoding: 'utf-8',
92
- timeout: 30_000,
93
- });
94
- if (result.stdout) {
95
- const gateOut = JSON.parse(result.stdout);
96
- if (gateOut.decision === 'block') {
97
- appendReplayEvent(payload.cwd || process.cwd(), {
98
- host: HOST,
99
- event: 'delivery_gate_blocked',
100
- source: 'delivery-gate',
101
- reason: gateOut.reason || '',
102
- });
103
- output(gateOut);
104
- return true;
105
- }
106
- }
107
- } catch {}
108
- return false;
109
- }
110
-
111
- function readCompletionText(payload = {}) {
112
- return payload['last-assistant-message']
113
- || payload.last_assistant_message
114
- || payload.lastAssistantMessage
115
- || '';
84
+ return runGateScript({
85
+ payload,
86
+ host: HOST,
87
+ scriptPath: join(__dirname, 'delivery-gate.mjs'),
88
+ source: 'delivery-gate',
89
+ blockEvent: 'delivery_gate_blocked',
90
+ timeout: 30_000,
91
+ appendReplayEvent,
92
+ output,
93
+ });
116
94
  }
117
95
 
118
96
  function readMainTurnState(cwd) {
@@ -124,9 +102,9 @@ function consumeMainTurnState(cwd, turnState) {
124
102
  if (turnState?.role === 'main') clearTurnState(cwd);
125
103
  }
126
104
 
127
- function shouldProcessCloseout(turnState, lastMsg) {
105
+ function shouldProcessCloseout(turnState) {
128
106
  if (turnState) return turnState.kind === 'complete';
129
- return claimsTaskComplete(lastMsg);
107
+ return false;
130
108
  }
131
109
 
132
110
  function cmdPreCompact() {
@@ -217,29 +195,24 @@ function cmdInject() {
217
195
 
218
196
  function cmdStop() {
219
197
  const payload = readStdinJson();
220
- const lastMsg = readCompletionText(payload);
221
198
  const cwd = payload.cwd || process.cwd();
222
199
  const turnState = readMainTurnState(cwd);
223
- const shouldProcess = shouldProcessCloseout(turnState, lastMsg);
200
+ const shouldProcess = shouldProcessCloseout(turnState);
224
201
  clearRouteContext();
225
202
  if (shouldProcess && runRalphLoop(payload)) {
226
203
  consumeMainTurnState(cwd, turnState);
227
- playSound('warning');
228
- desktopNotify('warning', buildNotifyExtra(payload));
204
+ notifyByLevel('warning', buildNotifyExtra(payload));
229
205
  return;
230
206
  }
231
207
  if (shouldProcess && runDeliveryGate(payload)) {
232
208
  consumeMainTurnState(cwd, turnState);
233
- playSound('warning');
234
- desktopNotify('warning', buildNotifyExtra(payload));
209
+ notifyByLevel('warning', buildNotifyExtra(payload));
235
210
  return;
236
211
  }
237
212
 
238
213
  const settings = getSettings();
239
- const level = settings.notify_level ?? 0;
240
214
  if (shouldProcess) {
241
- if (level === 2 || level === 3) playSound('complete');
242
- if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(payload));
215
+ notifyByLevel('complete', buildNotifyExtra(payload), settings);
243
216
  }
244
217
  consumeMainTurnState(cwd, turnState);
245
218
  emptySuppress();
@@ -262,8 +235,7 @@ function cmdCodexNotify() {
262
235
  if (shouldIgnoreCodexNotifyClient(client)) return;
263
236
 
264
237
  if (type === 'approval-requested') {
265
- playSound('confirm');
266
- desktopNotify('confirm', buildNotifyExtra(data));
238
+ notifyByLevel('confirm', buildNotifyExtra(data));
267
239
  return;
268
240
  }
269
241
  if (type !== 'agent-turn-complete') return;
@@ -279,20 +251,16 @@ function cmdCodexNotify() {
279
251
  const settings = getSettings();
280
252
  if (runRalphLoop(data)) {
281
253
  consumeMainTurnState(cwd, turnState);
282
- playSound('warning');
283
- desktopNotify('warning', buildNotifyExtra(data));
254
+ notifyByLevel('warning', buildNotifyExtra(data), settings);
284
255
  return;
285
256
  }
286
257
  if (runDeliveryGate(data)) {
287
258
  consumeMainTurnState(cwd, turnState);
288
- playSound('warning');
289
- desktopNotify('warning', buildNotifyExtra(data));
259
+ notifyByLevel('warning', buildNotifyExtra(data), settings);
290
260
  return;
291
261
  }
292
262
 
293
- const level = settings.notify_level ?? 0;
294
- if (level === 2 || level === 3) playSound('complete');
295
- if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(data));
263
+ notifyByLevel('complete', buildNotifyExtra(data), settings);
296
264
  consumeMainTurnState(cwd, turnState);
297
265
  }
298
266
 
@@ -277,9 +277,9 @@ export function describeProjectStoreFile(cwd, relativePath = '') {
277
277
  export function buildProjectStorageHint(cwd, options = {}) {
278
278
  const summary = getProjectStoreSummary(cwd, options)
279
279
  const hints = []
280
- hints.push(`当前恢复快照统一写入 \`${summary.promptStatePath}\``)
280
+ hints.push(`当前状态文件写入 \`${summary.promptStatePath}\``)
281
281
  if (summary.stateSessionMode === 'default') {
282
- hints.push(`当前宿主未提供稳定会话标识,因此落到分支级默认会话槽位 \`${summary.stateSessionToken}\``)
282
+ hints.push(`当前宿主未提供稳定会话标识,因此使用分支默认位置 \`${summary.stateSessionToken}\``)
283
283
  }
284
284
  if (summary.usesSharedStore) {
285
285
  hints.push(`项目存储:\`project_store_mode=repo-shared\`;本地激活/运行态目录仍是 \`${summary.promptActivationDir}\`,知识库/方案目录改为 \`${summary.promptStoreDir}\``)
@@ -307,12 +307,12 @@ export function buildProjectStorageBlock(cwd, options = {}) {
307
307
  }
308
308
 
309
309
  const explanations = []
310
- explanations.push('说明:恢复快照只认 `state_path` 这一个权威路径,不再读写旧的项目级 `.helloagents/STATE.md`。')
310
+ explanations.push('说明:状态文件只使用 `state_path`。')
311
311
  if (summary.stateSessionMode === 'default') {
312
- explanations.push('说明:当前宿主未提供稳定会话标识,因此自动使用分支级默认会话槽位,仍保持新目录结构。')
312
+ explanations.push('说明:当前宿主未提供稳定会话标识,因此使用分支默认位置。')
313
313
  }
314
314
  if (summary.usesSharedStore) {
315
- explanations.push('说明:`STATE.md` `.ralph-*.json` 继续写本地激活目录;`context.md`、`guidelines.md`、`DESIGN.md`、`verify.yaml`、`modules/`、`plans/`、`archive/` 写知识库/方案目录。')
315
+ explanations.push('说明:状态文件与 `.ralph-*.json` 写本地激活目录;`context.md`、`guidelines.md`、`DESIGN.md`、`verify.yaml`、`modules/`、`plans/`、`archive/` 写知识库/方案目录。')
316
316
  } else {
317
317
  explanations.push('说明:当前使用项目本地 `.helloagents/` 作为激活目录、知识库目录和方案目录。')
318
318
  }
@@ -16,9 +16,9 @@ export function getTargetPlans(snapshot) {
16
16
 
17
17
  function describeStateLabel(state) {
18
18
  if (state.stateSessionMode === 'default') {
19
- return '当前分支默认会话槽位的 `STATE.md`'
19
+ return '当前分支默认位置的状态文件'
20
20
  }
21
- return '当前会话的 `STATE.md`'
21
+ return '当前会话的状态文件'
22
22
  }
23
23
  export function classifyPlan(plan) {
24
24
  if (!plan) {
@@ -140,12 +140,12 @@ export function buildVerifyModeHintFromSnapshot(snapshot) {
140
140
  export function buildStateSyncHintFromSnapshot(snapshot) {
141
141
  const issues = collectStateSyncIssues(snapshot)
142
142
  if (issues.length === 0) return ''
143
- return `STATE.md 提醒:${issues.join(';')};继续项目级流程、收尾或进入压缩前先同步恢复快照。`
143
+ return `状态文件提醒:${issues.join(';')};继续项目级流程、收尾或进入压缩前先同步状态文件。`
144
144
  }
145
145
 
146
146
  export function buildStateRoleHintFromSnapshot(snapshot) {
147
147
  if (!snapshot.state.exists || snapshot.plans.length > 0) return ''
148
- return `恢复约束:当前仅检测到${describeStateLabel(snapshot.state)};先以当前用户消息、显式命令和代码事实确认主线,STATE.md 只用于找回上次停在哪,不是当前任务的自动授权或唯一判断依据。`
148
+ return `恢复约束:当前仅检测到${describeStateLabel(snapshot.state)};先以当前用户消息、显式命令和代码事实确认当前任务。状态文件只用于找回上次停在哪,不是当前任务的自动授权或唯一判断依据。`
149
149
  }
150
150
 
151
151
  export function buildUiContractHint(cwd, snapshot) {
@@ -167,7 +167,7 @@ export function buildUiContractHint(cwd, snapshot) {
167
167
  if (visualValidationRequired) {
168
168
  extraHints.push('若当前 UI 契约要求视觉验收,收尾前需写 `.helloagents/.ralph-visual.json` 记录关键视口、状态与结论')
169
169
  }
170
- return `UI 约束提示:如本次属于视觉/交互链路,设计决策优先级固定为:当前活跃 plan.md / prd/03-ui-design.md → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → hello-ui。${extraHints.length > 0 ? ` ${extraHints.join(';')}。` : ''}`
170
+ return `UI 约束提示:如本次属于视觉/交互任务,设计决策优先级固定为:当前活跃 plan.md / prd/03-ui-design.md → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → hello-ui。${extraHints.length > 0 ? ` ${extraHints.join(';')}。` : ''}`
171
171
  }
172
172
 
173
173
  export { normalizeTaskFile, readStateSnapshot, listPlanPackages, getWorkflowSnapshot }
@@ -59,7 +59,7 @@ function buildConsolidateAction(recommendation) {
59
59
  phase: 'consolidate',
60
60
  mode: recommendation.mode,
61
61
  routeHint: recommendation.guidance,
62
- gateHint: '交付把关:审查与验证证据已满足;先写 `.helloagents/.ralph-closeout.json` 记录需求覆盖与交付清单,再完成 STATE.md / 归档后才可交付。',
62
+ gateHint: '交付把关:审查与验证证据已满足;先写 `.helloagents/.ralph-closeout.json` 记录需求覆盖与交付清单,再更新 `state_path` 并归档后才可交付。',
63
63
  }
64
64
  }
65
65
 
@@ -67,7 +67,7 @@ function buildConsolidateAction(recommendation) {
67
67
  phase: 'consolidate',
68
68
  mode: recommendation.mode || 'ready',
69
69
  routeHint: recommendation.guidance,
70
- gateHint: '交付把关:当前已具备收尾证据;完成 STATE.md、知识沉淀与归档后即可交付。',
70
+ gateHint: '交付把关:当前已具备收尾证据;更新 `state_path`、知识文件并归档后即可交付。',
71
71
  }
72
72
  }
73
73
 
@@ -239,8 +239,8 @@ function buildClosedRecommendation(scopeLabel, plan, cwd) {
239
239
  ? `${scopeLabel} "${plan.planName}" 的任务与交付证据已闭合。`
240
240
  : `${scopeLabel} "${plan.planName}" 的任务、审查与验证已闭合。`,
241
241
  guidance: closedPlanEvidence.closeoutReady
242
- ? '当前进入 CONSOLIDATE:完成 `STATE.md`、知识沉淀与方案归档后即可交付;不要无故重开新的方案包或重新跑一遍无关验证。'
243
- : '当前进入 CONSOLIDATE:先写 `.helloagents/.ralph-closeout.json` 记录需求覆盖与交付清单,再同步 `STATE.md` / 归档后交付。',
242
+ ? '当前进入 CONSOLIDATE:更新 `state_path`、知识文件并归档方案后即可交付;不要无故重开新的方案包或重新跑一遍无关验证。'
243
+ : '当前进入 CONSOLIDATE:先写 `.helloagents/.ralph-closeout.json` 记录需求覆盖与交付清单,再更新 `state_path` 并归档后交付。',
244
244
  }
245
245
  }
246
246
 
@@ -58,7 +58,7 @@ function buildCommandRouteMessage(skillName, recommendation, verifyModeHint) {
58
58
  if (skillName === 'auto') {
59
59
  return recommendation.stage === 'consolidate'
60
60
  ? `当前工作流约束:${recommendation.summary} 当前建议下一阶段:CONSOLIDATE。${recommendation.guidance} 若本次明确使用 ~auto,则在未命中阻塞判定时直接完成当前收尾,不再额外停下询问。`
61
- : `当前工作流约束:${recommendation.summary} 当前建议主路径:${recommendation.nextPath}。${recommendation.guidance} 若本次明确使用 ~auto,则命中主路径后继续衔接后续阶段,除非触发阻塞判定,否则不要在方案/PRD 阶段额外停下。`
61
+ : `当前工作流约束:${recommendation.summary} 当前建议主路径:${recommendation.nextPath}。${recommendation.guidance} 若本次明确使用 ~auto,则命中主路径后继续执行后续阶段,除非触发阻塞判定,否则不要在方案/PRD 阶段额外停下。`
62
62
  }
63
63
  if (skillName === 'plan') {
64
64
  if (recommendation.stage === 'consolidate') {