helloagents 3.0.7 → 3.0.8-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 (40) 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 +62 -55
  5. package/README_CN.md +56 -49
  6. package/bootstrap-lite.md +42 -25
  7. package/bootstrap.md +46 -30
  8. package/gemini-extension.json +1 -1
  9. package/package.json +12 -2
  10. package/scripts/capability-registry.mjs +4 -4
  11. package/scripts/cli-codex-config.mjs +49 -55
  12. package/scripts/cli-codex.mjs +67 -77
  13. package/scripts/cli-doctor.mjs +20 -17
  14. package/scripts/cli-messages.mjs +1 -1
  15. package/scripts/cli-toml.mjs +30 -0
  16. package/scripts/guard-rules.mjs +26 -1
  17. package/scripts/guard.mjs +38 -10
  18. package/scripts/notify-context.mjs +2 -7
  19. package/scripts/notify.mjs +19 -8
  20. package/scripts/turn-state.mjs +173 -0
  21. package/scripts/workflow-core.mjs +6 -6
  22. package/scripts/workflow-recommendation.mjs +14 -14
  23. package/scripts/workflow-state.mjs +2 -2
  24. package/skills/_meta/SKILL.md +1 -1
  25. package/skills/commands/auto/SKILL.md +24 -9
  26. package/skills/commands/build/SKILL.md +3 -3
  27. package/skills/commands/clean/SKILL.md +3 -3
  28. package/skills/commands/help/SKILL.md +3 -3
  29. package/skills/commands/idea/SKILL.md +2 -2
  30. package/skills/commands/init/SKILL.md +12 -7
  31. package/skills/commands/loop/SKILL.md +1 -1
  32. package/skills/commands/plan/SKILL.md +11 -9
  33. package/skills/commands/prd/SKILL.md +8 -6
  34. package/skills/commands/verify/SKILL.md +5 -5
  35. package/skills/commands/wiki/SKILL.md +8 -10
  36. package/skills/hello-review/SKILL.md +1 -1
  37. package/skills/hello-subagent/SKILL.md +3 -2
  38. package/skills/hello-ui/SKILL.md +12 -12
  39. package/skills/hello-verify/SKILL.md +6 -5
  40. package/skills/helloagents/SKILL.md +17 -12
@@ -140,8 +140,8 @@ function inspectClaudeDoctor(settings) {
140
140
  issues.push(buildDoctorIssue('tracked-mode-mismatch', '记录模式与检测模式不一致', 'Tracked mode does not match detected mode'))
141
141
  }
142
142
  if (detectedMode === 'standby') {
143
- if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby 载体缺少 HELLOAGENTS 标记', 'Standby carrier is missing the HELLOAGENTS marker'))
144
- if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby 载体内容与当前 bootstrap-lite.md 不一致', 'Standby carrier content differs from the current bootstrap-lite.md'))
143
+ if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby 规则文件缺少 HELLOAGENTS 标记', 'Standby carrier is missing the HELLOAGENTS marker'))
144
+ if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby 规则文件内容与当前 bootstrap-lite.md 不一致', 'Standby carrier content differs from the current bootstrap-lite.md'))
145
145
  if (!checks.homeLink) issues.push(buildDoctorIssue('standby-link-missing', 'standby home 链接缺失或未指向当前包根目录', 'Standby home link is missing or points to a different package root'))
146
146
  if (!checks.settingsHooks) issues.push(buildDoctorIssue('standby-hooks-missing', 'standby settings hooks 缺失', 'Standby settings hooks are missing'))
147
147
  if (checks.settingsHooks && !checks.settingsHooksMatch) issues.push(buildDoctorIssue('standby-hooks-drift', 'standby settings hooks 与当前 hooks 配置不一致', 'Standby settings hooks differ from the current hook configuration'))
@@ -160,7 +160,7 @@ function inspectClaudeDoctor(settings) {
160
160
  issues.push(buildDoctorIssue('untracked-managed-state', '检测到受管状态,但配置中未记录该 CLI 模式', 'Managed state detected but this CLI mode is not tracked in config'))
161
161
  }
162
162
  if (trackedMode !== 'none' && detectedMode === 'none' && trackedMode !== 'global') {
163
- issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI 已安装,但未检测到对应受管痕迹', 'Config says this CLI is installed, but no managed artifacts were detected'))
163
+ issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI 已安装,但未检测到对应的受管文件或配置', 'Config says this CLI is installed, but no managed artifacts were detected'))
164
164
  }
165
165
 
166
166
  const status = summarizeDoctorStatus(issues, { host, trackedMode, detectedMode })
@@ -188,8 +188,8 @@ function inspectGeminiDoctor(settings) {
188
188
  issues.push(buildDoctorIssue('tracked-mode-mismatch', '记录模式与检测模式不一致', 'Tracked mode does not match detected mode'))
189
189
  }
190
190
  if (detectedMode === 'standby') {
191
- if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby 载体缺少 HELLOAGENTS 标记', 'Standby carrier is missing the HELLOAGENTS marker'))
192
- if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby 载体内容与当前 bootstrap-lite.md 不一致', 'Standby carrier content differs from the current bootstrap-lite.md'))
191
+ if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby 规则文件缺少 HELLOAGENTS 标记', 'Standby carrier is missing the HELLOAGENTS marker'))
192
+ if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby 规则文件内容与当前 bootstrap-lite.md 不一致', 'Standby carrier content differs from the current bootstrap-lite.md'))
193
193
  if (!checks.homeLink) issues.push(buildDoctorIssue('standby-link-missing', 'standby home 链接缺失或未指向当前包根目录', 'Standby home link is missing or points to a different package root'))
194
194
  if (!checks.settingsHooks) issues.push(buildDoctorIssue('standby-hooks-missing', 'standby settings hooks 缺失', 'Standby settings hooks are missing'))
195
195
  if (checks.settingsHooks && !checks.settingsHooksMatch) issues.push(buildDoctorIssue('standby-hooks-drift', 'standby settings hooks 与当前 hooks 配置不一致', 'Standby settings hooks differ from the current hook configuration'))
@@ -207,7 +207,7 @@ function inspectGeminiDoctor(settings) {
207
207
  issues.push(buildDoctorIssue('untracked-managed-state', '检测到受管状态,但配置中未记录该 CLI 模式', 'Managed state detected but this CLI mode is not tracked in config'))
208
208
  }
209
209
  if (trackedMode !== 'none' && detectedMode === 'none' && trackedMode !== 'global') {
210
- issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI 已安装,但未检测到对应受管痕迹', 'Config says this CLI is installed, but no managed artifacts were detected'))
210
+ issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI 已安装,但未检测到对应的受管文件或配置', 'Config says this CLI is installed, but no managed artifacts were detected'))
211
211
  }
212
212
 
213
213
  const status = summarizeDoctorStatus(issues, { host, trackedMode, detectedMode })
@@ -215,20 +215,20 @@ function inspectGeminiDoctor(settings) {
215
215
  }
216
216
 
217
217
  function appendCodexStandbyIssues(issues, checks) {
218
- if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby 载体缺少 HELLOAGENTS 标记', 'Standby carrier is missing the HELLOAGENTS marker'))
219
- if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby 载体内容与当前 bootstrap-lite.md 不一致', 'Standby carrier content differs from the current bootstrap-lite.md'))
218
+ if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby 规则文件缺少 HELLOAGENTS 标记', 'Standby carrier is missing the HELLOAGENTS marker'))
219
+ if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby 规则文件内容与当前 bootstrap-lite.md 不一致', 'Standby carrier content differs from the current bootstrap-lite.md'))
220
220
  if (!checks.homeLink) issues.push(buildDoctorIssue('standby-link-missing', 'standby home 链接缺失或未指向当前包根目录', 'Standby home link is missing or points to a different package root'))
221
- if (checks.modelInstructionsFile) issues.push(buildDoctorIssue('standby-model-instructions-shadow', 'standby config 中仍存在 model_instructions_file,可能覆盖 HelloAGENTS 载体', 'Standby config still contains model_instructions_file, which can shadow the HelloAGENTS carrier'))
221
+ if (!checks.modelInstructionsFile) issues.push(buildDoctorIssue('standby-model-instructions-missing', 'standby config 缺少受管 model_instructions_file', 'Standby config is missing the managed model_instructions_file'))
222
+ if (checks.modelInstructionsFile && !checks.modelInstructionsPathMatch) issues.push(buildDoctorIssue('standby-model-instructions-drift', 'standby model_instructions_file 未指向受管 `~/.codex/AGENTS.md`', 'Standby model_instructions_file does not point to the managed `~/.codex/AGENTS.md`'))
222
223
  if (!checks.codexNotify) issues.push(buildDoctorIssue('standby-notify-missing', 'standby notify 配置缺失', 'Standby notify configuration is missing'))
223
224
  if (checks.codexNotify && !checks.notifyPathMatch) issues.push(buildDoctorIssue('standby-notify-drift', 'standby notify 路径未指向当前包根目录', 'Standby notify path does not point to the current package root'))
224
- if (!checks.developerInstructions) issues.push(buildDoctorIssue('standby-developer-instructions-missing', 'standby developer_instructions 缺失', 'Standby developer_instructions block is missing'))
225
225
  if (checks.pluginRoot || checks.pluginCache || checks.marketplaceEntry || checks.pluginEnabled || checks.globalNotifyPath) {
226
- issues.push(buildDoctorIssue('standby-global-residue', 'standby 模式下仍残留 global 插件链路', 'Global plugin artifacts still remain while Codex is in standby mode'))
226
+ issues.push(buildDoctorIssue('standby-global-residue', 'standby 模式下仍残留 global 插件文件或配置', 'Global plugin artifacts still remain while Codex is in standby mode'))
227
227
  }
228
228
  }
229
229
 
230
230
  function appendCodexGlobalIssues(issues, checks, pluginVersion, cacheVersion) {
231
- if (!checks.carrierMarker) issues.push(buildDoctorIssue('global-home-carrier-missing', 'global `~/.codex/AGENTS.md` 缺少 HelloAGENTS 载体', 'Global `~/.codex/AGENTS.md` is missing the HelloAGENTS carrier'))
231
+ if (!checks.carrierMarker) issues.push(buildDoctorIssue('global-home-carrier-missing', 'global `~/.codex/AGENTS.md` 缺少 HelloAGENTS 规则内容', 'Global `~/.codex/AGENTS.md` is missing the HelloAGENTS carrier'))
232
232
  if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('global-home-carrier-drift', 'global `~/.codex/AGENTS.md` 与当前 bootstrap.md 不一致', 'Global `~/.codex/AGENTS.md` differs from the current bootstrap.md'))
233
233
  if (!checks.pluginRoot) issues.push(buildDoctorIssue('global-plugin-root-missing', 'global 插件根目录缺失', 'Global plugin root is missing'))
234
234
  if (!checks.pluginCache) issues.push(buildDoctorIssue('global-plugin-cache-missing', 'global 插件缓存目录缺失', 'Global plugin cache directory is missing'))
@@ -236,10 +236,10 @@ function appendCodexGlobalIssues(issues, checks, pluginVersion, cacheVersion) {
236
236
  if (checks.pluginCache && !checks.pluginCacheCarrierMatch) issues.push(buildDoctorIssue('global-plugin-cache-carrier-drift', 'global 插件缓存中的 AGENTS.md 与当前 bootstrap.md 不一致', 'Global plugin cache AGENTS.md differs from the current bootstrap.md'))
237
237
  if (!checks.marketplaceEntry) issues.push(buildDoctorIssue('global-marketplace-missing', 'global marketplace 条目缺失', 'Global marketplace entry is missing'))
238
238
  if (!checks.pluginEnabled) issues.push(buildDoctorIssue('global-plugin-disabled', 'global config 中缺少插件启用段', 'Global plugin enablement block is missing from config'))
239
- if (checks.modelInstructionsFile) issues.push(buildDoctorIssue('global-model-instructions-shadow', 'global config 中仍存在 model_instructions_file,可能覆盖 HelloAGENTS 载体', 'Global config still contains model_instructions_file, which can shadow the HelloAGENTS carrier'))
239
+ if (!checks.modelInstructionsFile) issues.push(buildDoctorIssue('global-model-instructions-missing', 'global config 缺少受管 model_instructions_file', 'Global config is missing the managed model_instructions_file'))
240
+ if (checks.modelInstructionsFile && !checks.modelInstructionsPathMatch) issues.push(buildDoctorIssue('global-model-instructions-drift', 'global model_instructions_file 未指向受管 `~/.codex/AGENTS.md`', 'Global model_instructions_file does not point to the managed `~/.codex/AGENTS.md`'))
240
241
  if (!checks.globalNotifyPath) issues.push(buildDoctorIssue('global-notify-missing', 'global notify 路径缺失', 'Global notify path is missing'))
241
242
  if (checks.globalNotifyPath && !checks.globalNotifyPathMatch) issues.push(buildDoctorIssue('global-notify-drift', 'global notify 路径未指向当前插件根目录', 'Global notify path does not point to the current plugin root'))
242
- if (!checks.developerInstructions) issues.push(buildDoctorIssue('global-developer-instructions-missing', 'global developer_instructions 缺失', 'Global developer_instructions block is missing'))
243
243
  if (pluginVersion && !checks.pluginVersionMatch) issues.push(buildDoctorIssue('global-plugin-version-drift', 'global 插件根目录版本与当前包版本不一致', 'Global plugin root version does not match the current package version'))
244
244
  if (cacheVersion && !checks.pluginCacheVersionMatch) issues.push(buildDoctorIssue('global-plugin-cache-version-drift', 'global 插件缓存版本与当前包版本不一致', 'Global plugin cache version does not match the current package version'))
245
245
  if (checks.homeLink) {
@@ -260,6 +260,8 @@ function inspectCodexDoctor(settings) {
260
260
  const cacheVersion = safeJson(join(pluginCacheRoot, 'package.json'))?.version || ''
261
261
  const standbyNotifyPath = normalizePath(join(runtime.pkgRoot, 'scripts', 'notify.mjs'))
262
262
  const globalNotifyPath = normalizePath(join(pluginRoot, 'scripts', 'notify.mjs'))
263
+ const managedHomeCarrierPath = normalizePath(join(codexDir, 'AGENTS.md'))
264
+ const modelInstructionsLine = readTopLevelTomlLine(codexConfig, 'model_instructions_file')
263
265
  const expectedHomeCarrier = (detectedMode === 'global' || (detectedMode === 'none' && trackedMode === 'global'))
264
266
  ? 'bootstrap.md'
265
267
  : 'bootstrap-lite.md'
@@ -267,10 +269,11 @@ function inspectCodexDoctor(settings) {
267
269
  carrierMarker: (safeRead(join(codexDir, 'AGENTS.md')) || '').includes('HELLOAGENTS_START'),
268
270
  carrierContentMatch: extractManagedCarrierContent(join(codexDir, 'AGENTS.md')) === readBootstrapContent(expectedHomeCarrier),
269
271
  homeLink: safeRealTarget(join(codexDir, 'helloagents')) === runtime.pkgRoot,
270
- modelInstructionsFile: !!readTopLevelTomlLine(codexConfig, 'model_instructions_file'),
272
+ modelInstructionsFile: !!modelInstructionsLine,
273
+ modelInstructionsPathMatch: !!modelInstructionsLine
274
+ && normalizePath(modelInstructionsLine).includes(`"${managedHomeCarrierPath}"`),
271
275
  codexNotify: codexConfig.includes('codex-notify'),
272
276
  notifyPathMatch: codexConfig.includes(standbyNotifyPath),
273
- developerInstructions: codexConfig.includes('HelloAGENTS'),
274
277
  pluginRoot: existsSync(pluginRoot),
275
278
  pluginCache: existsSync(pluginCacheRoot),
276
279
  pluginCarrierMatch: normalizeText(safeRead(join(pluginRoot, 'AGENTS.md')) || '') === readBootstrapContent('bootstrap.md'),
@@ -298,7 +301,7 @@ function inspectCodexDoctor(settings) {
298
301
  issues.push(buildDoctorIssue('untracked-managed-state', '检测到受管状态,但配置中未记录该 CLI 模式', 'Managed state detected but this CLI mode is not tracked in config'))
299
302
  }
300
303
  if (trackedMode !== 'none' && detectedMode === 'none') {
301
- issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI 已安装,但未检测到对应受管痕迹', 'Config says this CLI is installed, but no managed artifacts were detected'))
304
+ issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI 已安装,但未检测到对应的受管文件或配置', 'Config says this CLI is installed, but no managed artifacts were detected'))
302
305
  }
303
306
  if (!checks.pluginVersionMatch && !pluginVersion && detectedMode === 'global') {
304
307
  notes.push(runtime.msg('未读到 global 插件根目录版本信息', 'Global plugin root version was not readable'))
@@ -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, model_instructions_file shadowing risks, and version drift')}
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')}
96
96
 
97
97
  ${msg('卸载', 'Uninstall')}:
98
98
  helloagents cleanup ${msg('(推荐先执行,显式清理所有 CLI 注入/链接)', '(recommended first, explicitly cleans CLI injections/links)')}
@@ -33,6 +33,15 @@ function findFirstTomlSectionIndex(text) {
33
33
  return normalized.length;
34
34
  }
35
35
 
36
+ function splitTopLevelToml(text) {
37
+ const normalized = String(text || '').replace(/\r\n/g, '\n');
38
+ const topLevelEnd = findFirstTomlSectionIndex(normalized);
39
+ return {
40
+ topLevel: normalized.slice(0, topLevelEnd),
41
+ sections: normalized.slice(topLevelEnd),
42
+ };
43
+ }
44
+
36
45
  function findTopLevelTomlBlock(text, key) {
37
46
  const normalized = String(text || '').replace(/\r\n/g, '\n');
38
47
  const topLevelEnd = findFirstTomlSectionIndex(normalized);
@@ -93,6 +102,27 @@ export function removeTopLevelTomlBlock(text, key) {
93
102
  return normalizeToml(`${normalized.slice(0, existing.start)}${normalized.slice(existing.end)}`);
94
103
  }
95
104
 
105
+ export function prependTopLevelTomlBlocks(text, blocks) {
106
+ const normalizedBlocks = blocks
107
+ .map((block) => String(block || '').trim())
108
+ .filter(Boolean);
109
+
110
+ const { topLevel, sections } = splitTopLevelToml(text);
111
+ const normalizedTopLevel = topLevel.replace(/^\n+/, '').trimEnd();
112
+ const normalizedSections = sections.replace(/^\n+/, '').trimEnd();
113
+ const remainder = normalizedTopLevel && normalizedSections
114
+ ? `${normalizedTopLevel}\n\n${normalizedSections}`
115
+ : normalizedTopLevel || normalizedSections;
116
+ if (!normalizedBlocks.length) return normalizeToml(remainder);
117
+ const managedPrelude = normalizedBlocks.join('\n');
118
+
119
+ return normalizeToml(
120
+ remainder
121
+ ? `${managedPrelude}\n\n${remainder}`
122
+ : managedPrelude,
123
+ );
124
+ }
125
+
96
126
  export function upsertTopLevelTomlKey(text, key, value) {
97
127
  const re = new RegExp(`^${key}\\s*=.*$`, 'm');
98
128
  const next = re.test(text)
@@ -6,10 +6,12 @@ export const DANGEROUS_PATTERNS = [
6
6
  { pattern: /(sudo\s+)?rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?(\/|~|\*)/, reason: 'Recursive delete of critical path' },
7
7
  { pattern: /(sudo\s+)?rm\s+--recursive/, reason: 'Recursive delete (long option)' },
8
8
  { pattern: /(sudo\s+)?rm\s+-[a-zA-Z]*r[a-zA-Z]*\s+\.\.?(\s|$)/, reason: 'Recursive delete of current/parent directory' },
9
+ { pattern: /\bcmd(?:\.exe)?\s*\/c\b/i, reason: 'Nested cmd invocation bypasses PowerShell safety rules' },
10
+ { pattern: /\bStart-Process\s+cmd(?:\.exe)?\b/i, reason: 'Nested cmd invocation bypasses PowerShell safety rules' },
9
11
  { pattern: /git\s+push\s+(-f|--force)/, reason: 'Force push (specify branch explicitly)' },
10
12
  { pattern: /git\s+reset\s+--hard/, reason: 'Hard reset (destructive operation)' },
11
13
  { pattern: /DROP\s+(DATABASE|TABLE|SCHEMA)/i, reason: 'Database destruction command' },
12
- { pattern: /TRUNCATE\s+TABLE/i, reason: 'Table truncation' },
14
+ { pattern: /\bTRUNCATE(?:\s+TABLE)?\b/i, reason: 'Table truncation' },
13
15
  { pattern: /chmod\s+777/, reason: 'World-writable permissions' },
14
16
  { pattern: /mkfs\b/, reason: 'Filesystem format command' },
15
17
  { pattern: /dd\s+.*of=\/dev\//, reason: 'Direct device write' },
@@ -68,6 +70,29 @@ export function scanHighRiskCommands(command) {
68
70
  return warnings
69
71
  }
70
72
 
73
+ export function scanShellSafetyWarnings(command = '') {
74
+ const warnings = []
75
+ const normalized = String(command || '')
76
+
77
+ if (/\bpowershell(?:\.exe)?\b/i.test(normalized) && /\s-Command\b/i.test(normalized)) {
78
+ const inlineScript = normalized.split(/\s-Command\b/i).slice(1).join(' ').trim()
79
+ const logicalLines = inlineScript
80
+ .split(/[;\r\n]+/)
81
+ .map((entry) => entry.trim())
82
+ .filter(Boolean)
83
+ if (logicalLines.length > 3) {
84
+ warnings.push('PowerShell inline script exceeds 3 logical lines; prefer a temporary .ps1 file')
85
+ }
86
+ }
87
+
88
+ const fileOps = normalized.match(/\b(remove-item|move-item|copy-item|new-item|set-content|add-content|out-file|mkdir|md|touch|cp|copy|mv|move|ren|rename|del|erase|rm|rmdir)\b/ig) || []
89
+ if (fileOps.length > 1 && /[;\r\n]/.test(normalized)) {
90
+ warnings.push('Multiple file operations are chained in one shell command; split them into separate commands')
91
+ }
92
+
93
+ return warnings
94
+ }
95
+
71
96
  export function scanUnrequestedFiles(filePath, toolName) {
72
97
  if (!filePath || toolName?.toLowerCase() !== 'write') return []
73
98
  const basename = filePath.split(/[/\\]/).pop() || ''
package/scripts/guard.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  scanEnvCoverage,
19
19
  scanForSecrets,
20
20
  scanHighRiskCommands,
21
+ scanShellSafetyWarnings,
21
22
  scanUnrequestedFiles,
22
23
  } from './guard-rules.mjs'
23
24
 
@@ -122,7 +123,7 @@ function buildPostWriteWarnings(data) {
122
123
  const filePath = data.tool_input?.file_path || ''
123
124
  return [
124
125
  ...(detectIdeaBoundaryContext(data)?.zeroSideEffect
125
- ? ['~idea 当前轮要求只读探索;检测到写入工具落地,请回退到探索输出或升级到 ~plan / ~build / ~prd / ~auto 后再修改文件']
126
+ ? ['~idea 本轮要求只读探索;检测到写入工具落地,请回退到探索输出或升级到 ~plan / ~build / ~prd / ~auto 后再修改文件']
126
127
  : []),
127
128
  ...scanUnrequestedFiles(filePath, data.tool_name),
128
129
  ...(content ? [...scanForSecrets(content), ...scanDangerousPackages(content, filePath)] : []),
@@ -168,7 +169,7 @@ function handleDangerousCommand(data, command) {
168
169
 
169
170
  function handleHighRiskCommand(data, command) {
170
171
  const warnings = scanHighRiskCommands(command)
171
- if (warnings.length === 0) return
172
+ if (warnings.length === 0) return []
172
173
 
173
174
  const cwd = data.cwd || process.cwd()
174
175
  const gate = buildHighRiskGate(warnings, cwd)
@@ -185,20 +186,43 @@ function handleHighRiskCommand(data, command) {
185
186
  guardType: 'high-risk-gate',
186
187
  matches: warnings.map((warning) => warning.reason),
187
188
  })
188
- return
189
+ return null
190
+ }
191
+ return warnings.map((warning) => warning.reason)
192
+ }
193
+
194
+ function emitShellWarnings(data, command, highRiskWarnings, shellSafetyWarnings) {
195
+ const sections = []
196
+ if (highRiskWarnings.length > 0) {
197
+ sections.push(`⚠️ [HelloAGENTS 高风险链路提醒] 检测到高风险命令:\n${highRiskWarnings.map((warning) => ` - ${warning}`).join('\n')}\n请确认已完成相应规划/审查并获得必要授权。`)
198
+ }
199
+ if (shellSafetyWarnings.length > 0) {
200
+ sections.push(`⚠️ [HelloAGENTS Shell 安全提醒] 检测到建议调整的命令写法:\n${shellSafetyWarnings.map((warning) => ` - ${warning}`).join('\n')}\n当前仅提示,不中断执行。`)
189
201
  }
202
+ if (sections.length === 0) return
190
203
 
191
204
  emitHookPayload({
192
205
  hookSpecificOutput: {
193
206
  hookEventName: HOOK_EVENT,
194
- additionalContext: `⚠️ [HelloAGENTS 高风险链路提醒] 检测到高风险命令:\n${warnings.map((warning) => ` - ${warning.reason}`).join('\n')}\n请确认已完成相应规划/审查并获得必要授权。`,
207
+ additionalContext: sections.join('\n\n'),
195
208
  },
196
209
  })
197
- emitGuardEvent(cwd, 'guard_warning', 'command', '', {
198
- guardType: 'high-risk-warning',
199
- command: command.slice(0, 200),
200
- warnings: warnings.map((warning) => warning.reason),
201
- })
210
+
211
+ const cwd = data.cwd || process.cwd()
212
+ if (highRiskWarnings.length > 0) {
213
+ emitGuardEvent(cwd, 'guard_warning', 'command', '', {
214
+ guardType: 'high-risk-warning',
215
+ command: command.slice(0, 200),
216
+ warnings: highRiskWarnings,
217
+ })
218
+ }
219
+ if (shellSafetyWarnings.length > 0) {
220
+ emitGuardEvent(cwd, 'guard_warning', 'command', '', {
221
+ guardType: 'shell-safety-warning',
222
+ command: command.slice(0, 200),
223
+ warnings: shellSafetyWarnings,
224
+ })
225
+ }
202
226
  }
203
227
 
204
228
  function handleShellCommand(data) {
@@ -217,7 +241,11 @@ function handleShellCommand(data) {
217
241
  }
218
242
 
219
243
  if (handleDangerousCommand(data, command)) return
220
- handleHighRiskCommand(data, command)
244
+ const highRiskWarnings = handleHighRiskCommand(data, command)
245
+ if (highRiskWarnings === null) return
246
+
247
+ const shellSafetyWarnings = scanShellSafetyWarnings(command)
248
+ emitShellWarnings(data, command, highRiskWarnings, shellSafetyWarnings)
221
249
  }
222
250
 
223
251
  async function main() {
@@ -31,11 +31,6 @@ function resolveStandbyHostRoot(host) {
31
31
  }
32
32
 
33
33
  function resolveReadRoot({ cwd, pkgRoot, host, settings }) {
34
- const projectRoot = join(cwd, 'skills', 'helloagents');
35
- if (existsSync(projectRoot)) {
36
- return { source: 'project', root: projectRoot };
37
- }
38
-
39
34
  if (settings.install_mode === 'standby') {
40
35
  const standbyRoot = resolveStandbyHostRoot(host);
41
36
  if (standbyRoot && existsSync(standbyRoot)) {
@@ -48,7 +43,7 @@ function resolveReadRoot({ cwd, pkgRoot, host, settings }) {
48
43
 
49
44
  function buildReadRootBlock(readRoot) {
50
45
  if (!readRoot?.root) return '';
51
- return `## 当前 HelloAGENTS 读取根目录\n\`\`\`json\n${JSON.stringify(readRoot, null, 2)}\n\`\`\``;
46
+ return `## 本轮 HelloAGENTS 读取根目录\n\`\`\`json\n${JSON.stringify(readRoot, null, 2)}\n\`\`\``;
52
47
  }
53
48
 
54
49
  export function resolveCanonicalCommandSkill(skillName) {
@@ -172,7 +167,7 @@ export function buildSemanticRouteInstruction(cwd) {
172
167
  '当前消息未使用 ~command。',
173
168
  '请根据用户请求的真实意图选路,不依赖关键词表。',
174
169
  'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆链路。',
175
- '路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动选路。',
170
+ '路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动编排并自动衔接后续阶段。',
176
171
  '若判定为 T3,默认先走 ~plan / ~prd;纯审查/验证请求才优先 ~verify。',
177
172
  `涉及 UI 任务时,设计决策优先级:当前活跃 plan / PRD → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → 通用 UI 规则。`,
178
173
  projectStorageHint,
@@ -10,11 +10,12 @@ import { homedir } from 'node:os';
10
10
  import { playSound as _playSound, desktopNotify as _desktopNotify } from './notify-ui.mjs';
11
11
  import { resolveNotificationSource } from './notify-source.mjs';
12
12
  import { buildCompactionContext, buildInjectContext, buildRouteInstruction, buildSemanticRouteInstruction, resolveCanonicalCommandSkill } from './notify-context.mjs';
13
- import { claimsTaskComplete, shouldIgnoreCodexNotifyClient, shouldIgnoreFormattedSubagent } from './notify-events.mjs';
13
+ import { claimsTaskComplete, shouldIgnoreCodexNotifyClient } from './notify-events.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';
17
17
  import { appendReplayEvent, startReplaySession } from './replay-state.mjs';
18
+ import { clearTurnState, readTurnState } from './turn-state.mjs';
18
19
  import { getWorkflowRecommendation } from './workflow-state.mjs';
19
20
 
20
21
  const __filename = fileURLToPath(import.meta.url);
@@ -114,6 +115,14 @@ function readCompletionText(payload = {}) {
114
115
  || '';
115
116
  }
116
117
 
118
+ function shouldRunDeliveryGate(cwd, lastMsg) {
119
+ const turnState = readTurnState(cwd);
120
+ if (turnState?.role === 'main') {
121
+ return turnState.kind === 'complete';
122
+ }
123
+ return claimsTaskComplete(lastMsg);
124
+ }
125
+
117
126
  function cmdPreCompact() {
118
127
  const payload = readStdinJson();
119
128
  const cwd = payload.cwd || process.cwd();
@@ -140,6 +149,7 @@ function cmdPreCompact() {
140
149
 
141
150
  function cmdRoute() {
142
151
  const payload = readStdinJson();
152
+ clearTurnState(payload.cwd || process.cwd());
143
153
  handleRouteCommand({
144
154
  payload,
145
155
  host: HOST,
@@ -194,6 +204,7 @@ function cmdInject() {
194
204
  cwd,
195
205
  });
196
206
  clearRouteContext();
207
+ clearTurnState(cwd);
197
208
  suppressedOutput(EVENT_NAME.SessionStart, context || undefined);
198
209
  }
199
210
 
@@ -207,7 +218,7 @@ function cmdStop() {
207
218
  desktopNotify('warning', buildNotifyExtra(payload));
208
219
  return;
209
220
  }
210
- if (claimsTaskComplete(lastMsg) && runDeliveryGate(payload)) {
221
+ if (shouldRunDeliveryGate(cwd, lastMsg) && runDeliveryGate(payload)) {
211
222
  playSound('warning');
212
223
  desktopNotify('warning', buildNotifyExtra(payload));
213
224
  return;
@@ -243,17 +254,17 @@ function cmdCodexNotify() {
243
254
  }
244
255
  if (type !== 'agent-turn-complete') return;
245
256
 
246
- const lastMsg = data['last-assistant-message'] || '';
247
- const settings = getSettings();
248
- if (shouldIgnoreFormattedSubagent(lastMsg, settings.output_format !== false)) return;
249
-
250
257
  const cwd = data.cwd || process.cwd();
251
- if (claimsTaskComplete(lastMsg) && runRalphLoop({ cwd })) {
258
+ const turnState = readTurnState(cwd);
259
+ if (!turnState || turnState.role !== 'main') return;
260
+
261
+ const settings = getSettings();
262
+ if (turnState.kind === 'complete' && runRalphLoop({ cwd })) {
252
263
  playSound('warning');
253
264
  desktopNotify('warning', buildNotifyExtra(data));
254
265
  return;
255
266
  }
256
- if (claimsTaskComplete(lastMsg) && runDeliveryGate({ cwd })) {
267
+ if (turnState.kind === 'complete' && runDeliveryGate({ cwd })) {
257
268
  playSound('warning');
258
269
  desktopNotify('warning', buildNotifyExtra(data));
259
270
  return;
@@ -0,0 +1,173 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { dirname, join, normalize, resolve } from 'node:path'
3
+ import { homedir } from 'node:os'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ import { appendReplayEvent } from './replay-state.mjs'
7
+
8
+ const TURN_STATE_PATH = join(homedir(), '.helloagents', 'runtime', 'turn-state.json')
9
+ const TURN_STATE_TTL_MS = 30 * 60 * 1000
10
+ const VALID_KINDS = new Set(['complete', 'waiting', 'blocked', 'progress'])
11
+ const VALID_ROLES = new Set(['main', 'subagent'])
12
+
13
+ function normalizePath(filePath = '') {
14
+ return filePath ? normalize(resolve(filePath)) : ''
15
+ }
16
+
17
+ function ensureRuntimeDir() {
18
+ mkdirSync(dirname(TURN_STATE_PATH), { recursive: true })
19
+ }
20
+
21
+ function readStore() {
22
+ try {
23
+ return JSON.parse(readFileSync(TURN_STATE_PATH, 'utf-8'))
24
+ } catch {
25
+ return {}
26
+ }
27
+ }
28
+
29
+ function writeStore(store) {
30
+ const keys = Object.keys(store)
31
+ if (keys.length === 0) {
32
+ rmSync(TURN_STATE_PATH, { force: true })
33
+ return
34
+ }
35
+
36
+ ensureRuntimeDir()
37
+ writeFileSync(TURN_STATE_PATH, `${JSON.stringify(store, null, 2)}\n`, 'utf-8')
38
+ }
39
+
40
+ function getTurnStateKey(cwd = process.cwd()) {
41
+ return normalizePath(cwd)
42
+ }
43
+
44
+ function normalizeTurnState(input = {}) {
45
+ const kind = typeof input.kind === 'string' ? input.kind.trim().toLowerCase() : ''
46
+ const role = typeof input.role === 'string' ? input.role.trim().toLowerCase() : 'main'
47
+
48
+ return {
49
+ kind: VALID_KINDS.has(kind) ? kind : '',
50
+ role: VALID_ROLES.has(role) ? role : 'main',
51
+ phase: typeof input.phase === 'string' ? input.phase.trim().toLowerCase() : '',
52
+ source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'manual',
53
+ requiresDeliveryGate: Boolean(input.requiresDeliveryGate),
54
+ }
55
+ }
56
+
57
+ function pruneInvalidEntry(store, key) {
58
+ delete store[key]
59
+ writeStore(store)
60
+ }
61
+
62
+ export function clearTurnState(cwd = process.cwd()) {
63
+ const key = getTurnStateKey(cwd)
64
+ if (!key) return false
65
+ const store = readStore()
66
+ if (!(key in store)) return false
67
+ delete store[key]
68
+ writeStore(store)
69
+ return true
70
+ }
71
+
72
+ export function readTurnState(cwd = process.cwd(), { now = Date.now() } = {}) {
73
+ const key = getTurnStateKey(cwd)
74
+ if (!key) return null
75
+
76
+ const store = readStore()
77
+ const entry = store[key]
78
+ if (!entry?.cwd || !entry?.kind || !entry?.updatedAt) {
79
+ if (entry) pruneInvalidEntry(store, key)
80
+ return null
81
+ }
82
+
83
+ const updatedAt = Date.parse(entry.updatedAt)
84
+ if (!Number.isFinite(updatedAt) || (now - updatedAt > TURN_STATE_TTL_MS)) {
85
+ pruneInvalidEntry(store, key)
86
+ return null
87
+ }
88
+
89
+ const normalized = normalizeTurnState(entry)
90
+ if (!normalized.kind) {
91
+ pruneInvalidEntry(store, key)
92
+ return null
93
+ }
94
+
95
+ return {
96
+ cwd: normalizePath(entry.cwd),
97
+ updatedAt: entry.updatedAt,
98
+ ...normalized,
99
+ }
100
+ }
101
+
102
+ export function writeTurnState(cwd = process.cwd(), input = {}) {
103
+ const key = getTurnStateKey(cwd)
104
+ const normalized = normalizeTurnState(input)
105
+ if (!key || !normalized.kind) {
106
+ throw new Error('turn-state requires cwd and a valid kind')
107
+ }
108
+
109
+ const store = readStore()
110
+ const payload = {
111
+ cwd: key,
112
+ updatedAt: new Date().toISOString(),
113
+ ...normalized,
114
+ }
115
+ store[key] = payload
116
+ writeStore(store)
117
+
118
+ appendReplayEvent(cwd, {
119
+ event: 'turn_state_written',
120
+ source: normalized.source,
121
+ details: {
122
+ kind: normalized.kind,
123
+ role: normalized.role,
124
+ phase: normalized.phase,
125
+ requiresDeliveryGate: normalized.requiresDeliveryGate,
126
+ },
127
+ })
128
+
129
+ return payload
130
+ }
131
+
132
+ function readStdinJson() {
133
+ try {
134
+ return JSON.parse(readFileSync(0, 'utf-8'))
135
+ } catch {
136
+ return {}
137
+ }
138
+ }
139
+
140
+ function main() {
141
+ const command = process.argv[2] || ''
142
+ const input = readStdinJson()
143
+ const cwd = input.cwd || process.cwd()
144
+
145
+ if (command === 'write') {
146
+ const payload = writeTurnState(cwd, input)
147
+ process.stdout.write(JSON.stringify({
148
+ suppressOutput: true,
149
+ path: TURN_STATE_PATH,
150
+ payload,
151
+ }))
152
+ return
153
+ }
154
+
155
+ if (command === 'clear') {
156
+ process.stdout.write(JSON.stringify({
157
+ suppressOutput: true,
158
+ cleared: clearTurnState(cwd),
159
+ }))
160
+ return
161
+ }
162
+
163
+ if (command === 'read') {
164
+ process.stdout.write(JSON.stringify({
165
+ suppressOutput: true,
166
+ state: readTurnState(cwd),
167
+ }))
168
+ }
169
+ }
170
+
171
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
172
+ main()
173
+ }
@@ -63,7 +63,7 @@ export function determineVerifyMode(plan) {
63
63
  if (plan.contractIssues.length > 0) {
64
64
  return {
65
65
  mode: 'metadata-first',
66
- reason: '方案包缺少可信的结构化 contract',
66
+ reason: '方案包缺少可信的结构化契约',
67
67
  guidance: '验证分流:当前还不适合直接进入 reviewer / tester;先回到 ~plan / ~prd 补齐 `contract.json`,明确 `verifyMode`、`reviewerFocus` 和 `testerFocus`。',
68
68
  }
69
69
  }
@@ -73,7 +73,7 @@ export function determineVerifyMode(plan) {
73
73
  const testerFocus = plan.contract.testerFocus.join(';')
74
74
  return {
75
75
  mode: 'review-first',
76
- reason: '方案 contract 已明确要求审查优先',
76
+ reason: '方案契约已明确要求审查优先',
77
77
  guidance: `验证分流:当前更适合审查优先;先执行 reviewer / hello-review 范围审查,再交给 tester / hello-verify 跑完整验证。${reviewerFocus ? ` reviewer 重点:${reviewerFocus}。` : ''}${testerFocus ? ` tester 重点:${testerFocus}。` : ''}`.trim(),
78
78
  }
79
79
  }
@@ -88,7 +88,7 @@ export function determineVerifyMode(plan) {
88
88
 
89
89
  return {
90
90
  mode: 'test-first',
91
- reason: '方案 contract 已明确验证主路径,且任务 contract 已完整',
91
+ reason: '方案契约已明确验证主路径,且任务契约已完整',
92
92
  guidance: `验证分流:当前更适合测试优先;先执行 tester / hello-verify 跑完整验证,再针对失败点或关键边界补充 hello-review。${plan.contract?.testerFocus?.length ? ` tester 重点:${plan.contract.testerFocus.join(';')}。` : ''}`.trim(),
93
93
  }
94
94
  }
@@ -137,7 +137,7 @@ export function buildStateSyncHintFromSnapshot(snapshot) {
137
137
 
138
138
  export function buildStateRoleHintFromSnapshot(snapshot) {
139
139
  if (!snapshot.state.exists || snapshot.plans.length > 0) return ''
140
- return '恢复约束:当前仅检测到 `.helloagents/STATE.md`;先以当前用户消息、显式命令和代码事实确认主线,STATE.md 只用于找回上次停在哪,不是当前任务的自动授权或唯一事实源。'
140
+ return '恢复约束:当前仅检测到 `.helloagents/STATE.md`;先以当前用户消息、显式命令和代码事实确认主线,STATE.md 只用于找回上次停在哪,不是当前任务的自动授权或唯一判断依据。'
141
141
  }
142
142
 
143
143
  export function buildUiContractHint(cwd, snapshot) {
@@ -154,10 +154,10 @@ export function buildUiContractHint(cwd, snapshot) {
154
154
 
155
155
  const extraHints = []
156
156
  if (styleAdvisorRequired) {
157
- extraHints.push('若当前 UI contract 要求 style advisor,收尾前需复用 `.helloagents/.ralph-advisor.json` 留下独立复查证据')
157
+ extraHints.push('若当前 UI 契约要求 style advisor,收尾前需复用 `.helloagents/.ralph-advisor.json` 留下独立复查证据')
158
158
  }
159
159
  if (visualValidationRequired) {
160
- extraHints.push('若当前 UI contract 要求视觉验收,收尾前需写 `.helloagents/.ralph-visual.json` 记录关键视口、状态与结论')
160
+ extraHints.push('若当前 UI 契约要求视觉验收,收尾前需写 `.helloagents/.ralph-visual.json` 记录关键视口、状态与结论')
161
161
  }
162
162
  return `UI 约束提示:如本次属于视觉/交互链路,设计决策优先级固定为:当前活跃 plan.md / prd/03-ui-design.md → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → hello-ui。${extraHints.length > 0 ? ` ${extraHints.join(';')}。` : ''}`
163
163
  }