helloagents 3.0.3-beta.1 → 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 (75) 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 +157 -57
  5. package/README_CN.md +157 -57
  6. package/bootstrap-lite.md +125 -50
  7. package/bootstrap.md +169 -123
  8. package/cli.mjs +80 -427
  9. package/gemini-extension.json +1 -1
  10. package/hooks/hooks-claude.json +10 -0
  11. package/hooks/hooks.json +10 -0
  12. package/package.json +1 -1
  13. package/scripts/advisor-state.mjs +222 -0
  14. package/scripts/capability-registry.mjs +59 -0
  15. package/scripts/cli-codex-backup.mjs +59 -0
  16. package/scripts/cli-codex-config.mjs +94 -0
  17. package/scripts/cli-codex.mjs +90 -222
  18. package/scripts/cli-config.mjs +1 -0
  19. package/scripts/cli-doctor-render.mjs +28 -0
  20. package/scripts/cli-doctor.mjs +370 -0
  21. package/scripts/cli-host-detect.mjs +94 -0
  22. package/scripts/cli-lifecycle-hosts.mjs +123 -0
  23. package/scripts/cli-lifecycle.mjs +213 -0
  24. package/scripts/cli-messages.mjs +76 -52
  25. package/scripts/cli-toml.mjs +30 -0
  26. package/scripts/closeout-state.mjs +213 -0
  27. package/scripts/delivery-gate.mjs +256 -0
  28. package/scripts/guard-rules.mjs +147 -0
  29. package/scripts/guard.mjs +218 -168
  30. package/scripts/notify-context.mjs +78 -23
  31. package/scripts/notify-events.mjs +5 -1
  32. package/scripts/notify-route.mjs +111 -0
  33. package/scripts/notify-shared.mjs +0 -2
  34. package/scripts/notify-source.mjs +113 -0
  35. package/scripts/notify-ui.mjs +40 -6
  36. package/scripts/notify.mjs +137 -65
  37. package/scripts/plan-contract.mjs +210 -0
  38. package/scripts/project-storage.mjs +235 -0
  39. package/scripts/ralph-loop.mjs +9 -58
  40. package/scripts/replay-state.mjs +210 -0
  41. package/scripts/review-state.mjs +220 -0
  42. package/scripts/runtime-context.mjs +74 -0
  43. package/scripts/turn-state.mjs +173 -0
  44. package/scripts/verify-state.mjs +226 -0
  45. package/scripts/visual-state.mjs +244 -0
  46. package/scripts/workflow-core.mjs +165 -0
  47. package/scripts/workflow-plan-files.mjs +249 -0
  48. package/scripts/workflow-recommendation.mjs +335 -0
  49. package/scripts/workflow-state.mjs +113 -0
  50. package/skills/_meta/SKILL.md +1 -1
  51. package/skills/commands/auto/SKILL.md +48 -67
  52. package/skills/commands/build/SKILL.md +67 -0
  53. package/skills/commands/clean/SKILL.md +10 -8
  54. package/skills/commands/commit/SKILL.md +8 -4
  55. package/skills/commands/help/SKILL.md +18 -11
  56. package/skills/commands/idea/SKILL.md +55 -0
  57. package/skills/commands/init/SKILL.md +16 -8
  58. package/skills/commands/loop/SKILL.md +6 -5
  59. package/skills/commands/plan/SKILL.md +118 -0
  60. package/skills/commands/prd/SKILL.md +22 -15
  61. package/skills/commands/verify/SKILL.md +32 -9
  62. package/skills/commands/wiki/SKILL.md +11 -11
  63. package/skills/hello-review/SKILL.md +9 -0
  64. package/skills/hello-subagent/SKILL.md +5 -3
  65. package/skills/hello-ui/SKILL.md +36 -8
  66. package/skills/hello-verify/SKILL.md +12 -3
  67. package/skills/helloagents/SKILL.md +36 -20
  68. package/templates/DESIGN.md +25 -4
  69. package/templates/STATE.md +3 -0
  70. package/templates/plans/contract.json +48 -0
  71. package/templates/plans/plan.md +23 -0
  72. package/templates/plans/tasks.md +3 -3
  73. package/skills/commands/design/SKILL.md +0 -108
  74. package/skills/commands/review/SKILL.md +0 -16
  75. package/templates/plans/design.md +0 -14
package/scripts/guard.mjs CHANGED
@@ -4,214 +4,264 @@
4
4
  * Runs on PreToolUse hook for Bash/shell commands.
5
5
  * Runs on PostToolUse hook for Write/Edit (L2 scan).
6
6
  */
7
- import { readFileSync } from 'node:fs';
8
- import { join, dirname } from 'node:path';
9
- import { homedir } from 'node:os';
7
+ import { readFileSync } from 'node:fs'
8
+ import { join } from 'node:path'
9
+ import { homedir } from 'node:os'
10
10
 
11
- const CONFIG_FILE = join(homedir(), '.helloagents', 'helloagents.json');
11
+ import { buildStateSyncHint, getWorkflowRecommendation } from './workflow-state.mjs'
12
+ import { getApplicableRouteContext } from './runtime-context.mjs'
13
+ import { appendReplayEvent } from './replay-state.mjs'
14
+ import {
15
+ DANGEROUS_PATTERNS,
16
+ IDEA_SIDE_EFFECT_COMMAND_PATTERNS,
17
+ scanDangerousPackages,
18
+ scanEnvCoverage,
19
+ scanForSecrets,
20
+ scanHighRiskCommands,
21
+ scanShellSafetyWarnings,
22
+ scanUnrequestedFiles,
23
+ } from './guard-rules.mjs'
12
24
 
13
- // Hook event name: read from env or infer from CLI mode + --gemini flag.
14
- // Claude: PreToolUse/PostToolUse, Gemini: BeforeTool/AfterModel.
15
- const IS_GEMINI = process.argv.includes('--gemini');
16
- const IS_POST_WRITE = process.argv.includes('post-write');
25
+ const CONFIG_FILE = join(homedir(), '.helloagents', 'helloagents.json')
26
+ const IS_GEMINI = process.argv.includes('--gemini')
27
+ const HOST = IS_GEMINI ? 'gemini' : 'claude'
17
28
  const HOOK_EVENT = process.env.HELLOAGENTS_HOOK_EVENT
18
- || (IS_POST_WRITE ? (IS_GEMINI ? 'AfterModel' : 'PostToolUse') : (IS_GEMINI ? 'BeforeTool' : 'PreToolUse'));
29
+ || (
30
+ process.argv.includes('post-write')
31
+ ? (IS_GEMINI ? 'AfterModel' : 'PostToolUse')
32
+ : (IS_GEMINI ? 'BeforeTool' : 'PreToolUse')
33
+ )
19
34
 
20
35
  function readSettings() {
21
- try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')); } catch {}
22
- return {};
36
+ try {
37
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
38
+ } catch {
39
+ return {}
40
+ }
23
41
  }
24
42
 
25
- function emitHookPayload(payload) {
26
- process.stdout.write(JSON.stringify(payload));
27
- }
28
-
29
- const DANGEROUS_PATTERNS = [
30
- // Destructive file operations (including sudo prefix and long options)
31
- { 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' },
32
- { 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' },
33
- { pattern: /(sudo\s+)?rm\s+--recursive/, reason: 'Recursive delete (long option)' },
34
- { pattern: /(sudo\s+)?rm\s+-[a-zA-Z]*r[a-zA-Z]*\s+\.\.?(\s|$)/, reason: 'Recursive delete of current/parent directory' },
35
- // Force push
36
- { pattern: /git\s+push\s+(-f|--force)/, reason: 'Force push (specify branch explicitly)' },
37
- // Hard reset
38
- { pattern: /git\s+reset\s+--hard/, reason: 'Hard reset (destructive operation)' },
39
- // Database destruction
40
- { pattern: /DROP\s+(DATABASE|TABLE|SCHEMA)/i, reason: 'Database destruction command' },
41
- { pattern: /TRUNCATE\s+TABLE/i, reason: 'Table truncation' },
42
- // Dangerous system commands
43
- { pattern: /chmod\s+777/, reason: 'World-writable permissions' },
44
- { pattern: /mkfs\b/, reason: 'Filesystem format command' },
45
- { pattern: /dd\s+.*of=\/dev\//, reason: 'Direct device write' },
46
- // Redis flush
47
- { pattern: /FLUSHALL|FLUSHDB/i, reason: 'Redis data flush' },
48
- ];
49
-
50
- // ── L2 Semantic Security Patterns (advisory, non-blocking) ──────────────────
51
-
52
- const SECRET_PATTERNS = [
53
- { pattern: /AKIA[0-9A-Z]{16}/, reason: 'AWS Access Key ID detected' },
54
- { pattern: /ghp_[a-zA-Z0-9]{36}/, reason: 'GitHub Personal Access Token detected' },
55
- { pattern: /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/, reason: 'GitHub Fine-grained PAT detected' },
56
- { pattern: /sk-[a-zA-Z0-9]{20,}/, reason: 'API secret key pattern detected (sk-)' },
57
- { pattern: /key-[a-zA-Z0-9]{20,}/, reason: 'API key pattern detected (key-)' },
58
- { pattern: /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/, reason: 'Private key detected' },
59
- { pattern: /password\s*[:=]\s*["'][^"']{4,}["']/i, reason: 'Hardcoded password detected' },
60
- { pattern: /secret\s*[:=]\s*["'][^"']{4,}["']/i, reason: 'Hardcoded secret detected' },
61
- { pattern: /AIza[0-9A-Za-z\-_]{35}/, reason: 'Google API Key detected' },
62
- { pattern: /xox[bpras]-[0-9a-zA-Z\-]+/, reason: 'Slack Token detected' },
63
- { pattern: /eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.+/=]+/, reason: 'JWT token detected' },
64
- { pattern: /(postgres|mysql|mongodb(\+srv)?):\/\/[^:]+:[^@]+@/i, reason: 'Database connection string with credentials detected' },
65
- { pattern: /sk_live_[a-zA-Z0-9]{24,}/, reason: 'Stripe Secret Key detected' },
66
- { pattern: /sk-ant-[a-zA-Z0-9\-]{20,}/, reason: 'Anthropic API Key detected' },
67
- ];
68
-
69
- function scanForSecrets(content) {
70
- const warnings = [];
71
- for (const { pattern, reason } of SECRET_PATTERNS) {
72
- if (pattern.test(content)) {
73
- warnings.push(reason);
74
- }
75
- }
76
- return warnings;
77
- }
78
-
79
- // ── Post-Write L2 Scan ──────────────────────────────────────────────────────
80
- // Triggered by PostToolUse matcher: Write|Edit|NotebookEdit (see hooks.json).
81
- // If Claude Code adds new file-writing tools, update the matcher accordingly.
82
-
83
- /** Check for unrequested file creation (Write tool only). */
84
- function scanUnrequestedFiles(filePath, toolName) {
85
- if (!filePath || toolName?.toLowerCase() !== 'write') return [];
86
- const basename = filePath.split(/[/\\]/).pop() || '';
87
- const UNREQUESTED_PATTERNS = [
88
- { pattern: /^(SUMMARY|NOTES|TODO|SCRATCH|TEMP)\.(md|txt)$/i, reason: `Unrequested file creation: ${basename}` },
89
- { pattern: /^README.*\.md$/i, test: () => {
90
- const depth = filePath.replace(/\\/g, '/').split('/').length;
91
- return depth > 4;
92
- }, reason: `Suspicious README creation in nested path: ${basename}` },
93
- ];
94
- const warnings = [];
95
- for (const { pattern, test, reason } of UNREQUESTED_PATTERNS) {
96
- if (pattern.test(basename) && (!test || test())) warnings.push(reason);
43
+ function readHookInput() {
44
+ try {
45
+ return JSON.parse(readFileSync(0, 'utf-8'))
46
+ } catch {
47
+ return {}
97
48
  }
98
- return warnings;
99
49
  }
100
50
 
101
- /** Check for dangerous npm scripts and unsafe dependency patterns. */
102
- function scanDangerousPackages(content, filePath) {
103
- const warnings = [];
104
- if (filePath.endsWith('package.json')) {
105
- const dangerousScripts = /("(preinstall|postinstall|preuninstall)")\s*:\s*"[^"]*\b(curl|wget|bash|sh|eval|exec)\b/i;
106
- if (dangerousScripts.test(content)) {
107
- warnings.push('Potentially dangerous lifecycle script in package.json (preinstall/postinstall with curl/wget/bash/eval)');
51
+ function emitHookPayload(payload) {
52
+ process.stdout.write(JSON.stringify(payload))
53
+ }
54
+
55
+ function emitGuardEvent(cwd, event, source, reason, details = {}) {
56
+ appendReplayEvent(cwd, {
57
+ host: HOST,
58
+ event,
59
+ source,
60
+ reason,
61
+ details,
62
+ })
63
+ }
64
+
65
+ function buildHighRiskGate(matches, cwd) {
66
+ const stateSyncHint = buildStateSyncHint(cwd)
67
+ if (stateSyncHint) {
68
+ return {
69
+ reason: `[HelloAGENTS Guard] Blocked T3 command until project recovery state is synced.\n${stateSyncHint}`,
108
70
  }
109
71
  }
110
- const unsafeInstall = /npm install\s+[^-].*--ignore-scripts\s*=\s*false|pip install\s+--trusted-host|pip install\s+http:/i;
111
- if (unsafeInstall.test(content)) {
112
- warnings.push('Unsafe dependency installation pattern detected');
72
+
73
+ const recommendation = getWorkflowRecommendation(cwd)
74
+ if (!recommendation) return null
75
+ if (matches.some((match) => match.gate === 'post-verify')) {
76
+ return {
77
+ reason: `[HelloAGENTS Guard] Blocked T3 command until workflow reaches VERIFY / CONSOLIDATE.\n当前工作流:${recommendation.summary}\n建议路径:${recommendation.nextPath}\n${recommendation.guidance}`,
78
+ }
113
79
  }
114
- return warnings;
115
- }
116
-
117
- /** Check if .env file is covered by .gitignore. */
118
- function scanEnvCoverage(filePath) {
119
- if (!filePath.endsWith('.env') && !filePath.includes('.env.')) return [];
120
- let dir = dirname(filePath);
121
- for (let i = 0; i < 10; i++) {
122
- try {
123
- const gitignore = readFileSync(join(dir, '.gitignore'), 'utf-8');
124
- return gitignore.includes('.env') ? [] : ['.env file written but .gitignore does not contain .env pattern'];
125
- } catch {
126
- const parent = dirname(dir);
127
- if (parent === dir) break;
128
- dir = parent;
80
+ if (matches.some((match) => match.gate === 'plan-first') && recommendation.nextCommand === 'plan') {
81
+ return {
82
+ 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}`,
129
83
  }
130
84
  }
131
- return ['.env file written but no .gitignore found'];
85
+ return null
132
86
  }
133
87
 
134
- function postWriteScan(data) {
135
- const settings = readSettings();
136
- if (settings.guard_enabled === false) {
137
- return;
138
- }
88
+ function buildIdeaBoundaryReason(kind) {
89
+ return `[HelloAGENTS Guard] Blocked ${kind} during ~idea.\n当前路由:~idea 是只读探索;先停留在比较方案。若要写文件、改代码、创建知识库或执行有副作用的命令,请先升级到 ~plan / ~build / ~prd / ~auto。`
90
+ }
139
91
 
140
- const content = data.tool_input?.content || data.tool_input?.new_string || '';
141
- const filePath = data.tool_input?.file_path || '';
92
+ function detectIdeaBoundaryContext(data) {
93
+ return getApplicableRouteContext({
94
+ cwd: data.cwd || process.cwd(),
95
+ filePath: data.tool_input?.file_path || '',
96
+ })
97
+ }
142
98
 
143
- if (!content && !filePath) {
144
- return;
145
- }
99
+ function emitIdeaBoundaryBlock(data, kind, target) {
100
+ const reason = `${buildIdeaBoundaryReason(kind)}\n${target}`
101
+ emitHookPayload({
102
+ hookSpecificOutput: {
103
+ hookEventName: HOOK_EVENT,
104
+ permissionDecision: 'deny',
105
+ permissionDecisionReason: reason,
106
+ },
107
+ })
108
+ emitGuardEvent(data.cwd || process.cwd(), 'guard_blocked', kind === 'write' ? 'pre-write' : 'command', buildIdeaBoundaryReason(kind), {
109
+ command: kind === 'side-effect command' ? target.replace(/^Command:\s*/, '') : '',
110
+ target: kind === 'write' ? target.replace(/^Target:\s*/, '') : '',
111
+ guardType: kind === 'write' ? 'idea-write-boundary' : 'idea-command-boundary',
112
+ })
113
+ }
146
114
 
147
- const warnings = [
115
+ function preWriteGuard(data) {
116
+ if (readSettings().guard_enabled === false) return
117
+ if (!detectIdeaBoundaryContext(data)?.zeroSideEffect) return
118
+ emitIdeaBoundaryBlock(data, 'write', `Target: ${data.tool_input?.file_path || '(unknown file)'}`)
119
+ }
120
+
121
+ function buildPostWriteWarnings(data) {
122
+ const content = data.tool_input?.content || data.tool_input?.new_string || ''
123
+ const filePath = data.tool_input?.file_path || ''
124
+ return [
125
+ ...(detectIdeaBoundaryContext(data)?.zeroSideEffect
126
+ ? ['~idea 本轮要求只读探索;检测到写入工具落地,请回退到探索输出或升级到 ~plan / ~build / ~prd / ~auto 后再修改文件']
127
+ : []),
148
128
  ...scanUnrequestedFiles(filePath, data.tool_name),
149
129
  ...(content ? [...scanForSecrets(content), ...scanDangerousPackages(content, filePath)] : []),
150
130
  ...scanEnvCoverage(filePath),
151
- ];
131
+ ]
132
+ }
133
+
134
+ function postWriteScan(data) {
135
+ if (readSettings().guard_enabled === false) return
136
+ const warnings = buildPostWriteWarnings(data)
137
+ if (warnings.length === 0) return
138
+
139
+ emitHookPayload({
140
+ hookSpecificOutput: {
141
+ hookEventName: HOOK_EVENT,
142
+ additionalContext: `⚠️ [HelloAGENTS L2 安全扫描] 检测到潜在问题:\n${warnings.map((warning) => ` - ${warning}`).join('\n')}\n请检查以上问题。`,
143
+ },
144
+ })
145
+ emitGuardEvent(data.cwd || process.cwd(), 'guard_warning', 'post-write', '', {
146
+ warnings,
147
+ guardType: 'post-write-l2',
148
+ })
149
+ }
152
150
 
153
- if (warnings.length > 0) {
151
+ function handleDangerousCommand(data, command) {
152
+ for (const { pattern, reason } of DANGEROUS_PATTERNS) {
153
+ if (!pattern.test(command)) continue
154
154
  emitHookPayload({
155
155
  hookSpecificOutput: {
156
156
  hookEventName: HOOK_EVENT,
157
- additionalContext: `⚠️ [HelloAGENTS L2 安全扫描] 检测到潜在问题:\n${warnings.map(w => ` - ${w}`).join('\n')}\n请检查以上问题。`,
157
+ permissionDecision: 'deny',
158
+ permissionDecisionReason: `[HelloAGENTS Guard] Blocked: ${reason}\nCommand: ${command.slice(0, 200)}`,
158
159
  },
159
- });
160
+ })
161
+ emitGuardEvent(data.cwd || process.cwd(), 'guard_blocked', 'command', reason, {
162
+ command: command.slice(0, 200),
163
+ guardType: 'dangerous-command',
164
+ })
165
+ return true
160
166
  }
167
+ return false
161
168
  }
162
169
 
163
- // ── Main ──────────────────────────────────────────────────────────────────
164
-
165
- async function main() {
166
- // Latest Codex rejects suppressOutput on PreToolUse/PostToolUse.
167
- // For pass-through cases, emit nothing and exit 0.
170
+ function handleHighRiskCommand(data, command) {
171
+ const warnings = scanHighRiskCommands(command)
172
+ if (warnings.length === 0) return []
168
173
 
169
- // Check if running in post-write mode (PostToolUse)
170
- const mode = process.argv[2] || '';
171
- if (mode === 'post-write') {
172
- let data = {};
173
- try {
174
- const input = readFileSync(0, 'utf-8');
175
- data = JSON.parse(input);
176
- } catch {}
177
- postWriteScan(data);
178
- return;
174
+ const cwd = data.cwd || process.cwd()
175
+ const gate = buildHighRiskGate(warnings, cwd)
176
+ if (gate) {
177
+ emitHookPayload({
178
+ hookSpecificOutput: {
179
+ hookEventName: HOOK_EVENT,
180
+ permissionDecision: 'deny',
181
+ permissionDecisionReason: `${gate.reason}\nCommand: ${command.slice(0, 200)}`,
182
+ },
183
+ })
184
+ emitGuardEvent(cwd, 'guard_blocked', 'command', gate.reason, {
185
+ command: command.slice(0, 200),
186
+ guardType: 'high-risk-gate',
187
+ matches: warnings.map((warning) => warning.reason),
188
+ })
189
+ return null
179
190
  }
191
+ return warnings.map((warning) => warning.reason)
192
+ }
180
193
 
181
- const settings = readSettings();
182
- if (settings.guard_enabled === false) {
183
- return;
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请确认已完成相应规划/审查并获得必要授权。`)
184
198
  }
185
-
186
- let data = {};
187
- try {
188
- const input = readFileSync(0, 'utf-8');
189
- data = JSON.parse(input);
190
- } catch {}
191
-
192
- // Only check Bash/shell tool calls
193
- const toolName = (data.tool_name || '').toLowerCase();
194
- if (!['bash', 'shell', 'terminal', 'command'].some(t => toolName.includes(t))) {
195
- return;
199
+ if (shellSafetyWarnings.length > 0) {
200
+ sections.push(`⚠️ [HelloAGENTS Shell 安全提醒] 检测到建议调整的命令写法:\n${shellSafetyWarnings.map((warning) => ` - ${warning}`).join('\n')}\n当前仅提示,不中断执行。`)
196
201
  }
202
+ if (sections.length === 0) return
203
+
204
+ emitHookPayload({
205
+ hookSpecificOutput: {
206
+ hookEventName: HOOK_EVENT,
207
+ additionalContext: sections.join('\n\n'),
208
+ },
209
+ })
197
210
 
198
- const command = data.tool_input?.command || data.tool_input?.input || '';
199
- if (!command) {
200
- return;
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
+ })
201
225
  }
226
+ }
202
227
 
203
- for (const { pattern, reason } of DANGEROUS_PATTERNS) {
204
- if (pattern.test(command)) {
205
- emitHookPayload({
206
- hookSpecificOutput: {
207
- hookEventName: HOOK_EVENT,
208
- permissionDecision: 'deny',
209
- permissionDecisionReason: `[HelloAGENTS Guard] Blocked: ${reason}\nCommand: ${command.slice(0, 200)}`,
210
- },
211
- });
212
- return;
228
+ function handleShellCommand(data) {
229
+ const toolName = (data.tool_name || '').toLowerCase()
230
+ if (!['bash', 'shell', 'terminal', 'command'].some((name) => toolName.includes(name))) return
231
+
232
+ const command = data.tool_input?.command || data.tool_input?.input || ''
233
+ if (!command) return
234
+
235
+ if (detectIdeaBoundaryContext(data)?.zeroSideEffect) {
236
+ for (const pattern of IDEA_SIDE_EFFECT_COMMAND_PATTERNS) {
237
+ if (!pattern.test(command)) continue
238
+ emitIdeaBoundaryBlock(data, 'side-effect command', `Command: ${command.slice(0, 200)}`)
239
+ return
213
240
  }
214
241
  }
242
+
243
+ if (handleDangerousCommand(data, command)) return
244
+ const highRiskWarnings = handleHighRiskCommand(data, command)
245
+ if (highRiskWarnings === null) return
246
+
247
+ const shellSafetyWarnings = scanShellSafetyWarnings(command)
248
+ emitShellWarnings(data, command, highRiskWarnings, shellSafetyWarnings)
249
+ }
250
+
251
+ async function main() {
252
+ const mode = process.argv[2] || ''
253
+ const data = readHookInput()
254
+
255
+ if (mode === 'pre-write') {
256
+ preWriteGuard(data)
257
+ return
258
+ }
259
+ if (mode === 'post-write') {
260
+ postWriteScan(data)
261
+ return
262
+ }
263
+ if (readSettings().guard_enabled === false) return
264
+ handleShellCommand(data)
215
265
  }
216
266
 
217
- main().catch(() => {});
267
+ main().catch(() => {})
@@ -1,6 +1,19 @@
1
1
  import { join } from 'node:path';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
+ import { buildCommandRouteHint, buildStateSyncHint, buildWorkflowRouteHint } from './workflow-state.mjs';
5
+ import { buildCapabilityHint } from './capability-registry.mjs';
6
+ import {
7
+ buildProjectStorageBlock,
8
+ buildProjectStorageHint,
9
+ describeProjectStoreFile,
10
+ } from './project-storage.mjs';
11
+
12
+ const COMMAND_ALIASES = {
13
+ do: 'build',
14
+ design: 'plan',
15
+ review: 'verify',
16
+ };
4
17
 
5
18
  function buildPackageRootBlock(pkgRoot) {
6
19
  if (!pkgRoot) return '';
@@ -18,11 +31,6 @@ function resolveStandbyHostRoot(host) {
18
31
  }
19
32
 
20
33
  function resolveReadRoot({ cwd, pkgRoot, host, settings }) {
21
- const projectRoot = join(cwd, 'skills', 'helloagents');
22
- if (existsSync(projectRoot)) {
23
- return { source: 'project', root: projectRoot };
24
- }
25
-
26
34
  if (settings.install_mode === 'standby') {
27
35
  const standbyRoot = resolveStandbyHostRoot(host);
28
36
  if (standbyRoot && existsSync(standbyRoot)) {
@@ -35,7 +43,24 @@ function resolveReadRoot({ cwd, pkgRoot, host, settings }) {
35
43
 
36
44
  function buildReadRootBlock(readRoot) {
37
45
  if (!readRoot?.root) return '';
38
- return `## 当前 HelloAGENTS 读取根目录\n\`\`\`json\n${JSON.stringify(readRoot, null, 2)}\n\`\`\``;
46
+ return `## 本轮 HelloAGENTS 读取根目录\n\`\`\`json\n${JSON.stringify(readRoot, null, 2)}\n\`\`\``;
47
+ }
48
+
49
+ export function resolveCanonicalCommandSkill(skillName) {
50
+ return COMMAND_ALIASES[skillName] || skillName;
51
+ }
52
+
53
+ function buildAliasRouteNote(skillName) {
54
+ if (skillName === 'do') {
55
+ return '兼容别名映射:本次按 ~build 规则执行。';
56
+ }
57
+ if (skillName === 'design') {
58
+ return '兼容别名映射:本次按 ~plan 规则执行;方案文件使用 `plan.md`,项目级 UI 契约仍使用 `DESIGN.md`。';
59
+ }
60
+ if (skillName === 'review') {
61
+ return '兼容别名映射:本次按 ~verify 的审查优先模式执行。';
62
+ }
63
+ return '';
39
64
  }
40
65
 
41
66
  export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFile, host }) {
@@ -45,11 +70,13 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
45
70
 
46
71
  const cwd = payload.cwd || process.cwd();
47
72
  const statePath = join(cwd, '.helloagents', 'STATE.md');
73
+ const stateSyncHint = buildStateSyncHint(cwd);
48
74
  if (existsSync(statePath)) {
49
75
  try {
50
76
  const stateContent = readFileSync(statePath, 'utf-8');
51
77
  summaryParts.push('');
52
- summaryParts.push('## 恢复快照(从 STATE.md 读取,读完即可接上工作)');
78
+ summaryParts.push('## 恢复快照(从 STATE.md 读取,只用于找回上次停在哪)');
79
+ summaryParts.push('恢复时先看当前用户消息,确认仍是同一任务再按 STATE.md 接续。');
53
80
  summaryParts.push(stateContent);
54
81
  } catch {}
55
82
  }
@@ -76,6 +103,18 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
76
103
  summaryParts.push(readRootBlock);
77
104
  }
78
105
 
106
+ const projectStorageBlock = buildProjectStorageBlock(cwd);
107
+ if (projectStorageBlock) {
108
+ summaryParts.push('');
109
+ summaryParts.push(projectStorageBlock);
110
+ }
111
+
112
+ if (stateSyncHint) {
113
+ summaryParts.push('');
114
+ summaryParts.push('## STATE.md 提醒');
115
+ summaryParts.push(stateSyncHint);
116
+ }
117
+
79
118
  if (Object.keys(settings).length) {
80
119
  summaryParts.push('');
81
120
  summaryParts.push(`## 当前用户设置\n\`\`\`json\n${JSON.stringify(settings, null, 2)}\n\`\`\``);
@@ -87,6 +126,10 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
87
126
  export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host, cwd }) {
88
127
  const packageRootBlock = buildPackageRootBlock(pkgRoot);
89
128
  const readRootBlock = buildReadRootBlock(resolveReadRoot({ cwd, pkgRoot, host, settings }));
129
+ const workflowHint = buildWorkflowRouteHint(cwd);
130
+ const capabilityHint = buildCapabilityHint({ cwd });
131
+ const projectStorageBlock = buildProjectStorageBlock(cwd);
132
+ const stateSyncHint = buildStateSyncHint(cwd);
90
133
  const settingsBlock = Object.keys(settings).length
91
134
  ? `\n\n## 当前用户设置\n\`\`\`json\n${JSON.stringify(settings, null, 2)}\n\`\`\``
92
135
  : '';
@@ -94,30 +137,42 @@ export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host,
94
137
  let context = bootstrap;
95
138
  if (packageRootBlock) context += `\n\n${packageRootBlock}`;
96
139
  if (readRootBlock) context += `\n\n${readRootBlock}`;
140
+ if (projectStorageBlock) context += `\n\n${projectStorageBlock}`;
141
+ if (workflowHint) context += `\n\n## 当前工作流提示\n${workflowHint}`;
142
+ if (capabilityHint) context += `\n\n## 当前按需能力\n${capabilityHint}`;
143
+ if (stateSyncHint) context += `\n\n## STATE.md 提醒\n${stateSyncHint}`;
97
144
  context += settingsBlock;
98
145
  if (source === 'resume' || source === 'compact') {
99
- context += '\n\n> ⚠️ 会话已恢复/压缩,请先读取 .helloagents/STATE.md 恢复工作状态。';
146
+ context += '\n\n> ⚠️ 会话已恢复/压缩,请先读取 `.helloagents/STATE.md` 恢复工作状态;先看当前用户消息确认仍是同一任务,再按 STATE.md 接续。';
100
147
  }
101
148
  return context;
102
149
  }
103
150
 
104
151
  export function buildRouteInstruction({ skillName, extraRules = '', cwd, pkgRoot, host, settings }) {
105
152
  const readRoot = resolveReadRoot({ cwd, pkgRoot, host, settings });
106
- const skillPath = join(readRoot.root, 'skills', 'commands', skillName, 'SKILL.md');
107
- return `用户使用了 ~${skillName} 命令。当前命令技能文件已解析为:${skillPath}。请直接读取这个 SKILL.md;本轮不要再为同一个命令 skill 重复 Test-Path / Get-Content,也不要探测其他 helloagents 路径。${extraRules}`;
153
+ const canonicalSkillName = resolveCanonicalCommandSkill(skillName);
154
+ const skillPath = join(readRoot.root, 'skills', 'commands', canonicalSkillName, 'SKILL.md');
155
+ const aliasNote = buildAliasRouteNote(skillName);
156
+ const commandHint = buildCommandRouteHint(canonicalSkillName, cwd);
157
+ const capabilityHint = buildCapabilityHint({ cwd, skillName: canonicalSkillName });
158
+ const projectStorageHint = buildProjectStorageHint(cwd);
159
+ return `用户使用了 ~${skillName} 命令。当前命令技能文件已解析为:${skillPath}。请直接读取这个 SKILL.md;不要再探测其他 helloagents 路径。${aliasNote ? ` ${aliasNote}` : ''}${projectStorageHint ? ` ${projectStorageHint}` : ''}${commandHint ? ` ${commandHint}` : ''}${capabilityHint ? ` ${capabilityHint}` : ''}${extraRules}`;
108
160
  }
109
161
 
110
- export function detectNewProjectRoute(prompt) {
111
- const newProjectPatterns = [
112
- /(?:创建|新建|从零|搭建).*(?:项目|应用|系统|网站|游戏|工具|平台|小程序|APP)/,
113
- /(?:做|写|开发|实现)[一个]*.*(?:项目|应用|系统|网站|游戏|工具|平台|小程序|APP)/,
114
- /\b(build|create|design|make|new|start|init)\b.*\b(app|game|project|site|website|tool|system|platform)\b/i,
115
- ];
116
-
117
- for (const pattern of newProjectPatterns) {
118
- if (pattern.test(prompt)) {
119
- return '检测到可能是新项目/新应用任务。根据 HelloAGENTS 路由规则,新项目必须进入 ~design 设计流程。请引导用户进入 ~design。';
120
- }
121
- }
122
- return '';
162
+ export function buildSemanticRouteInstruction(cwd) {
163
+ const workflowHint = buildWorkflowRouteHint(cwd);
164
+ const capabilityHint = buildCapabilityHint({ cwd });
165
+ const projectStorageHint = buildProjectStorageHint(cwd);
166
+ return [
167
+ '当前消息未使用 ~command。',
168
+ '请根据用户请求的真实意图选路,不依赖关键词表。',
169
+ 'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆链路。',
170
+ '路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动编排并自动衔接后续阶段。',
171
+ '若判定为 T3,默认先走 ~plan / ~prd;纯审查/验证请求才优先 ~verify。',
172
+ `涉及 UI 任务时,设计决策优先级:当前活跃 plan / PRD → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → 通用 UI 规则。`,
173
+ projectStorageHint,
174
+ workflowHint ? `项目状态:${workflowHint}` : '',
175
+ capabilityHint,
176
+ '意图明确时直接按对应路径推进,不要把选路过程暴露给用户。',
177
+ ].filter(Boolean).join(' ');
123
178
  }
@@ -7,5 +7,9 @@ export function shouldIgnoreFormattedSubagent(lastMsg, outputFormatEnabled) {
7
7
  }
8
8
 
9
9
  export function claimsTaskComplete(lastMsg) {
10
- return /✅|完成|已修复|done|fixed|completed|finished/i.test(lastMsg);
10
+ if (!lastMsg) return false;
11
+ if (/^✅【HelloAGENTS】- .*(当前任务已完成|任务已完成|已修复|完成交付|done|fixed|completed|finished)/im.test(lastMsg)) {
12
+ return true;
13
+ }
14
+ return /(当前任务已完成|任务已完成|已全部完成|已修复|修复完成|\b(done|fixed|completed|finished)\b)/i.test(lastMsg);
11
15
  }