helloagents 3.0.12 → 3.0.15-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 (72) hide show
  1. package/.claude-plugin/marketplace.json +6 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +169 -30
  5. package/README_CN.md +169 -30
  6. package/bootstrap-lite.md +27 -20
  7. package/bootstrap.md +30 -23
  8. package/cli.mjs +119 -11
  9. package/gemini-extension.json +1 -1
  10. package/install.ps1 +125 -0
  11. package/install.sh +118 -0
  12. package/package.json +23 -4
  13. package/scripts/advisor-state.mjs +36 -63
  14. package/scripts/capability-registry.mjs +3 -3
  15. package/scripts/cli-branch.mjs +84 -0
  16. package/scripts/cli-codex-config.mjs +11 -20
  17. package/scripts/cli-codex.mjs +32 -38
  18. package/scripts/cli-doctor-render.mjs +4 -0
  19. package/scripts/cli-doctor.mjs +40 -30
  20. package/scripts/cli-host-detect.mjs +0 -1
  21. package/scripts/cli-hosts.mjs +16 -8
  22. package/scripts/cli-lifecycle-hosts.mjs +92 -27
  23. package/scripts/cli-lifecycle.mjs +9 -7
  24. package/scripts/cli-messages.mjs +34 -16
  25. package/scripts/cli-runtime-carrier.mjs +36 -0
  26. package/scripts/cli-runtime-root.mjs +72 -0
  27. package/scripts/cli-toml.mjs +0 -79
  28. package/scripts/cli-utils.mjs +30 -4
  29. package/scripts/closeout-state.mjs +35 -62
  30. package/scripts/delivery-gate-messages.mjs +70 -0
  31. package/scripts/delivery-gate.mjs +9 -75
  32. package/scripts/guard-rules.mjs +42 -42
  33. package/scripts/guard.mjs +44 -24
  34. package/scripts/notify-context.mjs +19 -28
  35. package/scripts/notify-gates.mjs +2 -0
  36. package/scripts/notify-route.mjs +9 -7
  37. package/scripts/notify-ui.mjs +46 -33
  38. package/scripts/notify.mjs +60 -32
  39. package/scripts/project-storage.mjs +35 -66
  40. package/scripts/ralph-loop.mjs +36 -31
  41. package/scripts/replay-state.mjs +31 -128
  42. package/scripts/review-state.mjs +34 -61
  43. package/scripts/runtime-artifacts.mjs +95 -0
  44. package/scripts/runtime-context.mjs +35 -29
  45. package/scripts/runtime-scope.mjs +313 -0
  46. package/scripts/session-capsule.mjs +202 -0
  47. package/scripts/turn-state-cli.mjs +17 -0
  48. package/scripts/turn-state.mjs +185 -66
  49. package/scripts/turn-stop-gate.mjs +24 -6
  50. package/scripts/verify-state.mjs +34 -85
  51. package/scripts/visual-state.mjs +38 -65
  52. package/scripts/workflow-core.mjs +2 -2
  53. package/scripts/workflow-plan-files.mjs +1 -1
  54. package/scripts/workflow-recommendation.mjs +17 -13
  55. package/scripts/workflow-state.mjs +5 -5
  56. package/skills/commands/build/SKILL.md +1 -1
  57. package/skills/commands/commit/SKILL.md +1 -1
  58. package/skills/commands/help/SKILL.md +3 -3
  59. package/skills/commands/loop/SKILL.md +1 -1
  60. package/skills/commands/plan/SKILL.md +8 -6
  61. package/skills/commands/prd/SKILL.md +5 -3
  62. package/skills/commands/verify/SKILL.md +5 -5
  63. package/skills/hello-debug/SKILL.md +20 -3
  64. package/skills/hello-review/SKILL.md +2 -2
  65. package/skills/hello-subagent/SKILL.md +2 -2
  66. package/skills/hello-test/SKILL.md +6 -2
  67. package/skills/hello-ui/SKILL.md +4 -4
  68. package/skills/hello-verify/SKILL.md +10 -7
  69. package/skills/helloagents/SKILL.md +12 -7
  70. package/templates/context.md +6 -0
  71. package/templates/plans/plan.md +3 -0
  72. package/templates/plans/tasks.md +8 -3
@@ -2,29 +2,29 @@ import { readFileSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
3
 
4
4
  export const DANGEROUS_PATTERNS = [
5
- { pattern: /(sudo\s+)?rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?(\/|~|\*)/, reason: 'Recursive delete of critical path' },
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
- { pattern: /(sudo\s+)?rm\s+--recursive/, reason: 'Recursive delete (long option)' },
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' },
11
- { pattern: /git\s+push\s+(-f|--force)/, reason: 'Force push (specify branch explicitly)' },
12
- { pattern: /git\s+reset\s+--hard/, reason: 'Hard reset (destructive operation)' },
13
- { pattern: /DROP\s+(DATABASE|TABLE|SCHEMA)/i, reason: 'Database destruction command' },
14
- { pattern: /\bTRUNCATE(?:\s+TABLE)?\b/i, reason: 'Table truncation' },
15
- { pattern: /chmod\s+777/, reason: 'World-writable permissions' },
16
- { pattern: /mkfs\b/, reason: 'Filesystem format command' },
17
- { pattern: /dd\s+.*of=\/dev\//, reason: 'Direct device write' },
18
- { pattern: /FLUSHALL|FLUSHDB/i, reason: 'Redis data flush' },
5
+ { pattern: /(sudo\s+)?rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?(\/|~|\*)/, reason: '递归删除关键路径' },
6
+ { pattern: /(sudo\s+)?rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?(\/|~|\*)/, reason: '递归删除关键路径' },
7
+ { pattern: /(sudo\s+)?rm\s+--recursive/, reason: '递归删除命令' },
8
+ { pattern: /(sudo\s+)?rm\s+-[a-zA-Z]*r[a-zA-Z]*\s+\.\.?(\s|$)/, reason: '递归删除当前目录或父目录' },
9
+ { pattern: /\bcmd(?:\.exe)?\s*\/c\b/i, reason: '嵌套 cmd 会绕过 PowerShell 安全规则' },
10
+ { pattern: /\bStart-Process\s+cmd(?:\.exe)?\b/i, reason: '嵌套 cmd 会绕过 PowerShell 安全规则' },
11
+ { pattern: /git\s+push\s+(-f|--force)/, reason: '强制推送风险高,必须明确分支与授权' },
12
+ { pattern: /git\s+reset\s+--hard/, reason: '硬重置会丢弃本地变更' },
13
+ { pattern: /DROP\s+(DATABASE|TABLE|SCHEMA)/i, reason: '数据库破坏性命令' },
14
+ { pattern: /\bTRUNCATE(?:\s+TABLE)?\b/i, reason: '表数据清空命令' },
15
+ { pattern: /chmod\s+777/, reason: '全局可写权限风险高' },
16
+ { pattern: /mkfs\b/, reason: '文件系统格式化命令' },
17
+ { pattern: /dd\s+.*of=\/dev\//, reason: '直接写入设备' },
18
+ { pattern: /FLUSHALL|FLUSHDB/i, reason: 'Redis 数据清空命令' },
19
19
  ]
20
20
 
21
21
  export const HIGH_RISK_COMMAND_PATTERNS = [
22
- { pattern: /\bnpm\s+publish\b/i, reason: 'Package publish command', gate: 'post-verify' },
23
- { pattern: /\bgh\s+release\s+create\b/i, reason: 'Release publication command', gate: 'post-verify' },
24
- { pattern: /\bterraform\s+(apply|destroy)\b/i, reason: 'Infrastructure apply/destroy command', gate: 'post-verify' },
25
- { pattern: /\b(kubectl|helm)\s+(apply|delete|upgrade|rollback|set|rollout)\b/i, reason: 'Cluster deployment command', gate: 'post-verify' },
26
- { pattern: /\b(prisma|drizzle-kit|sequelize-cli|typeorm)\b.*\b(migrate|migration)\b/i, reason: 'Database migration command', gate: 'plan-first' },
27
- { pattern: /\b(vercel|wrangler|netlify|flyctl|fly)\b.*\b(deploy|publish)\b/i, reason: 'Deployment command', gate: 'post-verify' },
22
+ { pattern: /\bnpm\s+publish\b/i, reason: '包发布命令', gate: 'post-verify' },
23
+ { pattern: /\bgh\s+release\s+create\b/i, reason: '发布 release 命令', gate: 'post-verify' },
24
+ { pattern: /\bterraform\s+(apply|destroy)\b/i, reason: '基础设施变更命令', gate: 'post-verify' },
25
+ { pattern: /\b(kubectl|helm)\s+(apply|delete|upgrade|rollback|set|rollout)\b/i, reason: '集群变更命令', gate: 'post-verify' },
26
+ { pattern: /\b(prisma|drizzle-kit|sequelize-cli|typeorm)\b.*\b(migrate|migration)\b/i, reason: '数据库迁移命令', gate: 'plan-first' },
27
+ { pattern: /\b(vercel|wrangler|netlify|flyctl|fly)\b.*\b(deploy|publish)\b/i, reason: '部署命令', gate: 'post-verify' },
28
28
  ]
29
29
 
30
30
  export const IDEA_SIDE_EFFECT_COMMAND_PATTERNS = [
@@ -36,20 +36,20 @@ export const IDEA_SIDE_EFFECT_COMMAND_PATTERNS = [
36
36
  ]
37
37
 
38
38
  const SECRET_PATTERNS = [
39
- { pattern: /AKIA[0-9A-Z]{16}/, reason: 'AWS Access Key ID detected' },
40
- { pattern: /ghp_[a-zA-Z0-9]{36}/, reason: 'GitHub Personal Access Token detected' },
41
- { pattern: /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/, reason: 'GitHub Fine-grained PAT detected' },
42
- { pattern: /sk-[a-zA-Z0-9]{20,}/, reason: 'API secret key pattern detected (sk-)' },
43
- { pattern: /key-[a-zA-Z0-9]{20,}/, reason: 'API key pattern detected (key-)' },
44
- { pattern: /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/, reason: 'Private key detected' },
45
- { pattern: /password\s*[:=]\s*["'][^"']{4,}["']/i, reason: 'Hardcoded password detected' },
46
- { pattern: /secret\s*[:=]\s*["'][^"']{4,}["']/i, reason: 'Hardcoded secret detected' },
47
- { pattern: /AIza[0-9A-Za-z\-_]{35}/, reason: 'Google API Key detected' },
48
- { pattern: /xox[bpras]-[0-9a-zA-Z\-]+/, reason: 'Slack Token detected' },
49
- { pattern: /eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.+/=]+/, reason: 'JWT token detected' },
50
- { pattern: /(postgres|mysql|mongodb(\+srv)?):\/\/[^:]+:[^@]+@/i, reason: 'Database connection string with credentials detected' },
51
- { pattern: /sk_live_[a-zA-Z0-9]{24,}/, reason: 'Stripe Secret Key detected' },
52
- { pattern: /sk-ant-[a-zA-Z0-9\-]{20,}/, reason: 'Anthropic API Key detected' },
39
+ { pattern: /AKIA[0-9A-Z]{16}/, reason: '检测到 AWS Access Key ID' },
40
+ { pattern: /ghp_[a-zA-Z0-9]{36}/, reason: '检测到 GitHub Personal Access Token' },
41
+ { pattern: /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/, reason: '检测到 GitHub Fine-grained PAT' },
42
+ { pattern: /sk-[a-zA-Z0-9]{20,}/, reason: '检测到 API secret keysk-)' },
43
+ { pattern: /key-[a-zA-Z0-9]{20,}/, reason: '检测到 API keykey-)' },
44
+ { pattern: /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/, reason: '检测到私钥' },
45
+ { pattern: /password\s*[:=]\s*["'][^"']{4,}["']/i, reason: '检测到硬编码密码' },
46
+ { pattern: /secret\s*[:=]\s*["'][^"']{4,}["']/i, reason: '检测到硬编码密钥' },
47
+ { pattern: /AIza[0-9A-Za-z\-_]{35}/, reason: '检测到 Google API Key' },
48
+ { pattern: /xox[bpras]-[0-9a-zA-Z\-]+/, reason: '检测到 Slack Token' },
49
+ { pattern: /eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.+/=]+/, reason: '检测到 JWT token' },
50
+ { pattern: /(postgres|mysql|mongodb(\+srv)?):\/\/[^:]+:[^@]+@/i, reason: '检测到包含凭据的数据库连接串' },
51
+ { pattern: /sk_live_[a-zA-Z0-9]{24,}/, reason: '检测到 Stripe Secret Key' },
52
+ { pattern: /sk-ant-[a-zA-Z0-9\-]{20,}/, reason: '检测到 Anthropic API Key' },
53
53
  ]
54
54
 
55
55
  export function scanForSecrets(content) {
@@ -81,13 +81,13 @@ export function scanShellSafetyWarnings(command = '') {
81
81
  .map((entry) => entry.trim())
82
82
  .filter(Boolean)
83
83
  if (logicalLines.length > 3) {
84
- warnings.push('PowerShell inline script exceeds 3 logical lines; prefer a temporary .ps1 file')
84
+ warnings.push('PowerShell 内联脚本超过 3 个逻辑行,建议改用临时 .ps1 文件')
85
85
  }
86
86
  }
87
87
 
88
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
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')
90
+ warnings.push('单条 shell 命令串联了多个文件操作,建议拆成独立命令')
91
91
  }
92
92
 
93
93
  return warnings
@@ -99,11 +99,11 @@ export function scanUnrequestedFiles(filePath, toolName) {
99
99
  const warnings = []
100
100
 
101
101
  const patterns = [
102
- { pattern: /^(SUMMARY|NOTES|TODO|SCRATCH|TEMP)\.(md|txt)$/i, reason: `Unrequested file creation: ${basename}` },
102
+ { pattern: /^(SUMMARY|NOTES|TODO|SCRATCH|TEMP)\.(md|txt)$/i, reason: `检测到未请求的文件创建:${basename}` },
103
103
  {
104
104
  pattern: /^README.*\.md$/i,
105
105
  matches: () => filePath.replace(/\\/g, '/').split('/').length > 4,
106
- reason: `Suspicious README creation in nested path: ${basename}`,
106
+ reason: `检测到嵌套路径中的可疑 README 创建:${basename}`,
107
107
  },
108
108
  ]
109
109
 
@@ -120,12 +120,12 @@ export function scanDangerousPackages(content, filePath) {
120
120
  if (filePath.endsWith('package.json')) {
121
121
  const dangerousScripts = /("(preinstall|postinstall|preuninstall)")\s*:\s*"[^"]*\b(curl|wget|bash|sh|eval|exec)\b/i
122
122
  if (dangerousScripts.test(content)) {
123
- warnings.push('Potentially dangerous lifecycle script in package.json (preinstall/postinstall with curl/wget/bash/eval)')
123
+ warnings.push('package.json 中存在潜在危险的生命周期脚本(preinstall/postinstall 调用 curlwgetbasheval')
124
124
  }
125
125
  }
126
126
  const unsafeInstall = /npm install\s+[^-].*--ignore-scripts\s*=\s*false|pip install\s+--trusted-host|pip install\s+http:/i
127
127
  if (unsafeInstall.test(content)) {
128
- warnings.push('Unsafe dependency installation pattern detected')
128
+ warnings.push('检测到不安全的依赖安装写法')
129
129
  }
130
130
  return warnings
131
131
  }
@@ -136,12 +136,12 @@ export function scanEnvCoverage(filePath) {
136
136
  for (let i = 0; i < 10; i += 1) {
137
137
  try {
138
138
  const gitignore = readFileSync(join(dir, '.gitignore'), 'utf-8')
139
- return gitignore.includes('.env') ? [] : ['.env file written but .gitignore does not contain .env pattern']
139
+ return gitignore.includes('.env') ? [] : ['写入了 .env 文件,但 .gitignore 未包含 .env 规则']
140
140
  } catch {
141
141
  const parent = dirname(dir)
142
142
  if (parent === dir) break
143
143
  dir = parent
144
144
  }
145
145
  }
146
- return ['.env file written but no .gitignore found']
146
+ return ['写入了 .env 文件,但未找到 .gitignore']
147
147
  }
package/scripts/guard.mjs CHANGED
@@ -52,13 +52,14 @@ function emitHookPayload(payload) {
52
52
  process.stdout.write(JSON.stringify(payload))
53
53
  }
54
54
 
55
- function emitGuardEvent(cwd, event, source, reason, details = {}) {
55
+ function emitGuardEvent(cwd, event, source, reason, details = {}, payload = {}) {
56
56
  appendReplayEvent(cwd, {
57
57
  host: HOST,
58
58
  event,
59
59
  source,
60
60
  reason,
61
61
  details,
62
+ payload,
62
63
  })
63
64
  }
64
65
 
@@ -67,7 +68,7 @@ function buildHighRiskGate(matches, cwd, payload = {}) {
67
68
  const stateSyncHint = buildStateSyncHint(cwd, workflowOptions)
68
69
  if (stateSyncHint) {
69
70
  return {
70
- reason: `[HelloAGENTS Guard] Blocked T3 command until project recovery state is synced.\n${stateSyncHint}`,
71
+ reason: `[HelloAGENTS Guard] 已阻止 T3 命令:项目恢复状态尚未同步。\n${stateSyncHint}`,
71
72
  }
72
73
  }
73
74
 
@@ -75,25 +76,26 @@ function buildHighRiskGate(matches, cwd, payload = {}) {
75
76
  if (!recommendation) return null
76
77
  if (matches.some((match) => match.gate === 'post-verify')) {
77
78
  return {
78
- reason: `[HelloAGENTS Guard] Blocked T3 command until workflow reaches VERIFY / CONSOLIDATE.\n当前工作流:${recommendation.summary}\n建议路径:${recommendation.nextPath}\n${recommendation.guidance}`,
79
+ reason: `[HelloAGENTS Guard] 已阻止 T3 命令:当前工作流尚未进入 VERIFY / CONSOLIDATE。\n当前工作流:${recommendation.summary}\n建议路径:${recommendation.nextPath}\n${recommendation.guidance}`,
79
80
  }
80
81
  }
81
82
  if (matches.some((match) => match.gate === 'plan-first') && recommendation.nextCommand === 'plan') {
82
83
  return {
83
- reason: `[HelloAGENTS Guard] Blocked T3 command because the current workflow still requires ~plan before risky schema changes.\n当前工作流:${recommendation.summary}\n建议路径:${recommendation.nextPath}\n${recommendation.guidance}`,
84
+ reason: `[HelloAGENTS Guard] 已阻止 T3 命令:高风险 schema 变更前仍需先完成 ~plan。\n当前工作流:${recommendation.summary}\n建议路径:${recommendation.nextPath}\n${recommendation.guidance}`,
84
85
  }
85
86
  }
86
87
  return null
87
88
  }
88
89
 
89
90
  function buildIdeaBoundaryReason(kind) {
90
- return `[HelloAGENTS Guard] Blocked ${kind} during ~idea.\n当前路由:~idea 是只读探索;先停留在比较方案。若要写文件、改代码、创建知识库或执行有副作用的命令,请先升级到 ~plan / ~build / ~prd / ~auto。`
91
+ return `[HelloAGENTS Guard] 已阻止 ~idea 中的${kind}。\n当前路由:~idea 是只读探索;先停留在比较方案。若要写文件、改代码、创建知识库或执行有副作用的命令,请先升级到 ~plan / ~build / ~prd / ~auto。`
91
92
  }
92
93
 
93
94
  function detectIdeaBoundaryContext(data) {
94
95
  return getApplicableRouteContext({
95
96
  cwd: data.cwd || process.cwd(),
96
97
  filePath: data.tool_input?.file_path || '',
98
+ payload: data,
97
99
  })
98
100
  }
99
101
 
@@ -106,17 +108,24 @@ function emitIdeaBoundaryBlock(data, kind, target) {
106
108
  permissionDecisionReason: reason,
107
109
  },
108
110
  })
109
- emitGuardEvent(data.cwd || process.cwd(), 'guard_blocked', kind === 'write' ? 'pre-write' : 'command', buildIdeaBoundaryReason(kind), {
110
- command: kind === 'side-effect command' ? target.replace(/^Command:\s*/, '') : '',
111
- target: kind === 'write' ? target.replace(/^Target:\s*/, '') : '',
112
- guardType: kind === 'write' ? 'idea-write-boundary' : 'idea-command-boundary',
113
- })
111
+ emitGuardEvent(
112
+ data.cwd || process.cwd(),
113
+ 'guard_blocked',
114
+ kind === 'write' ? 'pre-write' : 'command',
115
+ buildIdeaBoundaryReason(kind),
116
+ {
117
+ command: kind === '有副作用命令' ? target.replace(/^命令:\s*/, '') : '',
118
+ target: kind === '写入操作' ? target.replace(/^目标:\s*/, '') : '',
119
+ guardType: kind === '写入操作' ? 'idea-write-boundary' : 'idea-command-boundary',
120
+ },
121
+ data,
122
+ )
114
123
  }
115
124
 
116
125
  function preWriteGuard(data) {
117
126
  if (readSettings().guard_enabled === false) return
118
127
  if (!detectIdeaBoundaryContext(data)?.zeroSideEffect) return
119
- emitIdeaBoundaryBlock(data, 'write', `Target: ${data.tool_input?.file_path || '(unknown file)'}`)
128
+ emitIdeaBoundaryBlock(data, '写入操作', `目标:${data.tool_input?.file_path || '未知文件'}`)
120
129
  }
121
130
 
122
131
  function buildPostWriteWarnings(data) {
@@ -146,23 +155,23 @@ function postWriteScan(data) {
146
155
  emitGuardEvent(data.cwd || process.cwd(), 'guard_warning', 'post-write', '', {
147
156
  warnings,
148
157
  guardType: 'post-write-l2',
149
- })
158
+ }, data)
150
159
  }
151
160
 
152
161
  function handleDangerousCommand(data, command) {
153
162
  for (const { pattern, reason } of DANGEROUS_PATTERNS) {
154
163
  if (!pattern.test(command)) continue
155
164
  emitHookPayload({
156
- hookSpecificOutput: {
157
- hookEventName: HOOK_EVENT,
158
- permissionDecision: 'deny',
159
- permissionDecisionReason: `[HelloAGENTS Guard] Blocked: ${reason}\nCommand: ${command.slice(0, 200)}`,
160
- },
165
+ hookSpecificOutput: {
166
+ hookEventName: HOOK_EVENT,
167
+ permissionDecision: 'deny',
168
+ permissionDecisionReason: `[HelloAGENTS Guard] 已阻止:${reason}\n命令:${command.slice(0, 200)}`,
169
+ },
161
170
  })
162
171
  emitGuardEvent(data.cwd || process.cwd(), 'guard_blocked', 'command', reason, {
163
172
  command: command.slice(0, 200),
164
173
  guardType: 'dangerous-command',
165
- })
174
+ }, data)
166
175
  return true
167
176
  }
168
177
  return false
@@ -179,14 +188,14 @@ function handleHighRiskCommand(data, command) {
179
188
  hookSpecificOutput: {
180
189
  hookEventName: HOOK_EVENT,
181
190
  permissionDecision: 'deny',
182
- permissionDecisionReason: `${gate.reason}\nCommand: ${command.slice(0, 200)}`,
191
+ permissionDecisionReason: `${gate.reason}\n命令:${command.slice(0, 200)}`,
183
192
  },
184
193
  })
185
194
  emitGuardEvent(cwd, 'guard_blocked', 'command', gate.reason, {
186
195
  command: command.slice(0, 200),
187
196
  guardType: 'high-risk-gate',
188
197
  matches: warnings.map((warning) => warning.reason),
189
- })
198
+ }, data)
190
199
  return null
191
200
  }
192
201
  return warnings.map((warning) => warning.reason)
@@ -215,14 +224,14 @@ function emitShellWarnings(data, command, highRiskWarnings, shellSafetyWarnings)
215
224
  guardType: 'high-risk-warning',
216
225
  command: command.slice(0, 200),
217
226
  warnings: highRiskWarnings,
218
- })
227
+ }, data)
219
228
  }
220
229
  if (shellSafetyWarnings.length > 0) {
221
230
  emitGuardEvent(cwd, 'guard_warning', 'command', '', {
222
231
  guardType: 'shell-safety-warning',
223
232
  command: command.slice(0, 200),
224
233
  warnings: shellSafetyWarnings,
225
- })
234
+ }, data)
226
235
  }
227
236
  }
228
237
 
@@ -236,7 +245,7 @@ function handleShellCommand(data) {
236
245
  if (detectIdeaBoundaryContext(data)?.zeroSideEffect) {
237
246
  for (const pattern of IDEA_SIDE_EFFECT_COMMAND_PATTERNS) {
238
247
  if (!pattern.test(command)) continue
239
- emitIdeaBoundaryBlock(data, 'side-effect command', `Command: ${command.slice(0, 200)}`)
248
+ emitIdeaBoundaryBlock(data, '有副作用命令', `命令:${command.slice(0, 200)}`)
240
249
  return
241
250
  }
242
251
  }
@@ -265,4 +274,15 @@ async function main() {
265
274
  handleShellCommand(data)
266
275
  }
267
276
 
268
- main().catch(() => {})
277
+ main().catch((error) => {
278
+ const reason = `[HelloAGENTS Guard] 守卫脚本执行异常,已阻止本次操作以避免静默放行。\n原因:${error?.message || error}`
279
+ emitHookPayload({
280
+ hookSpecificOutput: {
281
+ hookEventName: HOOK_EVENT,
282
+ permissionDecision: 'deny',
283
+ permissionDecisionReason: reason,
284
+ },
285
+ })
286
+ process.stderr.write(`${reason}\n`)
287
+ process.exitCode = 1
288
+ })
@@ -1,6 +1,5 @@
1
1
  import { join } from 'node:path';
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import { homedir } from 'node:os';
2
+ import { readFileSync } from 'node:fs';
4
3
  import { buildCommandRouteHint, buildStateSyncHint, buildWorkflowRouteHint, readStateSnapshot } from './workflow-state.mjs';
5
4
  import { buildCapabilityHint } from './capability-registry.mjs';
6
5
  import {
@@ -15,35 +14,27 @@ const COMMAND_ALIASES = {
15
14
  review: 'verify',
16
15
  };
17
16
 
18
- function buildPackageRootBlock(pkgRoot) {
17
+ function buildRuntimeRootBlock(pkgRoot) {
19
18
  if (!pkgRoot) return '';
20
- return `## 当前 HelloAGENTS 包根目录\n\`\`\`text\n${pkgRoot}\n\`\`\``;
21
- }
22
-
23
- function resolveStandbyHostRoot(host) {
24
- const home = homedir();
25
- const map = {
26
- claude: join(home, '.claude', 'helloagents'),
27
- codex: join(home, '.codex', 'helloagents'),
28
- gemini: join(home, '.gemini', 'helloagents'),
29
- };
30
- return map[host] || '';
19
+ return `## 当前 HelloAGENTS 运行根目录\n\`\`\`text\n${pkgRoot}\n\`\`\``;
31
20
  }
32
21
 
33
22
  function resolveReadRoot({ cwd, pkgRoot, host, settings }) {
34
- if (settings.install_mode === 'standby') {
35
- const standbyRoot = resolveStandbyHostRoot(host);
36
- if (standbyRoot && existsSync(standbyRoot)) {
37
- return { source: 'standby-home', root: standbyRoot };
38
- }
39
- }
40
-
41
- return { source: 'package', root: pkgRoot };
23
+ void cwd
24
+ void host
25
+ void settings
26
+ return { source: 'runtime-root', root: pkgRoot }
42
27
  }
43
28
 
44
29
  function buildReadRootBlock(readRoot) {
45
30
  if (!readRoot?.root) return '';
46
- return `## 本轮 HelloAGENTS 读取根目录\n\`\`\`json\n${JSON.stringify(readRoot, null, 2)}\n\`\`\``;
31
+ const block = {
32
+ ...readRoot,
33
+ scriptRoot: join(readRoot.root, 'scripts'),
34
+ turnStateCommand: 'helloagents-turn-state write --kind complete --role main',
35
+ turnStateUsage: '仅在运行时需要识别完成、等待或阻塞时调用;普通问答不调用',
36
+ };
37
+ return `## 本轮 HelloAGENTS 读取根目录\n\`\`\`json\n${JSON.stringify(block, null, 2)}\n\`\`\``;
47
38
  }
48
39
 
49
40
  export function resolveCanonicalCommandSkill(skillName) {
@@ -89,10 +80,10 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
89
80
  summaryParts.push(bootstrap);
90
81
  }
91
82
 
92
- const packageRootBlock = buildPackageRootBlock(pkgRoot);
93
- if (packageRootBlock) {
83
+ const runtimeRootBlock = buildRuntimeRootBlock(pkgRoot);
84
+ if (runtimeRootBlock) {
94
85
  summaryParts.push('');
95
- summaryParts.push(packageRootBlock);
86
+ summaryParts.push(runtimeRootBlock);
96
87
  }
97
88
 
98
89
  const readRootBlock = buildReadRootBlock(resolveReadRoot({ cwd, pkgRoot, host, settings }));
@@ -123,7 +114,7 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
123
114
 
124
115
  export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host, cwd, payload = {} }) {
125
116
  const workflowOptions = { payload };
126
- const packageRootBlock = buildPackageRootBlock(pkgRoot);
117
+ const runtimeRootBlock = buildRuntimeRootBlock(pkgRoot);
127
118
  const readRootBlock = buildReadRootBlock(resolveReadRoot({ cwd, pkgRoot, host, settings }));
128
119
  const workflowHint = buildWorkflowRouteHint(cwd, workflowOptions);
129
120
  const capabilityHint = buildCapabilityHint({ cwd, options: workflowOptions });
@@ -135,7 +126,7 @@ export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host,
135
126
  : '';
136
127
 
137
128
  let context = bootstrap;
138
- if (packageRootBlock) context += `\n\n${packageRootBlock}`;
129
+ if (runtimeRootBlock) context += `\n\n${runtimeRootBlock}`;
139
130
  if (readRootBlock) context += `\n\n${readRootBlock}`;
140
131
  if (projectStorageBlock) context += `\n\n${projectStorageBlock}`;
141
132
  if (workflowHint) context += `\n\n## 当前工作流提示\n${workflowHint}`;
@@ -27,6 +27,7 @@ function emitGateError({
27
27
  event: 'runtime_gate_error',
28
28
  source,
29
29
  reason,
30
+ payload,
30
31
  })
31
32
  output({
32
33
  decision: 'block',
@@ -119,6 +120,7 @@ export function runGateScript({
119
120
  event: blockEvent,
120
121
  source,
121
122
  reason: gateOutput.reason || '',
123
+ payload,
122
124
  })
123
125
  output(gateOutput)
124
126
  return true
@@ -1,8 +1,7 @@
1
- import { existsSync } from 'node:fs'
2
- import { join } from 'node:path'
1
+ import { isProjectRuntimeActive } from './runtime-scope.mjs'
3
2
 
4
3
  export function resolveBootstrapFile(cwd, installMode) {
5
- const isActivated = existsSync(join(cwd, '.helloagents'))
4
+ const isActivated = isProjectRuntimeActive(cwd)
6
5
  return (installMode === 'global' || isActivated) ? 'bootstrap.md' : 'bootstrap-lite.md'
7
6
  }
8
7
 
@@ -12,7 +11,7 @@ function shouldBypassRoute(prompt) {
12
11
 
13
12
  function buildHelpExtraRules(skillName) {
14
13
  if (skillName !== 'help') return ''
15
- return ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。仅显示 HelloAGENTS 的帮助和当前设置;优先使用当前上下文中已注入的“当前用户设置”,只有上下文不存在该信息时才尝试读取 ~/.helloagents/helloagents.json;自动激活技能说明仅在全局模式或已激活项目中生效。不要调用宿主 CLI 的帮助工具(如 cli_help 或 /help),不要使用子代理,不要读取项目文件;若受工作区限制无法读取配置,必须明确说明并按已知默认值或已注入设置展示。'
14
+ return ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。仅显示 HelloAGENTS 的帮助和当前设置;优先使用当前上下文中已注入的“当前用户设置”,上下文不存在或缺少要展示的配置项时才读取 ~/.helloagents/helloagents.json;自动激活技能说明仅在全局模式或已激活项目中生效。不要调用宿主 CLI 的帮助工具(如 cli_help 或 /help),不要使用子代理,不要读取项目文件;若受工作区限制无法读取配置,必须明确说明并按已知默认值或已注入设置展示。'
16
15
  }
17
16
 
18
17
  function routeExplicitCommand({
@@ -37,6 +36,7 @@ function routeExplicitCommand({
37
36
  cwd,
38
37
  skillName: canonicalSkillName,
39
38
  sourceSkillName: skillName,
39
+ payload,
40
40
  })
41
41
  appendReplayEvent(cwd, {
42
42
  host,
@@ -44,6 +44,7 @@ function routeExplicitCommand({
44
44
  source: 'route',
45
45
  skillName: canonicalSkillName,
46
46
  sourceSkillName: skillName,
47
+ payload,
47
48
  })
48
49
  suppress(buildRouteInstruction({
49
50
  skillName,
@@ -75,7 +76,7 @@ export function handleRouteCommand({
75
76
  const prompt = (payload.prompt || '').trim()
76
77
  const cwd = payload.cwd || process.cwd()
77
78
  if (shouldBypassRoute(prompt)) {
78
- clearRouteContext()
79
+ clearRouteContext({ cwd, payload })
79
80
  emptySuppress()
80
81
  return
81
82
  }
@@ -98,17 +99,18 @@ export function handleRouteCommand({
98
99
 
99
100
  const bootstrapFile = resolveBootstrapFile(cwd, settings.install_mode)
100
101
  if (bootstrapFile === 'bootstrap.md') {
101
- clearRouteContext()
102
+ clearRouteContext({ cwd, payload })
102
103
  appendReplayEvent(cwd, {
103
104
  host,
104
105
  event: 'semantic_route_prompted',
105
106
  source: 'route',
106
107
  recommendation: getWorkflowRecommendation(cwd, { payload }),
108
+ payload,
107
109
  })
108
110
  suppress(buildSemanticRouteInstruction(cwd, payload))
109
111
  return
110
112
  }
111
113
 
112
- clearRouteContext()
114
+ clearRouteContext({ cwd, payload })
113
115
  emptySuppress()
114
116
  }
@@ -5,7 +5,7 @@
5
5
  import { platform } from 'node:os';
6
6
  import { join } from 'node:path';
7
7
  import { existsSync } from 'node:fs';
8
- import { spawnSync } from 'node:child_process';
8
+ import { spawn } from 'node:child_process';
9
9
 
10
10
  const PLAT = platform();
11
11
 
@@ -55,47 +55,53 @@ function resolveWav(pkgRoot, event) {
55
55
  return existsSync(p) ? p : null;
56
56
  }
57
57
 
58
+ function spawnDetached(command, args) {
59
+ try {
60
+ const child = spawn(command, args, {
61
+ detached: true,
62
+ stdio: 'ignore',
63
+ windowsHide: true,
64
+ });
65
+ child.on('error', () => {});
66
+ child.unref();
67
+ return true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+
58
73
  export function playSound(pkgRoot, event) {
59
74
  if (DISABLE_OS_NOTIFICATIONS) return;
60
75
  const wav = resolveWav(pkgRoot, event);
61
76
  if (!wav) { process.stderr.write('\x07'); return; }
62
77
  try {
63
78
  if (PLAT === 'win32') {
64
- spawnSync('powershell', ['-NoProfile', '-c',
65
- `(New-Object Media.SoundPlayer '${wav.replace(/'/g, "''")}').PlaySync()`],
66
- { stdio: 'ignore', windowsHide: true });
79
+ spawnDetached('powershell', [
80
+ '-NoProfile',
81
+ '-c',
82
+ `(New-Object Media.SoundPlayer '${wav.replace(/'/g, "''")}').PlaySync()`,
83
+ ]);
67
84
  } else if (PLAT === 'darwin') {
68
- spawnSync('afplay', [wav], { stdio: 'ignore' });
85
+ spawnDetached('afplay', [wav]);
69
86
  } else {
70
- const result = spawnSync('aplay', ['-q', wav], { stdio: 'ignore' });
71
- if (result.status !== 0) {
72
- const pa = spawnSync('paplay', [wav], { stdio: 'ignore' });
73
- if (pa.status !== 0) process.stderr.write('\x07');
74
- }
87
+ spawnDetached('aplay', ['-q', wav]) || spawnDetached('paplay', [wav]);
75
88
  }
76
89
  } catch { process.stderr.write('\x07'); }
77
90
  }
78
91
 
79
- function ensureWinAppId(pkgRoot) {
80
- if (PLAT !== 'win32') return;
92
+ function buildWindowsToastScript(notification, iconPath) {
81
93
  const regKey = `HKCU:\\Software\\Classes\\AppUserModelId\\${WIN_APPID}`;
82
- spawnSync('powershell', ['-NoProfile', '-c',
83
- `if (-not (Test-Path '${regKey}')) { New-Item -Path '${regKey}' -Force | Out-Null; Set-ItemProperty -Path '${regKey}' -Name 'DisplayName' -Value 'HelloAgents 通知' -Force }`],
84
- { stdio: 'ignore', windowsHide: true });
94
+ const iconXml = existsSync(iconPath)
95
+ ? `<image placement="appLogoOverride" src="${escapeToastText(iconPath)}" />`
96
+ : '';
97
+ const textXml = notification.toastLines
98
+ .map((line) => `<text>${escapeToastText(line)}</text>`)
99
+ .join('\n ');
100
+ return `
101
+ if (-not (Test-Path '${regKey}')) {
102
+ New-Item -Path '${regKey}' -Force | Out-Null
103
+ Set-ItemProperty -Path '${regKey}' -Name 'DisplayName' -Value 'HelloAgents 通知' -Force
85
104
  }
86
-
87
- export function desktopNotify(pkgRoot, event, extra) {
88
- if (DISABLE_OS_NOTIFICATIONS) return;
89
- const notification = buildDesktopNotificationContent(event, extra);
90
- try {
91
- if (PLAT === 'win32') {
92
- ensureWinAppId(pkgRoot);
93
- const iconPath = join(pkgRoot, 'assets', 'icons', 'icon.png').replace(/\//g, '\\');
94
- const iconXml = existsSync(iconPath) ? `<image placement="appLogoOverride" src="${iconPath}" />` : '';
95
- const textXml = notification.toastLines
96
- .map((line) => `<text>${escapeToastText(line)}</text>`)
97
- .join('\n ');
98
- const ps = `
99
105
  [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
100
106
  [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
101
107
  $xml = @"
@@ -113,17 +119,24 @@ $doc.LoadXml($xml)
113
119
  $toast = [Windows.UI.Notifications.ToastNotification]::new($doc)
114
120
  [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${WIN_APPID}').Show($toast)
115
121
  `.trim();
116
- spawnSync('powershell', ['-NoProfile', '-c', ps], { stdio: 'ignore', windowsHide: true });
122
+ }
123
+
124
+ export function desktopNotify(pkgRoot, event, extra) {
125
+ if (DISABLE_OS_NOTIFICATIONS) return;
126
+ const notification = buildDesktopNotificationContent(event, extra);
127
+ try {
128
+ if (PLAT === 'win32') {
129
+ const iconPath = join(pkgRoot, 'assets', 'icons', 'icon.png').replace(/\//g, '\\');
130
+ spawnDetached('powershell', ['-NoProfile', '-c', buildWindowsToastScript(notification, iconPath)]);
117
131
  } else if (PLAT === 'darwin') {
118
132
  const subtitle = notification.sourceLabel
119
133
  ? ` subtitle "${escapeAppleScriptText(notification.sourceLabel)}"`
120
134
  : '';
121
- spawnSync('osascript', ['-e',
135
+ spawnDetached('osascript', ['-e',
122
136
  `display notification "${escapeAppleScriptText(notification.message)}" with title "${escapeAppleScriptText(notification.title)}"${subtitle}`],
123
- { stdio: 'ignore' });
137
+ );
124
138
  } else {
125
- const result = spawnSync('notify-send', [notification.title, notification.body], { stdio: 'ignore' });
126
- if (result.status !== 0) process.stderr.write('\x07');
139
+ spawnDetached('notify-send', [notification.title, notification.body]);
127
140
  }
128
141
  } catch { process.stderr.write('\x07'); }
129
142
  }