helloagents 3.0.12 → 3.0.16-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.claude-plugin/marketplace.json +6 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +182 -35
  5. package/README_CN.md +184 -37
  6. package/bootstrap-lite.md +32 -26
  7. package/bootstrap.md +35 -29
  8. package/cli.mjs +119 -11
  9. package/gemini-extension.json +1 -1
  10. package/install.ps1 +128 -0
  11. package/install.sh +121 -0
  12. package/package.json +23 -4
  13. package/scripts/advisor-state.mjs +36 -63
  14. package/scripts/capability-registry.mjs +4 -4
  15. package/scripts/cli-branch.mjs +84 -0
  16. package/scripts/cli-codex-config.mjs +14 -20
  17. package/scripts/cli-codex.mjs +32 -38
  18. package/scripts/cli-doctor-render.mjs +4 -0
  19. package/scripts/cli-doctor.mjs +40 -30
  20. package/scripts/cli-host-detect.mjs +0 -1
  21. package/scripts/cli-hosts.mjs +16 -8
  22. package/scripts/cli-lifecycle-hosts.mjs +119 -32
  23. package/scripts/cli-lifecycle.mjs +24 -13
  24. package/scripts/cli-messages.mjs +34 -16
  25. package/scripts/cli-runtime-carrier.mjs +15 -0
  26. package/scripts/cli-runtime-root.mjs +72 -0
  27. package/scripts/cli-toml.mjs +0 -79
  28. package/scripts/cli-utils.mjs +30 -4
  29. package/scripts/closeout-state.mjs +35 -62
  30. package/scripts/delivery-gate-messages.mjs +70 -0
  31. package/scripts/delivery-gate.mjs +9 -75
  32. package/scripts/guard-rules.mjs +42 -42
  33. package/scripts/guard.mjs +44 -24
  34. package/scripts/notify-context.mjs +19 -28
  35. package/scripts/notify-events.mjs +3 -1
  36. package/scripts/notify-gates.mjs +2 -0
  37. package/scripts/notify-route.mjs +9 -7
  38. package/scripts/notify-ui.mjs +42 -32
  39. package/scripts/notify.mjs +72 -36
  40. package/scripts/project-storage.mjs +35 -66
  41. package/scripts/ralph-loop.mjs +36 -31
  42. package/scripts/replay-state.mjs +31 -128
  43. package/scripts/review-state.mjs +34 -61
  44. package/scripts/runtime-artifacts.mjs +95 -0
  45. package/scripts/runtime-context.mjs +35 -29
  46. package/scripts/runtime-scope.mjs +313 -0
  47. package/scripts/session-capsule.mjs +202 -0
  48. package/scripts/turn-state-cli.mjs +17 -0
  49. package/scripts/turn-state.mjs +185 -66
  50. package/scripts/turn-stop-gate.mjs +24 -6
  51. package/scripts/verify-state.mjs +34 -85
  52. package/scripts/visual-state.mjs +38 -65
  53. package/scripts/workflow-core.mjs +3 -3
  54. package/scripts/workflow-plan-files.mjs +1 -1
  55. package/scripts/workflow-recommendation.mjs +17 -13
  56. package/scripts/workflow-state.mjs +5 -5
  57. package/skills/commands/build/SKILL.md +1 -1
  58. package/skills/commands/commit/SKILL.md +1 -1
  59. package/skills/commands/help/SKILL.md +5 -3
  60. package/skills/commands/loop/SKILL.md +1 -1
  61. package/skills/commands/plan/SKILL.md +8 -6
  62. package/skills/commands/prd/SKILL.md +5 -3
  63. package/skills/commands/verify/SKILL.md +5 -5
  64. package/skills/hello-debug/SKILL.md +20 -3
  65. package/skills/hello-review/SKILL.md +2 -2
  66. package/skills/hello-subagent/SKILL.md +2 -2
  67. package/skills/hello-test/SKILL.md +6 -2
  68. package/skills/hello-ui/SKILL.md +7 -7
  69. package/skills/hello-verify/SKILL.md +10 -7
  70. package/skills/helloagents/SKILL.md +14 -9
  71. package/templates/context.md +6 -0
  72. package/templates/plans/plan.md +3 -0
  73. package/templates/plans/tasks.md +8 -3
@@ -12,24 +12,13 @@ import { getVisualEvidenceStatus } from './visual-state.mjs'
12
12
  import { buildDeliveryGateHint, getDeliveryAction, getWorkflowRecommendation, getWorkflowSnapshot } from './workflow-state.mjs'
13
13
  import { getReviewEvidenceStatus } from './review-state.mjs'
14
14
  import { getVerifyEvidenceStatus } from './verify-state.mjs'
15
+ import { buildDeliveryBlockReason, buildUnderSpecifiedDetails } from './delivery-gate-messages.mjs'
15
16
 
16
17
  function selectGatePlans(snapshot) {
17
18
  if (snapshot.activePlans.length > 0) return snapshot.activePlans
18
19
  return snapshot.plans
19
20
  }
20
21
 
21
- function buildUnderSpecifiedDetails(entry) {
22
- return entry.taskSummary.underSpecifiedItems
23
- .slice(0, 3)
24
- .map((item) => {
25
- const missing = []
26
- if (item.files.length === 0) missing.push('missing files')
27
- if (!item.acceptance) missing.push('missing acceptance')
28
- if (!item.validation) missing.push('missing validation')
29
- return `${item.text} (${missing.join(', ')})`
30
- })
31
- }
32
-
33
22
  function collectTaskMetadataIssues(entry, issues) {
34
23
  if (entry.taskSummary.underSpecifiedCount === 0) return
35
24
  issues.push({
@@ -48,7 +37,7 @@ function collectPlanIssues(planEntries) {
48
37
  issues.push({
49
38
  type: 'missing-files',
50
39
  planName: entry.planName,
51
- details: entry.missingFiles.map((file) => `missing ${file}`),
40
+ details: entry.missingFiles.map((file) => `缺少 ${file}`),
52
41
  })
53
42
  }
54
43
 
@@ -64,7 +53,7 @@ function collectPlanIssues(planEntries) {
64
53
  issues.push({
65
54
  type: 'missing-task-checklist',
66
55
  planName: entry.planName,
67
- details: ['tasks.md does not contain any checklist items'],
56
+ details: ['tasks.md 没有可执行检查项'],
68
57
  })
69
58
  continue
70
59
  }
@@ -140,65 +129,6 @@ function collectGateIssues(planEntries, verificationStatus, reviewStatus, adviso
140
129
  return issues
141
130
  }
142
131
 
143
- function issueHeading(issue) {
144
- switch (issue.type) {
145
- case 'missing-files':
146
- return 'active plan package is missing required artifacts'
147
- case 'template-placeholders':
148
- return 'active plan package still contains template placeholders'
149
- case 'missing-task-checklist':
150
- return 'active plan package has no executable tasks'
151
- case 'unfinished-tasks':
152
- return 'active plan package still has unfinished tasks'
153
- case 'under-specified-tasks':
154
- return 'active plan package has under-specified task metadata'
155
- case 'missing-contract':
156
- return 'active plan package is missing a trustworthy structured contract'
157
- case 'missing-verify-evidence':
158
- return 'current workflow is missing fresh verification evidence'
159
- case 'missing-review-evidence':
160
- return 'current workflow is missing fresh review evidence'
161
- case 'missing-advisor-evidence':
162
- return 'current workflow is missing fresh advisor evidence'
163
- case 'missing-visual-evidence':
164
- return 'current workflow is missing fresh visual validation evidence'
165
- case 'missing-closeout-evidence':
166
- return 'current workflow is missing fresh closeout evidence'
167
- default:
168
- return 'active plan package is not ready for delivery'
169
- }
170
- }
171
-
172
- function buildBlockReason(issues, recommendation, gateHint) {
173
- const lines = ['[Delivery Gate] Delivery is blocked because the current workflow state is not closed yet:']
174
-
175
- for (const issue of issues) {
176
- lines.push(`- ${issue.planName}: ${issueHeading(issue)}`)
177
- for (const detail of issue.details) {
178
- lines.push(` - ${detail}`)
179
- }
180
- if (issue.extraCount) {
181
- lines.push(` - ...and ${issue.extraCount} more`)
182
- }
183
- }
184
-
185
- lines.push('')
186
- if (recommendation?.nextPath) {
187
- lines.push(`Recommended path: ${recommendation.nextPath}`)
188
- }
189
- if (issues.some((issue) => issue.type === 'missing-closeout-evidence')) {
190
- lines.push('Next closeout step: write `.helloagents/.ralph-closeout.json` with `requirementsCoverage` and `deliveryChecklist` before reporting completion.')
191
- }
192
- if (issues.some((issue) => issue.type === 'missing-visual-evidence')) {
193
- lines.push('Next visual step: write `.helloagents/.ralph-visual.json` with `tooling`, `screensChecked`, `statesChecked`, `status`, and `summary` before reporting completion.')
194
- }
195
- if (gateHint) {
196
- lines.push(gateHint)
197
- }
198
- lines.push('Do not report completion yet. First finish or explicitly close the remaining tasks, or repair the active plan package so it becomes a trustworthy delivery record.')
199
- return lines.join('\n')
200
- }
201
-
202
132
  function main() {
203
133
  let data = {}
204
134
  try {
@@ -208,11 +138,12 @@ function main() {
208
138
  const workflowOptions = { payload: data }
209
139
  const snapshot = getWorkflowSnapshot(cwd, workflowOptions)
210
140
  const recommendation = getWorkflowRecommendation(cwd, workflowOptions)
211
- const verificationStatus = getVerifyEvidenceStatus(cwd)
141
+ const verificationStatus = getVerifyEvidenceStatus(cwd, workflowOptions)
212
142
  const deliveryAction = getDeliveryAction(cwd, workflowOptions)
213
143
  const gatePlans = selectGatePlans(snapshot)
214
144
  const reviewStatus = getReviewEvidenceStatus(cwd, {
215
145
  required: deliveryAction?.phase === 'verify' && deliveryAction?.mode === 'review-first',
146
+ ...workflowOptions,
216
147
  })
217
148
  if (gatePlans.length === 0) {
218
149
  process.stdout.write(JSON.stringify({ suppressOutput: true }))
@@ -223,12 +154,14 @@ function main() {
223
154
  const advisorStatus = getAdvisorEvidenceStatus(cwd, {
224
155
  required: advisorRequirements.some((entry) => entry.required),
225
156
  focus: advisorRequirements.flatMap((entry) => entry.focus || []),
157
+ ...workflowOptions,
226
158
  })
227
159
  const visualRequirements = gatePlans.map((entry) => getVisualValidationRequirement(entry.contract))
228
160
  const visualStatus = getVisualEvidenceStatus(cwd, {
229
161
  required: visualRequirements.some((entry) => entry.required),
230
162
  screens: visualRequirements.flatMap((entry) => entry.screens || []),
231
163
  states: visualRequirements.flatMap((entry) => entry.states || []),
164
+ ...workflowOptions,
232
165
  })
233
166
  const closeoutRequired = (
234
167
  gatePlans.every((entry) => entry.missingFiles.length === 0 && entry.templateIssues.length === 0 && entry.taskSummary.total > 0 && entry.taskSummary.open === 0 && entry.taskSummary.underSpecifiedCount === 0)
@@ -239,6 +172,7 @@ function main() {
239
172
  )
240
173
  const closeoutStatus = getCloseoutEvidenceStatus(cwd, {
241
174
  required: closeoutRequired,
175
+ ...workflowOptions,
242
176
  })
243
177
 
244
178
  const issues = collectGateIssues(gatePlans, verificationStatus, reviewStatus, advisorStatus, visualStatus, closeoutStatus)
@@ -249,7 +183,7 @@ function main() {
249
183
 
250
184
  process.stdout.write(JSON.stringify({
251
185
  decision: 'block',
252
- reason: buildBlockReason(issues, recommendation, buildDeliveryGateHint(cwd, workflowOptions)),
186
+ reason: buildDeliveryBlockReason(issues, recommendation, buildDeliveryGateHint(cwd, workflowOptions)),
253
187
  suppressOutput: true,
254
188
  }))
255
189
  }
@@ -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}`;
@@ -1,5 +1,7 @@
1
1
  export function shouldIgnoreCodexNotifyClient(client) {
2
- return !!client && client !== 'codex-tui';
2
+ if (!client) return false;
3
+ const normalized = String(client).trim().toLowerCase().replace(/[_\s]+/g, '-');
4
+ return normalized !== 'codex' && !normalized.startsWith('codex-');
3
5
  }
4
6
 
5
7
  export function shouldIgnoreFormattedSubagent(lastMsg, outputFormatEnabled) {
@@ -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 的帮助和当前设置;优先使用当前会话上下文中已注入的“当前用户设置”、配置文件原始 JSON 或此前读取结果摘要,上下文不存在或缺少要展示的配置项时才读取一次 ~/.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
  }