helloagents 3.0.7 → 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 +62 -55
- package/README_CN.md +56 -49
- package/bootstrap-lite.md +42 -25
- package/bootstrap.md +46 -30
- package/gemini-extension.json +1 -1
- package/package.json +12 -2
- package/scripts/capability-registry.mjs +4 -4
- package/scripts/cli-codex-config.mjs +49 -55
- package/scripts/cli-codex.mjs +67 -77
- package/scripts/cli-doctor.mjs +20 -17
- package/scripts/cli-messages.mjs +1 -1
- package/scripts/cli-toml.mjs +30 -0
- package/scripts/guard-rules.mjs +26 -1
- package/scripts/guard.mjs +38 -10
- package/scripts/notify-context.mjs +2 -7
- package/scripts/notify.mjs +19 -8
- package/scripts/turn-state.mjs +173 -0
- package/scripts/workflow-core.mjs +6 -6
- package/scripts/workflow-recommendation.mjs +14 -14
- package/scripts/workflow-state.mjs +2 -2
- package/skills/_meta/SKILL.md +1 -1
- package/skills/commands/auto/SKILL.md +24 -9
- package/skills/commands/build/SKILL.md +3 -3
- package/skills/commands/clean/SKILL.md +3 -3
- package/skills/commands/help/SKILL.md +3 -3
- package/skills/commands/idea/SKILL.md +2 -2
- package/skills/commands/init/SKILL.md +12 -7
- package/skills/commands/loop/SKILL.md +1 -1
- package/skills/commands/plan/SKILL.md +11 -9
- package/skills/commands/prd/SKILL.md +8 -6
- package/skills/commands/verify/SKILL.md +5 -5
- package/skills/commands/wiki/SKILL.md +8 -10
- package/skills/hello-review/SKILL.md +1 -1
- package/skills/hello-subagent/SKILL.md +3 -2
- package/skills/hello-ui/SKILL.md +12 -12
- package/skills/hello-verify/SKILL.md +6 -5
- package/skills/helloagents/SKILL.md +17 -12
package/scripts/cli-doctor.mjs
CHANGED
|
@@ -140,8 +140,8 @@ function inspectClaudeDoctor(settings) {
|
|
|
140
140
|
issues.push(buildDoctorIssue('tracked-mode-mismatch', '记录模式与检测模式不一致', 'Tracked mode does not match detected mode'))
|
|
141
141
|
}
|
|
142
142
|
if (detectedMode === 'standby') {
|
|
143
|
-
if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby
|
|
144
|
-
if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby
|
|
143
|
+
if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby 规则文件缺少 HELLOAGENTS 标记', 'Standby carrier is missing the HELLOAGENTS marker'))
|
|
144
|
+
if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby 规则文件内容与当前 bootstrap-lite.md 不一致', 'Standby carrier content differs from the current bootstrap-lite.md'))
|
|
145
145
|
if (!checks.homeLink) issues.push(buildDoctorIssue('standby-link-missing', 'standby home 链接缺失或未指向当前包根目录', 'Standby home link is missing or points to a different package root'))
|
|
146
146
|
if (!checks.settingsHooks) issues.push(buildDoctorIssue('standby-hooks-missing', 'standby settings hooks 缺失', 'Standby settings hooks are missing'))
|
|
147
147
|
if (checks.settingsHooks && !checks.settingsHooksMatch) issues.push(buildDoctorIssue('standby-hooks-drift', 'standby settings hooks 与当前 hooks 配置不一致', 'Standby settings hooks differ from the current hook configuration'))
|
|
@@ -160,7 +160,7 @@ function inspectClaudeDoctor(settings) {
|
|
|
160
160
|
issues.push(buildDoctorIssue('untracked-managed-state', '检测到受管状态,但配置中未记录该 CLI 模式', 'Managed state detected but this CLI mode is not tracked in config'))
|
|
161
161
|
}
|
|
162
162
|
if (trackedMode !== 'none' && detectedMode === 'none' && trackedMode !== 'global') {
|
|
163
|
-
issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI
|
|
163
|
+
issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI 已安装,但未检测到对应的受管文件或配置', 'Config says this CLI is installed, but no managed artifacts were detected'))
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
const status = summarizeDoctorStatus(issues, { host, trackedMode, detectedMode })
|
|
@@ -188,8 +188,8 @@ function inspectGeminiDoctor(settings) {
|
|
|
188
188
|
issues.push(buildDoctorIssue('tracked-mode-mismatch', '记录模式与检测模式不一致', 'Tracked mode does not match detected mode'))
|
|
189
189
|
}
|
|
190
190
|
if (detectedMode === 'standby') {
|
|
191
|
-
if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby
|
|
192
|
-
if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby
|
|
191
|
+
if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby 规则文件缺少 HELLOAGENTS 标记', 'Standby carrier is missing the HELLOAGENTS marker'))
|
|
192
|
+
if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby 规则文件内容与当前 bootstrap-lite.md 不一致', 'Standby carrier content differs from the current bootstrap-lite.md'))
|
|
193
193
|
if (!checks.homeLink) issues.push(buildDoctorIssue('standby-link-missing', 'standby home 链接缺失或未指向当前包根目录', 'Standby home link is missing or points to a different package root'))
|
|
194
194
|
if (!checks.settingsHooks) issues.push(buildDoctorIssue('standby-hooks-missing', 'standby settings hooks 缺失', 'Standby settings hooks are missing'))
|
|
195
195
|
if (checks.settingsHooks && !checks.settingsHooksMatch) issues.push(buildDoctorIssue('standby-hooks-drift', 'standby settings hooks 与当前 hooks 配置不一致', 'Standby settings hooks differ from the current hook configuration'))
|
|
@@ -207,7 +207,7 @@ function inspectGeminiDoctor(settings) {
|
|
|
207
207
|
issues.push(buildDoctorIssue('untracked-managed-state', '检测到受管状态,但配置中未记录该 CLI 模式', 'Managed state detected but this CLI mode is not tracked in config'))
|
|
208
208
|
}
|
|
209
209
|
if (trackedMode !== 'none' && detectedMode === 'none' && trackedMode !== 'global') {
|
|
210
|
-
issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI
|
|
210
|
+
issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI 已安装,但未检测到对应的受管文件或配置', 'Config says this CLI is installed, but no managed artifacts were detected'))
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
const status = summarizeDoctorStatus(issues, { host, trackedMode, detectedMode })
|
|
@@ -215,20 +215,20 @@ function inspectGeminiDoctor(settings) {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
function appendCodexStandbyIssues(issues, checks) {
|
|
218
|
-
if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby
|
|
219
|
-
if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby
|
|
218
|
+
if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby 规则文件缺少 HELLOAGENTS 标记', 'Standby carrier is missing the HELLOAGENTS marker'))
|
|
219
|
+
if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby 规则文件内容与当前 bootstrap-lite.md 不一致', 'Standby carrier content differs from the current bootstrap-lite.md'))
|
|
220
220
|
if (!checks.homeLink) issues.push(buildDoctorIssue('standby-link-missing', 'standby home 链接缺失或未指向当前包根目录', 'Standby home link is missing or points to a different package root'))
|
|
221
|
-
if (checks.modelInstructionsFile) issues.push(buildDoctorIssue('standby-model-instructions-
|
|
221
|
+
if (!checks.modelInstructionsFile) issues.push(buildDoctorIssue('standby-model-instructions-missing', 'standby config 缺少受管 model_instructions_file', 'Standby config is missing the managed model_instructions_file'))
|
|
222
|
+
if (checks.modelInstructionsFile && !checks.modelInstructionsPathMatch) issues.push(buildDoctorIssue('standby-model-instructions-drift', 'standby model_instructions_file 未指向受管 `~/.codex/AGENTS.md`', 'Standby model_instructions_file does not point to the managed `~/.codex/AGENTS.md`'))
|
|
222
223
|
if (!checks.codexNotify) issues.push(buildDoctorIssue('standby-notify-missing', 'standby notify 配置缺失', 'Standby notify configuration is missing'))
|
|
223
224
|
if (checks.codexNotify && !checks.notifyPathMatch) issues.push(buildDoctorIssue('standby-notify-drift', 'standby notify 路径未指向当前包根目录', 'Standby notify path does not point to the current package root'))
|
|
224
|
-
if (!checks.developerInstructions) issues.push(buildDoctorIssue('standby-developer-instructions-missing', 'standby developer_instructions 缺失', 'Standby developer_instructions block is missing'))
|
|
225
225
|
if (checks.pluginRoot || checks.pluginCache || checks.marketplaceEntry || checks.pluginEnabled || checks.globalNotifyPath) {
|
|
226
|
-
issues.push(buildDoctorIssue('standby-global-residue', 'standby 模式下仍残留 global
|
|
226
|
+
issues.push(buildDoctorIssue('standby-global-residue', 'standby 模式下仍残留 global 插件文件或配置', 'Global plugin artifacts still remain while Codex is in standby mode'))
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
function appendCodexGlobalIssues(issues, checks, pluginVersion, cacheVersion) {
|
|
231
|
-
if (!checks.carrierMarker) issues.push(buildDoctorIssue('global-home-carrier-missing', 'global `~/.codex/AGENTS.md` 缺少 HelloAGENTS
|
|
231
|
+
if (!checks.carrierMarker) issues.push(buildDoctorIssue('global-home-carrier-missing', 'global `~/.codex/AGENTS.md` 缺少 HelloAGENTS 规则内容', 'Global `~/.codex/AGENTS.md` is missing the HelloAGENTS carrier'))
|
|
232
232
|
if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('global-home-carrier-drift', 'global `~/.codex/AGENTS.md` 与当前 bootstrap.md 不一致', 'Global `~/.codex/AGENTS.md` differs from the current bootstrap.md'))
|
|
233
233
|
if (!checks.pluginRoot) issues.push(buildDoctorIssue('global-plugin-root-missing', 'global 插件根目录缺失', 'Global plugin root is missing'))
|
|
234
234
|
if (!checks.pluginCache) issues.push(buildDoctorIssue('global-plugin-cache-missing', 'global 插件缓存目录缺失', 'Global plugin cache directory is missing'))
|
|
@@ -236,10 +236,10 @@ function appendCodexGlobalIssues(issues, checks, pluginVersion, cacheVersion) {
|
|
|
236
236
|
if (checks.pluginCache && !checks.pluginCacheCarrierMatch) issues.push(buildDoctorIssue('global-plugin-cache-carrier-drift', 'global 插件缓存中的 AGENTS.md 与当前 bootstrap.md 不一致', 'Global plugin cache AGENTS.md differs from the current bootstrap.md'))
|
|
237
237
|
if (!checks.marketplaceEntry) issues.push(buildDoctorIssue('global-marketplace-missing', 'global marketplace 条目缺失', 'Global marketplace entry is missing'))
|
|
238
238
|
if (!checks.pluginEnabled) issues.push(buildDoctorIssue('global-plugin-disabled', 'global config 中缺少插件启用段', 'Global plugin enablement block is missing from config'))
|
|
239
|
-
if (checks.modelInstructionsFile) issues.push(buildDoctorIssue('global-model-instructions-
|
|
239
|
+
if (!checks.modelInstructionsFile) issues.push(buildDoctorIssue('global-model-instructions-missing', 'global config 缺少受管 model_instructions_file', 'Global config is missing the managed model_instructions_file'))
|
|
240
|
+
if (checks.modelInstructionsFile && !checks.modelInstructionsPathMatch) issues.push(buildDoctorIssue('global-model-instructions-drift', 'global model_instructions_file 未指向受管 `~/.codex/AGENTS.md`', 'Global model_instructions_file does not point to the managed `~/.codex/AGENTS.md`'))
|
|
240
241
|
if (!checks.globalNotifyPath) issues.push(buildDoctorIssue('global-notify-missing', 'global notify 路径缺失', 'Global notify path is missing'))
|
|
241
242
|
if (checks.globalNotifyPath && !checks.globalNotifyPathMatch) issues.push(buildDoctorIssue('global-notify-drift', 'global notify 路径未指向当前插件根目录', 'Global notify path does not point to the current plugin root'))
|
|
242
|
-
if (!checks.developerInstructions) issues.push(buildDoctorIssue('global-developer-instructions-missing', 'global developer_instructions 缺失', 'Global developer_instructions block is missing'))
|
|
243
243
|
if (pluginVersion && !checks.pluginVersionMatch) issues.push(buildDoctorIssue('global-plugin-version-drift', 'global 插件根目录版本与当前包版本不一致', 'Global plugin root version does not match the current package version'))
|
|
244
244
|
if (cacheVersion && !checks.pluginCacheVersionMatch) issues.push(buildDoctorIssue('global-plugin-cache-version-drift', 'global 插件缓存版本与当前包版本不一致', 'Global plugin cache version does not match the current package version'))
|
|
245
245
|
if (checks.homeLink) {
|
|
@@ -260,6 +260,8 @@ function inspectCodexDoctor(settings) {
|
|
|
260
260
|
const cacheVersion = safeJson(join(pluginCacheRoot, 'package.json'))?.version || ''
|
|
261
261
|
const standbyNotifyPath = normalizePath(join(runtime.pkgRoot, 'scripts', 'notify.mjs'))
|
|
262
262
|
const globalNotifyPath = normalizePath(join(pluginRoot, 'scripts', 'notify.mjs'))
|
|
263
|
+
const managedHomeCarrierPath = normalizePath(join(codexDir, 'AGENTS.md'))
|
|
264
|
+
const modelInstructionsLine = readTopLevelTomlLine(codexConfig, 'model_instructions_file')
|
|
263
265
|
const expectedHomeCarrier = (detectedMode === 'global' || (detectedMode === 'none' && trackedMode === 'global'))
|
|
264
266
|
? 'bootstrap.md'
|
|
265
267
|
: 'bootstrap-lite.md'
|
|
@@ -267,10 +269,11 @@ function inspectCodexDoctor(settings) {
|
|
|
267
269
|
carrierMarker: (safeRead(join(codexDir, 'AGENTS.md')) || '').includes('HELLOAGENTS_START'),
|
|
268
270
|
carrierContentMatch: extractManagedCarrierContent(join(codexDir, 'AGENTS.md')) === readBootstrapContent(expectedHomeCarrier),
|
|
269
271
|
homeLink: safeRealTarget(join(codexDir, 'helloagents')) === runtime.pkgRoot,
|
|
270
|
-
modelInstructionsFile: !!
|
|
272
|
+
modelInstructionsFile: !!modelInstructionsLine,
|
|
273
|
+
modelInstructionsPathMatch: !!modelInstructionsLine
|
|
274
|
+
&& normalizePath(modelInstructionsLine).includes(`"${managedHomeCarrierPath}"`),
|
|
271
275
|
codexNotify: codexConfig.includes('codex-notify'),
|
|
272
276
|
notifyPathMatch: codexConfig.includes(standbyNotifyPath),
|
|
273
|
-
developerInstructions: codexConfig.includes('HelloAGENTS'),
|
|
274
277
|
pluginRoot: existsSync(pluginRoot),
|
|
275
278
|
pluginCache: existsSync(pluginCacheRoot),
|
|
276
279
|
pluginCarrierMatch: normalizeText(safeRead(join(pluginRoot, 'AGENTS.md')) || '') === readBootstrapContent('bootstrap.md'),
|
|
@@ -298,7 +301,7 @@ function inspectCodexDoctor(settings) {
|
|
|
298
301
|
issues.push(buildDoctorIssue('untracked-managed-state', '检测到受管状态,但配置中未记录该 CLI 模式', 'Managed state detected but this CLI mode is not tracked in config'))
|
|
299
302
|
}
|
|
300
303
|
if (trackedMode !== 'none' && detectedMode === 'none') {
|
|
301
|
-
issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI
|
|
304
|
+
issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI 已安装,但未检测到对应的受管文件或配置', 'Config says this CLI is installed, but no managed artifacts were detected'))
|
|
302
305
|
}
|
|
303
306
|
if (!checks.pluginVersionMatch && !pluginVersion && detectedMode === 'global') {
|
|
304
307
|
notes.push(runtime.msg('未读到 global 插件根目录版本信息', 'Global plugin root version was not readable'))
|
package/scripts/cli-messages.mjs
CHANGED
|
@@ -92,7 +92,7 @@ ${msg('单 CLI 管理', 'Scoped CLI management')}:
|
|
|
92
92
|
${msg('诊断', 'Diagnostics')}:
|
|
93
93
|
helloagents doctor
|
|
94
94
|
helloagents doctor codex --json
|
|
95
|
-
${msg('检查 carrier、链接、hooks、配置注入、Codex
|
|
95
|
+
${msg('检查 carrier、链接、hooks、配置注入、Codex 插件链路、受管 model_instructions_file 指向与版本漂移', 'Checks carriers, links, hooks, config injections, the Codex plugin chain, managed model_instructions_file targeting, and version drift')}
|
|
96
96
|
|
|
97
97
|
${msg('卸载', 'Uninstall')}:
|
|
98
98
|
helloagents cleanup ${msg('(推荐先执行,显式清理所有 CLI 注入/链接)', '(recommended first, explicitly cleans CLI injections/links)')}
|
package/scripts/cli-toml.mjs
CHANGED
|
@@ -33,6 +33,15 @@ function findFirstTomlSectionIndex(text) {
|
|
|
33
33
|
return normalized.length;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function splitTopLevelToml(text) {
|
|
37
|
+
const normalized = String(text || '').replace(/\r\n/g, '\n');
|
|
38
|
+
const topLevelEnd = findFirstTomlSectionIndex(normalized);
|
|
39
|
+
return {
|
|
40
|
+
topLevel: normalized.slice(0, topLevelEnd),
|
|
41
|
+
sections: normalized.slice(topLevelEnd),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
function findTopLevelTomlBlock(text, key) {
|
|
37
46
|
const normalized = String(text || '').replace(/\r\n/g, '\n');
|
|
38
47
|
const topLevelEnd = findFirstTomlSectionIndex(normalized);
|
|
@@ -93,6 +102,27 @@ export function removeTopLevelTomlBlock(text, key) {
|
|
|
93
102
|
return normalizeToml(`${normalized.slice(0, existing.start)}${normalized.slice(existing.end)}`);
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
export function prependTopLevelTomlBlocks(text, blocks) {
|
|
106
|
+
const normalizedBlocks = blocks
|
|
107
|
+
.map((block) => String(block || '').trim())
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
|
|
110
|
+
const { topLevel, sections } = splitTopLevelToml(text);
|
|
111
|
+
const normalizedTopLevel = topLevel.replace(/^\n+/, '').trimEnd();
|
|
112
|
+
const normalizedSections = sections.replace(/^\n+/, '').trimEnd();
|
|
113
|
+
const remainder = normalizedTopLevel && normalizedSections
|
|
114
|
+
? `${normalizedTopLevel}\n\n${normalizedSections}`
|
|
115
|
+
: normalizedTopLevel || normalizedSections;
|
|
116
|
+
if (!normalizedBlocks.length) return normalizeToml(remainder);
|
|
117
|
+
const managedPrelude = normalizedBlocks.join('\n');
|
|
118
|
+
|
|
119
|
+
return normalizeToml(
|
|
120
|
+
remainder
|
|
121
|
+
? `${managedPrelude}\n\n${remainder}`
|
|
122
|
+
: managedPrelude,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
96
126
|
export function upsertTopLevelTomlKey(text, key, value) {
|
|
97
127
|
const re = new RegExp(`^${key}\\s*=.*$`, 'm');
|
|
98
128
|
const next = re.test(text)
|
package/scripts/guard-rules.mjs
CHANGED
|
@@ -6,10 +6,12 @@ export const DANGEROUS_PATTERNS = [
|
|
|
6
6
|
{ pattern: /(sudo\s+)?rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?(\/|~|\*)/, reason: 'Recursive delete of critical path' },
|
|
7
7
|
{ pattern: /(sudo\s+)?rm\s+--recursive/, reason: 'Recursive delete (long option)' },
|
|
8
8
|
{ pattern: /(sudo\s+)?rm\s+-[a-zA-Z]*r[a-zA-Z]*\s+\.\.?(\s|$)/, reason: 'Recursive delete of current/parent directory' },
|
|
9
|
+
{ pattern: /\bcmd(?:\.exe)?\s*\/c\b/i, reason: 'Nested cmd invocation bypasses PowerShell safety rules' },
|
|
10
|
+
{ pattern: /\bStart-Process\s+cmd(?:\.exe)?\b/i, reason: 'Nested cmd invocation bypasses PowerShell safety rules' },
|
|
9
11
|
{ pattern: /git\s+push\s+(-f|--force)/, reason: 'Force push (specify branch explicitly)' },
|
|
10
12
|
{ pattern: /git\s+reset\s+--hard/, reason: 'Hard reset (destructive operation)' },
|
|
11
13
|
{ pattern: /DROP\s+(DATABASE|TABLE|SCHEMA)/i, reason: 'Database destruction command' },
|
|
12
|
-
{ pattern:
|
|
14
|
+
{ pattern: /\bTRUNCATE(?:\s+TABLE)?\b/i, reason: 'Table truncation' },
|
|
13
15
|
{ pattern: /chmod\s+777/, reason: 'World-writable permissions' },
|
|
14
16
|
{ pattern: /mkfs\b/, reason: 'Filesystem format command' },
|
|
15
17
|
{ pattern: /dd\s+.*of=\/dev\//, reason: 'Direct device write' },
|
|
@@ -68,6 +70,29 @@ export function scanHighRiskCommands(command) {
|
|
|
68
70
|
return warnings
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
export function scanShellSafetyWarnings(command = '') {
|
|
74
|
+
const warnings = []
|
|
75
|
+
const normalized = String(command || '')
|
|
76
|
+
|
|
77
|
+
if (/\bpowershell(?:\.exe)?\b/i.test(normalized) && /\s-Command\b/i.test(normalized)) {
|
|
78
|
+
const inlineScript = normalized.split(/\s-Command\b/i).slice(1).join(' ').trim()
|
|
79
|
+
const logicalLines = inlineScript
|
|
80
|
+
.split(/[;\r\n]+/)
|
|
81
|
+
.map((entry) => entry.trim())
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
if (logicalLines.length > 3) {
|
|
84
|
+
warnings.push('PowerShell inline script exceeds 3 logical lines; prefer a temporary .ps1 file')
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const fileOps = normalized.match(/\b(remove-item|move-item|copy-item|new-item|set-content|add-content|out-file|mkdir|md|touch|cp|copy|mv|move|ren|rename|del|erase|rm|rmdir)\b/ig) || []
|
|
89
|
+
if (fileOps.length > 1 && /[;\r\n]/.test(normalized)) {
|
|
90
|
+
warnings.push('Multiple file operations are chained in one shell command; split them into separate commands')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return warnings
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
export function scanUnrequestedFiles(filePath, toolName) {
|
|
72
97
|
if (!filePath || toolName?.toLowerCase() !== 'write') return []
|
|
73
98
|
const basename = filePath.split(/[/\\]/).pop() || ''
|
package/scripts/guard.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
scanEnvCoverage,
|
|
19
19
|
scanForSecrets,
|
|
20
20
|
scanHighRiskCommands,
|
|
21
|
+
scanShellSafetyWarnings,
|
|
21
22
|
scanUnrequestedFiles,
|
|
22
23
|
} from './guard-rules.mjs'
|
|
23
24
|
|
|
@@ -122,7 +123,7 @@ function buildPostWriteWarnings(data) {
|
|
|
122
123
|
const filePath = data.tool_input?.file_path || ''
|
|
123
124
|
return [
|
|
124
125
|
...(detectIdeaBoundaryContext(data)?.zeroSideEffect
|
|
125
|
-
? ['~idea
|
|
126
|
+
? ['~idea 本轮要求只读探索;检测到写入工具落地,请回退到探索输出或升级到 ~plan / ~build / ~prd / ~auto 后再修改文件']
|
|
126
127
|
: []),
|
|
127
128
|
...scanUnrequestedFiles(filePath, data.tool_name),
|
|
128
129
|
...(content ? [...scanForSecrets(content), ...scanDangerousPackages(content, filePath)] : []),
|
|
@@ -168,7 +169,7 @@ function handleDangerousCommand(data, command) {
|
|
|
168
169
|
|
|
169
170
|
function handleHighRiskCommand(data, command) {
|
|
170
171
|
const warnings = scanHighRiskCommands(command)
|
|
171
|
-
if (warnings.length === 0) return
|
|
172
|
+
if (warnings.length === 0) return []
|
|
172
173
|
|
|
173
174
|
const cwd = data.cwd || process.cwd()
|
|
174
175
|
const gate = buildHighRiskGate(warnings, cwd)
|
|
@@ -185,20 +186,43 @@ function handleHighRiskCommand(data, command) {
|
|
|
185
186
|
guardType: 'high-risk-gate',
|
|
186
187
|
matches: warnings.map((warning) => warning.reason),
|
|
187
188
|
})
|
|
188
|
-
return
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
return warnings.map((warning) => warning.reason)
|
|
192
|
+
}
|
|
193
|
+
|
|
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请确认已完成相应规划/审查并获得必要授权。`)
|
|
198
|
+
}
|
|
199
|
+
if (shellSafetyWarnings.length > 0) {
|
|
200
|
+
sections.push(`⚠️ [HelloAGENTS Shell 安全提醒] 检测到建议调整的命令写法:\n${shellSafetyWarnings.map((warning) => ` - ${warning}`).join('\n')}\n当前仅提示,不中断执行。`)
|
|
189
201
|
}
|
|
202
|
+
if (sections.length === 0) return
|
|
190
203
|
|
|
191
204
|
emitHookPayload({
|
|
192
205
|
hookSpecificOutput: {
|
|
193
206
|
hookEventName: HOOK_EVENT,
|
|
194
|
-
additionalContext:
|
|
207
|
+
additionalContext: sections.join('\n\n'),
|
|
195
208
|
},
|
|
196
209
|
})
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
210
|
+
|
|
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
|
+
})
|
|
225
|
+
}
|
|
202
226
|
}
|
|
203
227
|
|
|
204
228
|
function handleShellCommand(data) {
|
|
@@ -217,7 +241,11 @@ function handleShellCommand(data) {
|
|
|
217
241
|
}
|
|
218
242
|
|
|
219
243
|
if (handleDangerousCommand(data, command)) return
|
|
220
|
-
handleHighRiskCommand(data, command)
|
|
244
|
+
const highRiskWarnings = handleHighRiskCommand(data, command)
|
|
245
|
+
if (highRiskWarnings === null) return
|
|
246
|
+
|
|
247
|
+
const shellSafetyWarnings = scanShellSafetyWarnings(command)
|
|
248
|
+
emitShellWarnings(data, command, highRiskWarnings, shellSafetyWarnings)
|
|
221
249
|
}
|
|
222
250
|
|
|
223
251
|
async function main() {
|
|
@@ -31,11 +31,6 @@ function resolveStandbyHostRoot(host) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function resolveReadRoot({ cwd, pkgRoot, host, settings }) {
|
|
34
|
-
const projectRoot = join(cwd, 'skills', 'helloagents');
|
|
35
|
-
if (existsSync(projectRoot)) {
|
|
36
|
-
return { source: 'project', root: projectRoot };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
34
|
if (settings.install_mode === 'standby') {
|
|
40
35
|
const standbyRoot = resolveStandbyHostRoot(host);
|
|
41
36
|
if (standbyRoot && existsSync(standbyRoot)) {
|
|
@@ -48,7 +43,7 @@ function resolveReadRoot({ cwd, pkgRoot, host, settings }) {
|
|
|
48
43
|
|
|
49
44
|
function buildReadRootBlock(readRoot) {
|
|
50
45
|
if (!readRoot?.root) return '';
|
|
51
|
-
return `##
|
|
46
|
+
return `## 本轮 HelloAGENTS 读取根目录\n\`\`\`json\n${JSON.stringify(readRoot, null, 2)}\n\`\`\``;
|
|
52
47
|
}
|
|
53
48
|
|
|
54
49
|
export function resolveCanonicalCommandSkill(skillName) {
|
|
@@ -172,7 +167,7 @@ export function buildSemanticRouteInstruction(cwd) {
|
|
|
172
167
|
'当前消息未使用 ~command。',
|
|
173
168
|
'请根据用户请求的真实意图选路,不依赖关键词表。',
|
|
174
169
|
'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆链路。',
|
|
175
|
-
'路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto
|
|
170
|
+
'路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动编排并自动衔接后续阶段。',
|
|
176
171
|
'若判定为 T3,默认先走 ~plan / ~prd;纯审查/验证请求才优先 ~verify。',
|
|
177
172
|
`涉及 UI 任务时,设计决策优先级:当前活跃 plan / PRD → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → 通用 UI 规则。`,
|
|
178
173
|
projectStorageHint,
|
package/scripts/notify.mjs
CHANGED
|
@@ -10,11 +10,12 @@ import { homedir } from 'node:os';
|
|
|
10
10
|
import { playSound as _playSound, desktopNotify as _desktopNotify } from './notify-ui.mjs';
|
|
11
11
|
import { resolveNotificationSource } from './notify-source.mjs';
|
|
12
12
|
import { buildCompactionContext, buildInjectContext, buildRouteInstruction, buildSemanticRouteInstruction, resolveCanonicalCommandSkill } from './notify-context.mjs';
|
|
13
|
-
import { claimsTaskComplete, shouldIgnoreCodexNotifyClient
|
|
13
|
+
import { claimsTaskComplete, shouldIgnoreCodexNotifyClient } from './notify-events.mjs';
|
|
14
14
|
import { handleRouteCommand, resolveBootstrapFile } from './notify-route.mjs';
|
|
15
15
|
import { readSettings, readStdinJson, output, suppressedOutput, emptySuppress } from './notify-shared.mjs';
|
|
16
16
|
import { clearRouteContext, writeRouteContext } from './runtime-context.mjs';
|
|
17
17
|
import { appendReplayEvent, startReplaySession } from './replay-state.mjs';
|
|
18
|
+
import { clearTurnState, readTurnState } from './turn-state.mjs';
|
|
18
19
|
import { getWorkflowRecommendation } from './workflow-state.mjs';
|
|
19
20
|
|
|
20
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -114,6 +115,14 @@ function readCompletionText(payload = {}) {
|
|
|
114
115
|
|| '';
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
function shouldRunDeliveryGate(cwd, lastMsg) {
|
|
119
|
+
const turnState = readTurnState(cwd);
|
|
120
|
+
if (turnState?.role === 'main') {
|
|
121
|
+
return turnState.kind === 'complete';
|
|
122
|
+
}
|
|
123
|
+
return claimsTaskComplete(lastMsg);
|
|
124
|
+
}
|
|
125
|
+
|
|
117
126
|
function cmdPreCompact() {
|
|
118
127
|
const payload = readStdinJson();
|
|
119
128
|
const cwd = payload.cwd || process.cwd();
|
|
@@ -140,6 +149,7 @@ function cmdPreCompact() {
|
|
|
140
149
|
|
|
141
150
|
function cmdRoute() {
|
|
142
151
|
const payload = readStdinJson();
|
|
152
|
+
clearTurnState(payload.cwd || process.cwd());
|
|
143
153
|
handleRouteCommand({
|
|
144
154
|
payload,
|
|
145
155
|
host: HOST,
|
|
@@ -194,6 +204,7 @@ function cmdInject() {
|
|
|
194
204
|
cwd,
|
|
195
205
|
});
|
|
196
206
|
clearRouteContext();
|
|
207
|
+
clearTurnState(cwd);
|
|
197
208
|
suppressedOutput(EVENT_NAME.SessionStart, context || undefined);
|
|
198
209
|
}
|
|
199
210
|
|
|
@@ -207,7 +218,7 @@ function cmdStop() {
|
|
|
207
218
|
desktopNotify('warning', buildNotifyExtra(payload));
|
|
208
219
|
return;
|
|
209
220
|
}
|
|
210
|
-
if (
|
|
221
|
+
if (shouldRunDeliveryGate(cwd, lastMsg) && runDeliveryGate(payload)) {
|
|
211
222
|
playSound('warning');
|
|
212
223
|
desktopNotify('warning', buildNotifyExtra(payload));
|
|
213
224
|
return;
|
|
@@ -243,17 +254,17 @@ function cmdCodexNotify() {
|
|
|
243
254
|
}
|
|
244
255
|
if (type !== 'agent-turn-complete') return;
|
|
245
256
|
|
|
246
|
-
const lastMsg = data['last-assistant-message'] || '';
|
|
247
|
-
const settings = getSettings();
|
|
248
|
-
if (shouldIgnoreFormattedSubagent(lastMsg, settings.output_format !== false)) return;
|
|
249
|
-
|
|
250
257
|
const cwd = data.cwd || process.cwd();
|
|
251
|
-
|
|
258
|
+
const turnState = readTurnState(cwd);
|
|
259
|
+
if (!turnState || turnState.role !== 'main') return;
|
|
260
|
+
|
|
261
|
+
const settings = getSettings();
|
|
262
|
+
if (turnState.kind === 'complete' && runRalphLoop({ cwd })) {
|
|
252
263
|
playSound('warning');
|
|
253
264
|
desktopNotify('warning', buildNotifyExtra(data));
|
|
254
265
|
return;
|
|
255
266
|
}
|
|
256
|
-
if (
|
|
267
|
+
if (turnState.kind === 'complete' && runDeliveryGate({ cwd })) {
|
|
257
268
|
playSound('warning');
|
|
258
269
|
desktopNotify('warning', buildNotifyExtra(data));
|
|
259
270
|
return;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, normalize, resolve } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import { appendReplayEvent } from './replay-state.mjs'
|
|
7
|
+
|
|
8
|
+
const TURN_STATE_PATH = join(homedir(), '.helloagents', 'runtime', 'turn-state.json')
|
|
9
|
+
const TURN_STATE_TTL_MS = 30 * 60 * 1000
|
|
10
|
+
const VALID_KINDS = new Set(['complete', 'waiting', 'blocked', 'progress'])
|
|
11
|
+
const VALID_ROLES = new Set(['main', 'subagent'])
|
|
12
|
+
|
|
13
|
+
function normalizePath(filePath = '') {
|
|
14
|
+
return filePath ? normalize(resolve(filePath)) : ''
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureRuntimeDir() {
|
|
18
|
+
mkdirSync(dirname(TURN_STATE_PATH), { recursive: true })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readStore() {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(TURN_STATE_PATH, 'utf-8'))
|
|
24
|
+
} catch {
|
|
25
|
+
return {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeStore(store) {
|
|
30
|
+
const keys = Object.keys(store)
|
|
31
|
+
if (keys.length === 0) {
|
|
32
|
+
rmSync(TURN_STATE_PATH, { force: true })
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ensureRuntimeDir()
|
|
37
|
+
writeFileSync(TURN_STATE_PATH, `${JSON.stringify(store, null, 2)}\n`, 'utf-8')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getTurnStateKey(cwd = process.cwd()) {
|
|
41
|
+
return normalizePath(cwd)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeTurnState(input = {}) {
|
|
45
|
+
const kind = typeof input.kind === 'string' ? input.kind.trim().toLowerCase() : ''
|
|
46
|
+
const role = typeof input.role === 'string' ? input.role.trim().toLowerCase() : 'main'
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
kind: VALID_KINDS.has(kind) ? kind : '',
|
|
50
|
+
role: VALID_ROLES.has(role) ? role : 'main',
|
|
51
|
+
phase: typeof input.phase === 'string' ? input.phase.trim().toLowerCase() : '',
|
|
52
|
+
source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'manual',
|
|
53
|
+
requiresDeliveryGate: Boolean(input.requiresDeliveryGate),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pruneInvalidEntry(store, key) {
|
|
58
|
+
delete store[key]
|
|
59
|
+
writeStore(store)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function clearTurnState(cwd = process.cwd()) {
|
|
63
|
+
const key = getTurnStateKey(cwd)
|
|
64
|
+
if (!key) return false
|
|
65
|
+
const store = readStore()
|
|
66
|
+
if (!(key in store)) return false
|
|
67
|
+
delete store[key]
|
|
68
|
+
writeStore(store)
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function readTurnState(cwd = process.cwd(), { now = Date.now() } = {}) {
|
|
73
|
+
const key = getTurnStateKey(cwd)
|
|
74
|
+
if (!key) return null
|
|
75
|
+
|
|
76
|
+
const store = readStore()
|
|
77
|
+
const entry = store[key]
|
|
78
|
+
if (!entry?.cwd || !entry?.kind || !entry?.updatedAt) {
|
|
79
|
+
if (entry) pruneInvalidEntry(store, key)
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const updatedAt = Date.parse(entry.updatedAt)
|
|
84
|
+
if (!Number.isFinite(updatedAt) || (now - updatedAt > TURN_STATE_TTL_MS)) {
|
|
85
|
+
pruneInvalidEntry(store, key)
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const normalized = normalizeTurnState(entry)
|
|
90
|
+
if (!normalized.kind) {
|
|
91
|
+
pruneInvalidEntry(store, key)
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
cwd: normalizePath(entry.cwd),
|
|
97
|
+
updatedAt: entry.updatedAt,
|
|
98
|
+
...normalized,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function writeTurnState(cwd = process.cwd(), input = {}) {
|
|
103
|
+
const key = getTurnStateKey(cwd)
|
|
104
|
+
const normalized = normalizeTurnState(input)
|
|
105
|
+
if (!key || !normalized.kind) {
|
|
106
|
+
throw new Error('turn-state requires cwd and a valid kind')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const store = readStore()
|
|
110
|
+
const payload = {
|
|
111
|
+
cwd: key,
|
|
112
|
+
updatedAt: new Date().toISOString(),
|
|
113
|
+
...normalized,
|
|
114
|
+
}
|
|
115
|
+
store[key] = payload
|
|
116
|
+
writeStore(store)
|
|
117
|
+
|
|
118
|
+
appendReplayEvent(cwd, {
|
|
119
|
+
event: 'turn_state_written',
|
|
120
|
+
source: normalized.source,
|
|
121
|
+
details: {
|
|
122
|
+
kind: normalized.kind,
|
|
123
|
+
role: normalized.role,
|
|
124
|
+
phase: normalized.phase,
|
|
125
|
+
requiresDeliveryGate: normalized.requiresDeliveryGate,
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return payload
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function readStdinJson() {
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(readFileSync(0, 'utf-8'))
|
|
135
|
+
} catch {
|
|
136
|
+
return {}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function main() {
|
|
141
|
+
const command = process.argv[2] || ''
|
|
142
|
+
const input = readStdinJson()
|
|
143
|
+
const cwd = input.cwd || process.cwd()
|
|
144
|
+
|
|
145
|
+
if (command === 'write') {
|
|
146
|
+
const payload = writeTurnState(cwd, input)
|
|
147
|
+
process.stdout.write(JSON.stringify({
|
|
148
|
+
suppressOutput: true,
|
|
149
|
+
path: TURN_STATE_PATH,
|
|
150
|
+
payload,
|
|
151
|
+
}))
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (command === 'clear') {
|
|
156
|
+
process.stdout.write(JSON.stringify({
|
|
157
|
+
suppressOutput: true,
|
|
158
|
+
cleared: clearTurnState(cwd),
|
|
159
|
+
}))
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (command === 'read') {
|
|
164
|
+
process.stdout.write(JSON.stringify({
|
|
165
|
+
suppressOutput: true,
|
|
166
|
+
state: readTurnState(cwd),
|
|
167
|
+
}))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
172
|
+
main()
|
|
173
|
+
}
|
|
@@ -63,7 +63,7 @@ export function determineVerifyMode(plan) {
|
|
|
63
63
|
if (plan.contractIssues.length > 0) {
|
|
64
64
|
return {
|
|
65
65
|
mode: 'metadata-first',
|
|
66
|
-
reason: '
|
|
66
|
+
reason: '方案包缺少可信的结构化契约',
|
|
67
67
|
guidance: '验证分流:当前还不适合直接进入 reviewer / tester;先回到 ~plan / ~prd 补齐 `contract.json`,明确 `verifyMode`、`reviewerFocus` 和 `testerFocus`。',
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -73,7 +73,7 @@ export function determineVerifyMode(plan) {
|
|
|
73
73
|
const testerFocus = plan.contract.testerFocus.join(';')
|
|
74
74
|
return {
|
|
75
75
|
mode: 'review-first',
|
|
76
|
-
reason: '
|
|
76
|
+
reason: '方案契约已明确要求审查优先',
|
|
77
77
|
guidance: `验证分流:当前更适合审查优先;先执行 reviewer / hello-review 范围审查,再交给 tester / hello-verify 跑完整验证。${reviewerFocus ? ` reviewer 重点:${reviewerFocus}。` : ''}${testerFocus ? ` tester 重点:${testerFocus}。` : ''}`.trim(),
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -88,7 +88,7 @@ export function determineVerifyMode(plan) {
|
|
|
88
88
|
|
|
89
89
|
return {
|
|
90
90
|
mode: 'test-first',
|
|
91
|
-
reason: '
|
|
91
|
+
reason: '方案契约已明确验证主路径,且任务契约已完整',
|
|
92
92
|
guidance: `验证分流:当前更适合测试优先;先执行 tester / hello-verify 跑完整验证,再针对失败点或关键边界补充 hello-review。${plan.contract?.testerFocus?.length ? ` tester 重点:${plan.contract.testerFocus.join(';')}。` : ''}`.trim(),
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -137,7 +137,7 @@ export function buildStateSyncHintFromSnapshot(snapshot) {
|
|
|
137
137
|
|
|
138
138
|
export function buildStateRoleHintFromSnapshot(snapshot) {
|
|
139
139
|
if (!snapshot.state.exists || snapshot.plans.length > 0) return ''
|
|
140
|
-
return '恢复约束:当前仅检测到 `.helloagents/STATE.md`;先以当前用户消息、显式命令和代码事实确认主线,STATE.md
|
|
140
|
+
return '恢复约束:当前仅检测到 `.helloagents/STATE.md`;先以当前用户消息、显式命令和代码事实确认主线,STATE.md 只用于找回上次停在哪,不是当前任务的自动授权或唯一判断依据。'
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
export function buildUiContractHint(cwd, snapshot) {
|
|
@@ -154,10 +154,10 @@ export function buildUiContractHint(cwd, snapshot) {
|
|
|
154
154
|
|
|
155
155
|
const extraHints = []
|
|
156
156
|
if (styleAdvisorRequired) {
|
|
157
|
-
extraHints.push('若当前 UI
|
|
157
|
+
extraHints.push('若当前 UI 契约要求 style advisor,收尾前需复用 `.helloagents/.ralph-advisor.json` 留下独立复查证据')
|
|
158
158
|
}
|
|
159
159
|
if (visualValidationRequired) {
|
|
160
|
-
extraHints.push('若当前 UI
|
|
160
|
+
extraHints.push('若当前 UI 契约要求视觉验收,收尾前需写 `.helloagents/.ralph-visual.json` 记录关键视口、状态与结论')
|
|
161
161
|
}
|
|
162
162
|
return `UI 约束提示:如本次属于视觉/交互链路,设计决策优先级固定为:当前活跃 plan.md / prd/03-ui-design.md → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → hello-ui。${extraHints.length > 0 ? ` ${extraHints.join(';')}。` : ''}`
|
|
163
163
|
}
|