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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +157 -57
- package/README_CN.md +157 -57
- package/bootstrap-lite.md +125 -50
- package/bootstrap.md +169 -123
- 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 +1 -1
- 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 +94 -0
- package/scripts/cli-codex.mjs +90 -222
- package/scripts/cli-config.mjs +1 -0
- package/scripts/cli-doctor-render.mjs +28 -0
- package/scripts/cli-doctor.mjs +370 -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/cli-toml.mjs +30 -0
- package/scripts/closeout-state.mjs +213 -0
- package/scripts/delivery-gate.mjs +256 -0
- package/scripts/guard-rules.mjs +147 -0
- package/scripts/guard.mjs +218 -168
- package/scripts/notify-context.mjs +78 -23
- 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 +137 -65
- 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/turn-state.mjs +173 -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/_meta/SKILL.md +1 -1
- package/skills/commands/auto/SKILL.md +48 -67
- 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 +18 -11
- package/skills/commands/idea/SKILL.md +55 -0
- package/skills/commands/init/SKILL.md +16 -8
- package/skills/commands/loop/SKILL.md +6 -5
- package/skills/commands/plan/SKILL.md +118 -0
- package/skills/commands/prd/SKILL.md +22 -15
- package/skills/commands/verify/SKILL.md +32 -9
- package/skills/commands/wiki/SKILL.md +11 -11
- package/skills/hello-review/SKILL.md +9 -0
- package/skills/hello-subagent/SKILL.md +5 -3
- package/skills/hello-ui/SKILL.md +36 -8
- package/skills/hello-verify/SKILL.md +12 -3
- package/skills/helloagents/SKILL.md +36 -20
- 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,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
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
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
|
-
|| (
|
|
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 {
|
|
22
|
-
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
|
|
38
|
+
} catch {
|
|
39
|
+
return {}
|
|
40
|
+
}
|
|
23
41
|
}
|
|
24
42
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
85
|
+
return null
|
|
132
86
|
}
|
|
133
87
|
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
|
199
|
-
if (
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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 `##
|
|
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> ⚠️ 会话已恢复/压缩,请先读取
|
|
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
|
|
107
|
-
|
|
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
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
}
|