helloagents 3.0.3-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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +134 -41
- package/README_CN.md +135 -42
- package/bootstrap-lite.md +104 -46
- package/bootstrap.md +143 -113
- package/cli.mjs +80 -427
- package/gemini-extension.json +1 -1
- package/hooks/hooks-claude.json +10 -0
- package/hooks/hooks.json +10 -0
- package/package.json +2 -12
- package/scripts/advisor-state.mjs +222 -0
- package/scripts/capability-registry.mjs +59 -0
- package/scripts/cli-codex-backup.mjs +59 -0
- package/scripts/cli-codex-config.mjs +100 -0
- package/scripts/cli-codex.mjs +34 -156
- package/scripts/cli-config.mjs +1 -0
- package/scripts/cli-doctor-render.mjs +28 -0
- package/scripts/cli-doctor.mjs +367 -0
- package/scripts/cli-host-detect.mjs +94 -0
- package/scripts/cli-lifecycle-hosts.mjs +123 -0
- package/scripts/cli-lifecycle.mjs +213 -0
- package/scripts/cli-messages.mjs +76 -52
- package/scripts/closeout-state.mjs +213 -0
- package/scripts/delivery-gate.mjs +256 -0
- package/scripts/guard-rules.mjs +122 -0
- package/scripts/guard.mjs +190 -168
- package/scripts/notify-context.mjs +77 -17
- package/scripts/notify-events.mjs +5 -1
- package/scripts/notify-route.mjs +111 -0
- package/scripts/notify-shared.mjs +0 -2
- package/scripts/notify-source.mjs +113 -0
- package/scripts/notify-ui.mjs +40 -6
- package/scripts/notify.mjs +120 -59
- package/scripts/plan-contract.mjs +210 -0
- package/scripts/project-storage.mjs +235 -0
- package/scripts/ralph-loop.mjs +9 -58
- package/scripts/replay-state.mjs +210 -0
- package/scripts/review-state.mjs +220 -0
- package/scripts/runtime-context.mjs +74 -0
- package/scripts/verify-state.mjs +226 -0
- package/scripts/visual-state.mjs +244 -0
- package/scripts/workflow-core.mjs +165 -0
- package/scripts/workflow-plan-files.mjs +249 -0
- package/scripts/workflow-recommendation.mjs +335 -0
- package/scripts/workflow-state.mjs +113 -0
- package/skills/commands/auto/SKILL.md +37 -71
- package/skills/commands/build/SKILL.md +67 -0
- package/skills/commands/clean/SKILL.md +10 -8
- package/skills/commands/commit/SKILL.md +8 -4
- package/skills/commands/help/SKILL.md +17 -10
- package/skills/commands/idea/SKILL.md +55 -0
- package/skills/commands/init/SKILL.md +6 -3
- package/skills/commands/loop/SKILL.md +6 -5
- package/skills/commands/plan/SKILL.md +116 -0
- package/skills/commands/prd/SKILL.md +20 -15
- package/skills/commands/verify/SKILL.md +32 -9
- package/skills/commands/wiki/SKILL.md +7 -5
- package/skills/hello-review/SKILL.md +9 -0
- package/skills/hello-subagent/SKILL.md +4 -3
- package/skills/hello-ui/SKILL.md +36 -8
- package/skills/hello-verify/SKILL.md +10 -2
- package/skills/helloagents/SKILL.md +24 -13
- package/templates/DESIGN.md +25 -4
- package/templates/STATE.md +3 -0
- package/templates/plans/contract.json +48 -0
- package/templates/plans/plan.md +23 -0
- package/templates/plans/tasks.md +3 -3
- package/skills/commands/design/SKILL.md +0 -108
- package/skills/commands/review/SKILL.md +0 -16
- 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
|
|
9
|
-
import { homedir } from 'node:os'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|| (
|
|
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 {
|
|
22
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
84
|
+
return null
|
|
115
85
|
}
|
|
116
86
|
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
const
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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> ⚠️ 会话已恢复/压缩,请先读取
|
|
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
|
|
107
|
-
|
|
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
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
}
|