helloagents 3.0.2-beta.1 → 3.0.7

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 +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +147 -45
  5. package/README_CN.md +148 -46
  6. package/bootstrap-lite.md +104 -46
  7. package/bootstrap.md +143 -112
  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 +2 -12
  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 +100 -0
  17. package/scripts/cli-codex.mjs +34 -156
  18. package/scripts/cli-config.mjs +1 -0
  19. package/scripts/cli-doctor-render.mjs +28 -0
  20. package/scripts/cli-doctor.mjs +367 -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/closeout-state.mjs +213 -0
  26. package/scripts/delivery-gate.mjs +256 -0
  27. package/scripts/guard-rules.mjs +122 -0
  28. package/scripts/guard.mjs +190 -168
  29. package/scripts/notify-context.mjs +77 -17
  30. package/scripts/notify-events.mjs +5 -1
  31. package/scripts/notify-route.mjs +111 -0
  32. package/scripts/notify-shared.mjs +0 -2
  33. package/scripts/notify-source.mjs +113 -0
  34. package/scripts/notify-ui.mjs +40 -6
  35. package/scripts/notify.mjs +120 -59
  36. package/scripts/plan-contract.mjs +210 -0
  37. package/scripts/project-storage.mjs +235 -0
  38. package/scripts/ralph-loop.mjs +9 -58
  39. package/scripts/replay-state.mjs +210 -0
  40. package/scripts/review-state.mjs +220 -0
  41. package/scripts/runtime-context.mjs +74 -0
  42. package/scripts/verify-state.mjs +226 -0
  43. package/scripts/visual-state.mjs +244 -0
  44. package/scripts/workflow-core.mjs +165 -0
  45. package/scripts/workflow-plan-files.mjs +249 -0
  46. package/scripts/workflow-recommendation.mjs +335 -0
  47. package/scripts/workflow-state.mjs +113 -0
  48. package/skills/commands/auto/SKILL.md +37 -71
  49. package/skills/commands/build/SKILL.md +67 -0
  50. package/skills/commands/clean/SKILL.md +10 -8
  51. package/skills/commands/commit/SKILL.md +8 -4
  52. package/skills/commands/help/SKILL.md +19 -11
  53. package/skills/commands/idea/SKILL.md +55 -0
  54. package/skills/commands/init/SKILL.md +6 -3
  55. package/skills/commands/loop/SKILL.md +6 -5
  56. package/skills/commands/plan/SKILL.md +116 -0
  57. package/skills/commands/prd/SKILL.md +20 -15
  58. package/skills/commands/verify/SKILL.md +32 -9
  59. package/skills/commands/wiki/SKILL.md +59 -0
  60. package/skills/hello-review/SKILL.md +9 -0
  61. package/skills/hello-subagent/SKILL.md +4 -3
  62. package/skills/hello-ui/SKILL.md +36 -8
  63. package/skills/hello-verify/SKILL.md +10 -2
  64. package/skills/helloagents/SKILL.md +24 -13
  65. package/templates/DESIGN.md +25 -4
  66. package/templates/STATE.md +3 -0
  67. package/templates/plans/contract.json +48 -0
  68. package/templates/plans/plan.md +23 -0
  69. package/templates/plans/tasks.md +3 -3
  70. package/skills/commands/design/SKILL.md +0 -108
  71. package/skills/commands/review/SKILL.md +0 -16
  72. package/templates/plans/design.md +0 -14
package/scripts/guard.mjs CHANGED
@@ -4,214 +4,236 @@
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';
10
-
11
- const CONFIG_FILE = join(homedir(), '.helloagents', 'helloagents.json');
12
-
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');
7
+ import { readFileSync } from 'node:fs'
8
+ import { join } from 'node:path'
9
+ import { homedir } from 'node:os'
10
+
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
+ scanUnrequestedFiles,
22
+ } from './guard-rules.mjs'
23
+
24
+ const CONFIG_FILE = join(homedir(), '.helloagents', 'helloagents.json')
25
+ const IS_GEMINI = process.argv.includes('--gemini')
26
+ const HOST = IS_GEMINI ? 'gemini' : 'claude'
17
27
  const HOOK_EVENT = process.env.HELLOAGENTS_HOOK_EVENT
18
- || (IS_POST_WRITE ? (IS_GEMINI ? 'AfterModel' : 'PostToolUse') : (IS_GEMINI ? 'BeforeTool' : 'PreToolUse'));
28
+ || (
29
+ process.argv.includes('post-write')
30
+ ? (IS_GEMINI ? 'AfterModel' : 'PostToolUse')
31
+ : (IS_GEMINI ? 'BeforeTool' : 'PreToolUse')
32
+ )
19
33
 
20
34
  function readSettings() {
21
- try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')); } catch {}
22
- return {};
35
+ try {
36
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
37
+ } catch {
38
+ return {}
39
+ }
40
+ }
41
+
42
+ function readHookInput() {
43
+ try {
44
+ return JSON.parse(readFileSync(0, 'utf-8'))
45
+ } catch {
46
+ return {}
47
+ }
23
48
  }
24
49
 
25
50
  function emitHookPayload(payload) {
26
- process.stdout.write(JSON.stringify(payload));
51
+ process.stdout.write(JSON.stringify(payload))
27
52
  }
28
53
 
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;
54
+ function emitGuardEvent(cwd, event, source, reason, details = {}) {
55
+ appendReplayEvent(cwd, {
56
+ host: HOST,
57
+ event,
58
+ source,
59
+ reason,
60
+ details,
61
+ })
77
62
  }
78
63
 
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);
64
+ function buildHighRiskGate(matches, cwd) {
65
+ const stateSyncHint = buildStateSyncHint(cwd)
66
+ if (stateSyncHint) {
67
+ return {
68
+ reason: `[HelloAGENTS Guard] Blocked T3 command until project recovery state is synced.\n${stateSyncHint}`,
69
+ }
97
70
  }
98
- return warnings;
99
- }
100
71
 
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)');
72
+ const recommendation = getWorkflowRecommendation(cwd)
73
+ if (!recommendation) return null
74
+ if (matches.some((match) => match.gate === 'post-verify')) {
75
+ return {
76
+ reason: `[HelloAGENTS Guard] Blocked T3 command until workflow reaches VERIFY / CONSOLIDATE.\n当前工作流:${recommendation.summary}\n建议路径:${recommendation.nextPath}\n${recommendation.guidance}`,
108
77
  }
109
78
  }
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');
79
+ if (matches.some((match) => match.gate === 'plan-first') && recommendation.nextCommand === 'plan') {
80
+ return {
81
+ 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}`,
82
+ }
113
83
  }
114
- return warnings;
84
+ return null
115
85
  }
116
86
 
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;
129
- }
130
- }
131
- return ['.env file written but no .gitignore found'];
87
+ function buildIdeaBoundaryReason(kind) {
88
+ return `[HelloAGENTS Guard] Blocked ${kind} during ~idea.\n当前路由:~idea 是只读探索;先停留在比较方案。若要写文件、改代码、创建知识库或执行有副作用的命令,请先升级到 ~plan / ~build / ~prd / ~auto。`
132
89
  }
133
90
 
134
- function postWriteScan(data) {
135
- const settings = readSettings();
136
- if (settings.guard_enabled === false) {
137
- return;
138
- }
91
+ function detectIdeaBoundaryContext(data) {
92
+ return getApplicableRouteContext({
93
+ cwd: data.cwd || process.cwd(),
94
+ filePath: data.tool_input?.file_path || '',
95
+ })
96
+ }
139
97
 
140
- const content = data.tool_input?.content || data.tool_input?.new_string || '';
141
- const filePath = data.tool_input?.file_path || '';
98
+ function emitIdeaBoundaryBlock(data, kind, target) {
99
+ const reason = `${buildIdeaBoundaryReason(kind)}\n${target}`
100
+ emitHookPayload({
101
+ hookSpecificOutput: {
102
+ hookEventName: HOOK_EVENT,
103
+ permissionDecision: 'deny',
104
+ permissionDecisionReason: reason,
105
+ },
106
+ })
107
+ emitGuardEvent(data.cwd || process.cwd(), 'guard_blocked', kind === 'write' ? 'pre-write' : 'command', buildIdeaBoundaryReason(kind), {
108
+ command: kind === 'side-effect command' ? target.replace(/^Command:\s*/, '') : '',
109
+ target: kind === 'write' ? target.replace(/^Target:\s*/, '') : '',
110
+ guardType: kind === 'write' ? 'idea-write-boundary' : 'idea-command-boundary',
111
+ })
112
+ }
142
113
 
143
- if (!content && !filePath) {
144
- return;
145
- }
114
+ function preWriteGuard(data) {
115
+ if (readSettings().guard_enabled === false) return
116
+ if (!detectIdeaBoundaryContext(data)?.zeroSideEffect) return
117
+ emitIdeaBoundaryBlock(data, 'write', `Target: ${data.tool_input?.file_path || '(unknown file)'}`)
118
+ }
146
119
 
147
- const warnings = [
120
+ function buildPostWriteWarnings(data) {
121
+ const content = data.tool_input?.content || data.tool_input?.new_string || ''
122
+ const filePath = data.tool_input?.file_path || ''
123
+ return [
124
+ ...(detectIdeaBoundaryContext(data)?.zeroSideEffect
125
+ ? ['~idea 当前轮要求只读探索;检测到写入工具落地,请回退到探索输出或升级到 ~plan / ~build / ~prd / ~auto 后再修改文件']
126
+ : []),
148
127
  ...scanUnrequestedFiles(filePath, data.tool_name),
149
128
  ...(content ? [...scanForSecrets(content), ...scanDangerousPackages(content, filePath)] : []),
150
129
  ...scanEnvCoverage(filePath),
151
- ];
130
+ ]
131
+ }
152
132
 
153
- if (warnings.length > 0) {
133
+ function postWriteScan(data) {
134
+ if (readSettings().guard_enabled === false) return
135
+ const warnings = buildPostWriteWarnings(data)
136
+ if (warnings.length === 0) return
137
+
138
+ emitHookPayload({
139
+ hookSpecificOutput: {
140
+ hookEventName: HOOK_EVENT,
141
+ additionalContext: `⚠️ [HelloAGENTS L2 安全扫描] 检测到潜在问题:\n${warnings.map((warning) => ` - ${warning}`).join('\n')}\n请检查以上问题。`,
142
+ },
143
+ })
144
+ emitGuardEvent(data.cwd || process.cwd(), 'guard_warning', 'post-write', '', {
145
+ warnings,
146
+ guardType: 'post-write-l2',
147
+ })
148
+ }
149
+
150
+ function handleDangerousCommand(data, command) {
151
+ for (const { pattern, reason } of DANGEROUS_PATTERNS) {
152
+ if (!pattern.test(command)) continue
154
153
  emitHookPayload({
155
154
  hookSpecificOutput: {
156
155
  hookEventName: HOOK_EVENT,
157
- additionalContext: `⚠️ [HelloAGENTS L2 安全扫描] 检测到潜在问题:\n${warnings.map(w => ` - ${w}`).join('\n')}\n请检查以上问题。`,
156
+ permissionDecision: 'deny',
157
+ permissionDecisionReason: `[HelloAGENTS Guard] Blocked: ${reason}\nCommand: ${command.slice(0, 200)}`,
158
158
  },
159
- });
159
+ })
160
+ emitGuardEvent(data.cwd || process.cwd(), 'guard_blocked', 'command', reason, {
161
+ command: command.slice(0, 200),
162
+ guardType: 'dangerous-command',
163
+ })
164
+ return true
160
165
  }
166
+ return false
161
167
  }
162
168
 
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.
169
+ function handleHighRiskCommand(data, command) {
170
+ const warnings = scanHighRiskCommands(command)
171
+ if (warnings.length === 0) return
168
172
 
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;
173
+ const cwd = data.cwd || process.cwd()
174
+ const gate = buildHighRiskGate(warnings, cwd)
175
+ if (gate) {
176
+ emitHookPayload({
177
+ hookSpecificOutput: {
178
+ hookEventName: HOOK_EVENT,
179
+ permissionDecision: 'deny',
180
+ permissionDecisionReason: `${gate.reason}\nCommand: ${command.slice(0, 200)}`,
181
+ },
182
+ })
183
+ emitGuardEvent(cwd, 'guard_blocked', 'command', gate.reason, {
184
+ command: command.slice(0, 200),
185
+ guardType: 'high-risk-gate',
186
+ matches: warnings.map((warning) => warning.reason),
187
+ })
188
+ return
179
189
  }
180
190
 
181
- const settings = readSettings();
182
- if (settings.guard_enabled === false) {
183
- return;
184
- }
191
+ emitHookPayload({
192
+ hookSpecificOutput: {
193
+ hookEventName: HOOK_EVENT,
194
+ additionalContext: `⚠️ [HelloAGENTS 高风险链路提醒] 检测到高风险命令:\n${warnings.map((warning) => ` - ${warning.reason}`).join('\n')}\n请确认已完成相应规划/审查并获得必要授权。`,
195
+ },
196
+ })
197
+ emitGuardEvent(cwd, 'guard_warning', 'command', '', {
198
+ guardType: 'high-risk-warning',
199
+ command: command.slice(0, 200),
200
+ warnings: warnings.map((warning) => warning.reason),
201
+ })
202
+ }
185
203
 
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;
196
- }
204
+ function handleShellCommand(data) {
205
+ const toolName = (data.tool_name || '').toLowerCase()
206
+ if (!['bash', 'shell', 'terminal', 'command'].some((name) => toolName.includes(name))) return
197
207
 
198
- const command = data.tool_input?.command || data.tool_input?.input || '';
199
- if (!command) {
200
- return;
201
- }
208
+ const command = data.tool_input?.command || data.tool_input?.input || ''
209
+ if (!command) return
202
210
 
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;
211
+ if (detectIdeaBoundaryContext(data)?.zeroSideEffect) {
212
+ for (const pattern of IDEA_SIDE_EFFECT_COMMAND_PATTERNS) {
213
+ if (!pattern.test(command)) continue
214
+ emitIdeaBoundaryBlock(data, 'side-effect command', `Command: ${command.slice(0, 200)}`)
215
+ return
213
216
  }
214
217
  }
218
+
219
+ if (handleDangerousCommand(data, command)) return
220
+ handleHighRiskCommand(data, command)
221
+ }
222
+
223
+ async function main() {
224
+ const mode = process.argv[2] || ''
225
+ const data = readHookInput()
226
+
227
+ if (mode === 'pre-write') {
228
+ preWriteGuard(data)
229
+ return
230
+ }
231
+ if (mode === 'post-write') {
232
+ postWriteScan(data)
233
+ return
234
+ }
235
+ if (readSettings().guard_enabled === false) return
236
+ handleShellCommand(data)
215
237
  }
216
238
 
217
- main().catch(() => {});
239
+ 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 '';
@@ -38,6 +51,23 @@ function buildReadRootBlock(readRoot) {
38
51
  return `## 当前 HelloAGENTS 读取根目录\n\`\`\`json\n${JSON.stringify(readRoot, null, 2)}\n\`\`\``;
39
52
  }
40
53
 
54
+ export function resolveCanonicalCommandSkill(skillName) {
55
+ return COMMAND_ALIASES[skillName] || skillName;
56
+ }
57
+
58
+ function buildAliasRouteNote(skillName) {
59
+ if (skillName === 'do') {
60
+ return '兼容别名映射:本次按 ~build 规则执行。';
61
+ }
62
+ if (skillName === 'design') {
63
+ return '兼容别名映射:本次按 ~plan 规则执行;方案文件使用 `plan.md`,项目级 UI 契约仍使用 `DESIGN.md`。';
64
+ }
65
+ if (skillName === 'review') {
66
+ return '兼容别名映射:本次按 ~verify 的审查优先模式执行。';
67
+ }
68
+ return '';
69
+ }
70
+
41
71
  export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFile, host }) {
42
72
  const summaryParts = [];
43
73
  summaryParts.push('## HelloAGENTS 压缩摘要');
@@ -45,11 +75,13 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
45
75
 
46
76
  const cwd = payload.cwd || process.cwd();
47
77
  const statePath = join(cwd, '.helloagents', 'STATE.md');
78
+ const stateSyncHint = buildStateSyncHint(cwd);
48
79
  if (existsSync(statePath)) {
49
80
  try {
50
81
  const stateContent = readFileSync(statePath, 'utf-8');
51
82
  summaryParts.push('');
52
- summaryParts.push('## 恢复快照(从 STATE.md 读取,读完即可接上工作)');
83
+ summaryParts.push('## 恢复快照(从 STATE.md 读取,只用于找回上次停在哪)');
84
+ summaryParts.push('恢复时先看当前用户消息,确认仍是同一任务再按 STATE.md 接续。');
53
85
  summaryParts.push(stateContent);
54
86
  } catch {}
55
87
  }
@@ -76,6 +108,18 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
76
108
  summaryParts.push(readRootBlock);
77
109
  }
78
110
 
111
+ const projectStorageBlock = buildProjectStorageBlock(cwd);
112
+ if (projectStorageBlock) {
113
+ summaryParts.push('');
114
+ summaryParts.push(projectStorageBlock);
115
+ }
116
+
117
+ if (stateSyncHint) {
118
+ summaryParts.push('');
119
+ summaryParts.push('## STATE.md 提醒');
120
+ summaryParts.push(stateSyncHint);
121
+ }
122
+
79
123
  if (Object.keys(settings).length) {
80
124
  summaryParts.push('');
81
125
  summaryParts.push(`## 当前用户设置\n\`\`\`json\n${JSON.stringify(settings, null, 2)}\n\`\`\``);
@@ -87,6 +131,10 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
87
131
  export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host, cwd }) {
88
132
  const packageRootBlock = buildPackageRootBlock(pkgRoot);
89
133
  const readRootBlock = buildReadRootBlock(resolveReadRoot({ cwd, pkgRoot, host, settings }));
134
+ const workflowHint = buildWorkflowRouteHint(cwd);
135
+ const capabilityHint = buildCapabilityHint({ cwd });
136
+ const projectStorageBlock = buildProjectStorageBlock(cwd);
137
+ const stateSyncHint = buildStateSyncHint(cwd);
90
138
  const settingsBlock = Object.keys(settings).length
91
139
  ? `\n\n## 当前用户设置\n\`\`\`json\n${JSON.stringify(settings, null, 2)}\n\`\`\``
92
140
  : '';
@@ -94,30 +142,42 @@ export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host,
94
142
  let context = bootstrap;
95
143
  if (packageRootBlock) context += `\n\n${packageRootBlock}`;
96
144
  if (readRootBlock) context += `\n\n${readRootBlock}`;
145
+ if (projectStorageBlock) context += `\n\n${projectStorageBlock}`;
146
+ if (workflowHint) context += `\n\n## 当前工作流提示\n${workflowHint}`;
147
+ if (capabilityHint) context += `\n\n## 当前按需能力\n${capabilityHint}`;
148
+ if (stateSyncHint) context += `\n\n## STATE.md 提醒\n${stateSyncHint}`;
97
149
  context += settingsBlock;
98
150
  if (source === 'resume' || source === 'compact') {
99
- context += '\n\n> ⚠️ 会话已恢复/压缩,请先读取 .helloagents/STATE.md 恢复工作状态。';
151
+ context += '\n\n> ⚠️ 会话已恢复/压缩,请先读取 `.helloagents/STATE.md` 恢复工作状态;先看当前用户消息确认仍是同一任务,再按 STATE.md 接续。';
100
152
  }
101
153
  return context;
102
154
  }
103
155
 
104
156
  export function buildRouteInstruction({ skillName, extraRules = '', cwd, pkgRoot, host, settings }) {
105
157
  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}`;
158
+ const canonicalSkillName = resolveCanonicalCommandSkill(skillName);
159
+ const skillPath = join(readRoot.root, 'skills', 'commands', canonicalSkillName, 'SKILL.md');
160
+ const aliasNote = buildAliasRouteNote(skillName);
161
+ const commandHint = buildCommandRouteHint(canonicalSkillName, cwd);
162
+ const capabilityHint = buildCapabilityHint({ cwd, skillName: canonicalSkillName });
163
+ const projectStorageHint = buildProjectStorageHint(cwd);
164
+ return `用户使用了 ~${skillName} 命令。当前命令技能文件已解析为:${skillPath}。请直接读取这个 SKILL.md;不要再探测其他 helloagents 路径。${aliasNote ? ` ${aliasNote}` : ''}${projectStorageHint ? ` ${projectStorageHint}` : ''}${commandHint ? ` ${commandHint}` : ''}${capabilityHint ? ` ${capabilityHint}` : ''}${extraRules}`;
108
165
  }
109
166
 
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 '';
167
+ export function buildSemanticRouteInstruction(cwd) {
168
+ const workflowHint = buildWorkflowRouteHint(cwd);
169
+ const capabilityHint = buildCapabilityHint({ cwd });
170
+ const projectStorageHint = buildProjectStorageHint(cwd);
171
+ return [
172
+ '当前消息未使用 ~command。',
173
+ '请根据用户请求的真实意图选路,不依赖关键词表。',
174
+ 'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆链路。',
175
+ '路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动选路。',
176
+ '若判定为 T3,默认先走 ~plan / ~prd;纯审查/验证请求才优先 ~verify。',
177
+ `涉及 UI 任务时,设计决策优先级:当前活跃 plan / PRD → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → 通用 UI 规则。`,
178
+ projectStorageHint,
179
+ workflowHint ? `项目状态:${workflowHint}` : '',
180
+ capabilityHint,
181
+ '意图明确时直接按对应路径推进,不要把选路过程暴露给用户。',
182
+ ].filter(Boolean).join(' ');
123
183
  }
@@ -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
  }