helloagents 3.0.9-beta.1 → 3.0.10-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 +16 -7
- package/README_CN.md +41 -32
- package/bootstrap-lite.md +18 -18
- package/bootstrap.md +22 -22
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/scripts/cli-codex-config.mjs +8 -2
- package/scripts/cli-codex.mjs +5 -3
- package/scripts/cli-lifecycle.mjs +11 -0
- package/scripts/cli-messages.mjs +3 -3
- package/scripts/cli-toml-values.mjs +25 -0
- package/scripts/cli-toml.mjs +15 -0
- package/scripts/guard.mjs +2 -2
- package/scripts/notify-context.mjs +7 -7
- package/scripts/notify-events.mjs +0 -8
- package/scripts/notify-gates.mjs +128 -0
- package/scripts/notify-ui.mjs +3 -0
- package/scripts/notify.mjs +44 -76
- package/scripts/project-storage.mjs +5 -5
- package/scripts/workflow-core.mjs +5 -5
- package/scripts/workflow-recommendation.mjs +4 -4
- package/scripts/workflow-state.mjs +1 -1
- package/skills/commands/auto/SKILL.md +13 -13
- package/skills/commands/build/SKILL.md +5 -5
- package/skills/commands/clean/SKILL.md +6 -6
- package/skills/commands/commit/SKILL.md +2 -2
- package/skills/commands/help/SKILL.md +2 -2
- package/skills/commands/idea/SKILL.md +5 -5
- package/skills/commands/init/SKILL.md +4 -4
- package/skills/commands/loop/SKILL.md +4 -4
- package/skills/commands/plan/SKILL.md +13 -13
- package/skills/commands/prd/SKILL.md +13 -13
- package/skills/commands/verify/SKILL.md +3 -3
- package/skills/commands/wiki/SKILL.md +5 -5
- package/skills/hello-subagent/SKILL.md +1 -1
- package/skills/hello-ui/SKILL.md +2 -2
- package/skills/helloagents/SKILL.md +3 -2
- package/templates/plans/contract.json +2 -2
|
@@ -181,6 +181,17 @@ function runAllHostsLifecycle(action, explicitMode) {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
const settings = readSettings(true)
|
|
184
|
+
if (action === 'update' && !explicitMode) {
|
|
185
|
+
for (const host of HOSTS) {
|
|
186
|
+
const mode = resolveHostMode(host, '', settings)
|
|
187
|
+
const result = runHostLifecycle(runtime, action, host, mode)
|
|
188
|
+
if (!result.skipped) setTrackedHostMode(settings, host, mode)
|
|
189
|
+
}
|
|
190
|
+
writeSettings(settings)
|
|
191
|
+
runtime.printInstallMsg(settings.install_mode || DEFAULTS.install_mode, 'refresh')
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
184
195
|
const mode = resolveInstallMode(explicitMode, settings)
|
|
185
196
|
if (explicitMode) settings.install_mode = explicitMode
|
|
186
197
|
installAllHosts(runtime, mode)
|
package/scripts/cli-messages.mjs
CHANGED
|
@@ -44,8 +44,8 @@ function renderInstallMessage(context, mode, state) {
|
|
|
44
44
|
}
|
|
45
45
|
return msg(
|
|
46
46
|
refresh
|
|
47
|
-
? ' global 模式已刷新。\n Claude Code / Gemini 请保持插件已安装;Codex
|
|
48
|
-
: ' 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 请手动安装插件;Codex
|
|
47
|
+
? ' global 模式已刷新。\n Claude Code / Gemini 请保持插件已安装;Codex 原生本地插件已重装并同步最新文件。'
|
|
48
|
+
: ' 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 请手动安装插件;Codex 已自动安装原生本地插件。',
|
|
49
49
|
refresh
|
|
50
50
|
? ' Global mode refreshed.\n Keep Claude Code / Gemini plugins installed; Codex native local-plugin files were reinstalled and synced.'
|
|
51
51
|
: ' All projects will use full HelloAGENTS rules.\n Install Claude Code / Gemini plugins manually; Codex now uses the native local-plugin path automatically.',
|
|
@@ -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, Codex plugin installation, 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)')}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function getTomlArrayDepthDelta(text) {
|
|
2
|
+
let depth = 0
|
|
3
|
+
let quoted = false
|
|
4
|
+
let escaped = false
|
|
5
|
+
|
|
6
|
+
for (const char of String(text || '')) {
|
|
7
|
+
if (escaped) {
|
|
8
|
+
escaped = false
|
|
9
|
+
continue
|
|
10
|
+
}
|
|
11
|
+
if (char === '\\' && quoted) {
|
|
12
|
+
escaped = true
|
|
13
|
+
continue
|
|
14
|
+
}
|
|
15
|
+
if (char === '"') {
|
|
16
|
+
quoted = !quoted
|
|
17
|
+
continue
|
|
18
|
+
}
|
|
19
|
+
if (quoted) continue
|
|
20
|
+
if (char === '[') depth += 1
|
|
21
|
+
if (char === ']') depth -= 1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return depth
|
|
25
|
+
}
|
package/scripts/cli-toml.mjs
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Targets the small subset of TOML structures used by Codex CLI config.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { getTomlArrayDepthDelta } from './cli-toml-values.mjs'
|
|
7
|
+
|
|
6
8
|
export function isTomlTableHeader(line) {
|
|
7
9
|
const trimmed = String(line || '').trim();
|
|
8
10
|
return trimmed.startsWith('[') && trimmed.endsWith(']');
|
|
@@ -62,6 +64,19 @@ function findTopLevelTomlBlock(text, key) {
|
|
|
62
64
|
const closeIndex = normalized.indexOf('"""', openIndex + 3);
|
|
63
65
|
end = closeIndex >= 0 ? closeIndex + 3 : normalized.length;
|
|
64
66
|
}
|
|
67
|
+
if (value.startsWith('[')) {
|
|
68
|
+
let depth = getTomlArrayDepthDelta(firstLine.slice(firstLine.indexOf('=') + 1));
|
|
69
|
+
let lineStart = firstLineEnd + (normalized[firstLineEnd] === '\n' ? 1 : 0);
|
|
70
|
+
|
|
71
|
+
while (depth > 0 && lineStart < normalized.length) {
|
|
72
|
+
const lineEndIndex = normalized.indexOf('\n', lineStart);
|
|
73
|
+
const nextLineEnd = lineEndIndex >= 0 ? lineEndIndex : normalized.length;
|
|
74
|
+
const nextLine = normalized.slice(lineStart, nextLineEnd);
|
|
75
|
+
depth += getTomlArrayDepthDelta(nextLine);
|
|
76
|
+
end = nextLineEnd;
|
|
77
|
+
lineStart = nextLineEnd + 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
65
80
|
|
|
66
81
|
while (end < normalized.length && normalized[end] === '\n') {
|
|
67
82
|
end += 1;
|
package/scripts/guard.mjs
CHANGED
|
@@ -124,7 +124,7 @@ function buildPostWriteWarnings(data) {
|
|
|
124
124
|
const filePath = data.tool_input?.file_path || ''
|
|
125
125
|
return [
|
|
126
126
|
...(detectIdeaBoundaryContext(data)?.zeroSideEffect
|
|
127
|
-
? ['~idea
|
|
127
|
+
? ['~idea 本轮要求只读探索;检测到写入文件的工具调用,请回到探索输出,或升级到 ~plan / ~build / ~prd / ~auto 后再修改文件']
|
|
128
128
|
: []),
|
|
129
129
|
...scanUnrequestedFiles(filePath, data.tool_name),
|
|
130
130
|
...(content ? [...scanForSecrets(content), ...scanDangerousPackages(content, filePath)] : []),
|
|
@@ -195,7 +195,7 @@ function handleHighRiskCommand(data, command) {
|
|
|
195
195
|
function emitShellWarnings(data, command, highRiskWarnings, shellSafetyWarnings) {
|
|
196
196
|
const sections = []
|
|
197
197
|
if (highRiskWarnings.length > 0) {
|
|
198
|
-
sections.push(`⚠️ [HelloAGENTS
|
|
198
|
+
sections.push(`⚠️ [HelloAGENTS 高风险操作提醒] 检测到高风险命令:\n${highRiskWarnings.map((warning) => ` - ${warning}`).join('\n')}\n请确认已完成相应规划/审查并获得必要授权。`)
|
|
199
199
|
}
|
|
200
200
|
if (shellSafetyWarnings.length > 0) {
|
|
201
201
|
sections.push(`⚠️ [HelloAGENTS Shell 安全提醒] 检测到建议调整的命令写法:\n${shellSafetyWarnings.map((warning) => ` - ${warning}`).join('\n')}\n当前仅提示,不中断执行。`)
|
|
@@ -74,8 +74,8 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
|
|
|
74
74
|
const stateSyncHint = buildStateSyncHint(cwd, workflowOptions);
|
|
75
75
|
if (stateSnapshot.exists && stateSnapshot.content) {
|
|
76
76
|
summaryParts.push('');
|
|
77
|
-
summaryParts.push(`##
|
|
78
|
-
summaryParts.push('
|
|
77
|
+
summaryParts.push(`## 状态文件(从 ${stateSnapshot.statePath.replace(/\\/g, '/')} 读取,只用于找回上次停在哪)`);
|
|
78
|
+
summaryParts.push('恢复时先看当前用户消息;如果仍是同一任务,再参考状态文件。');
|
|
79
79
|
summaryParts.push(stateSnapshot.content);
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -109,7 +109,7 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
|
|
|
109
109
|
|
|
110
110
|
if (stateSyncHint) {
|
|
111
111
|
summaryParts.push('');
|
|
112
|
-
summaryParts.push('##
|
|
112
|
+
summaryParts.push('## 状态文件提醒');
|
|
113
113
|
summaryParts.push(stateSyncHint);
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -140,10 +140,10 @@ export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host,
|
|
|
140
140
|
if (projectStorageBlock) context += `\n\n${projectStorageBlock}`;
|
|
141
141
|
if (workflowHint) context += `\n\n## 当前工作流提示\n${workflowHint}`;
|
|
142
142
|
if (capabilityHint) context += `\n\n## 当前按需能力\n${capabilityHint}`;
|
|
143
|
-
if (stateSyncHint) context += `\n\n##
|
|
143
|
+
if (stateSyncHint) context += `\n\n## 状态文件提醒\n${stateSyncHint}`;
|
|
144
144
|
context += settingsBlock;
|
|
145
145
|
if (source === 'resume' || source === 'compact') {
|
|
146
|
-
context += `\n\n> ⚠️
|
|
146
|
+
context += `\n\n> ⚠️ 会话已恢复/压缩,请先读取 \`state_path\` 指向的 \`${stateSnapshot.statePath.replace(/\\/g, '/')}\`;先看当前用户消息,如果仍是同一任务,再参考状态文件。`;
|
|
147
147
|
}
|
|
148
148
|
return context;
|
|
149
149
|
}
|
|
@@ -168,8 +168,8 @@ export function buildSemanticRouteInstruction(cwd, payload = {}) {
|
|
|
168
168
|
return [
|
|
169
169
|
'当前消息未使用 ~command。',
|
|
170
170
|
'请根据用户请求的真实意图选路,不依赖关键词表。',
|
|
171
|
-
'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3
|
|
172
|
-
'路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto
|
|
171
|
+
'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆操作。',
|
|
172
|
+
'路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动选择并继续执行后续阶段。',
|
|
173
173
|
'若判定为 T3,默认先走 ~plan / ~prd;纯审查/验证请求才优先 ~verify。',
|
|
174
174
|
`涉及 UI 任务时,设计决策优先级:当前活跃 plan / PRD → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → 通用 UI 规则。`,
|
|
175
175
|
projectStorageHint,
|
|
@@ -5,11 +5,3 @@ export function shouldIgnoreCodexNotifyClient(client) {
|
|
|
5
5
|
export function shouldIgnoreFormattedSubagent(lastMsg, outputFormatEnabled) {
|
|
6
6
|
return outputFormatEnabled && !lastMsg.includes('【HelloAGENTS】');
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
export function claimsTaskComplete(lastMsg) {
|
|
10
|
-
if (!lastMsg) return false;
|
|
11
|
-
if (/^✅【HelloAGENTS】- .*(当前任务已完成|任务已完成|已修复|完成交付|done|fixed|completed|finished)/im.test(lastMsg)) {
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
return /(当前任务已完成|任务已完成|已全部完成|已修复|修复完成|\b(done|fixed|completed|finished)\b)/i.test(lastMsg);
|
|
15
|
-
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
|
|
4
|
+
function truncateText(value = '') {
|
|
5
|
+
const text = String(value || '').trim()
|
|
6
|
+
return text.length > 1000 ? `${text.slice(0, 1000)}\n...(truncated)` : text
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function buildGateErrorReason(source, detail = '') {
|
|
10
|
+
return [
|
|
11
|
+
`[HelloAGENTS Runtime] ${source} 执行失败,已暂停完成通知。`,
|
|
12
|
+
detail ? `原因:${detail}` : '',
|
|
13
|
+
'请修复脚本或重新运行验证后再报告完成。',
|
|
14
|
+
].filter(Boolean).join('\n')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function emitGateError({
|
|
18
|
+
payload,
|
|
19
|
+
host,
|
|
20
|
+
source,
|
|
21
|
+
reason,
|
|
22
|
+
appendReplayEvent,
|
|
23
|
+
output,
|
|
24
|
+
}) {
|
|
25
|
+
appendReplayEvent(payload.cwd || process.cwd(), {
|
|
26
|
+
host,
|
|
27
|
+
event: 'runtime_gate_error',
|
|
28
|
+
source,
|
|
29
|
+
reason,
|
|
30
|
+
})
|
|
31
|
+
output({
|
|
32
|
+
decision: 'block',
|
|
33
|
+
reason,
|
|
34
|
+
suppressOutput: true,
|
|
35
|
+
})
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function runGateScript({
|
|
40
|
+
payload,
|
|
41
|
+
host,
|
|
42
|
+
scriptPath,
|
|
43
|
+
args = [],
|
|
44
|
+
source,
|
|
45
|
+
blockEvent,
|
|
46
|
+
timeout,
|
|
47
|
+
appendReplayEvent,
|
|
48
|
+
output,
|
|
49
|
+
}) {
|
|
50
|
+
if (!existsSync(scriptPath)) {
|
|
51
|
+
return emitGateError({
|
|
52
|
+
payload,
|
|
53
|
+
host,
|
|
54
|
+
source,
|
|
55
|
+
reason: buildGateErrorReason(source, `脚本不存在:${scriptPath}`),
|
|
56
|
+
appendReplayEvent,
|
|
57
|
+
output,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = spawnSync(process.execPath, [scriptPath, ...args], {
|
|
62
|
+
input: JSON.stringify(payload),
|
|
63
|
+
encoding: 'utf-8',
|
|
64
|
+
timeout,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (result.error) {
|
|
68
|
+
return emitGateError({
|
|
69
|
+
payload,
|
|
70
|
+
host,
|
|
71
|
+
source,
|
|
72
|
+
reason: buildGateErrorReason(source, result.error.message),
|
|
73
|
+
appendReplayEvent,
|
|
74
|
+
output,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (result.status !== 0) {
|
|
79
|
+
const detail = truncateText(`${result.stderr || ''}\n${result.stdout || ''}`) || `退出码 ${result.status}`
|
|
80
|
+
return emitGateError({
|
|
81
|
+
payload,
|
|
82
|
+
host,
|
|
83
|
+
source,
|
|
84
|
+
reason: buildGateErrorReason(source, detail),
|
|
85
|
+
appendReplayEvent,
|
|
86
|
+
output,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const stdout = String(result.stdout || '').trim()
|
|
91
|
+
if (!stdout) {
|
|
92
|
+
return emitGateError({
|
|
93
|
+
payload,
|
|
94
|
+
host,
|
|
95
|
+
source,
|
|
96
|
+
reason: buildGateErrorReason(source, '脚本未返回有效结果'),
|
|
97
|
+
appendReplayEvent,
|
|
98
|
+
output,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let gateOutput
|
|
103
|
+
try {
|
|
104
|
+
gateOutput = JSON.parse(stdout)
|
|
105
|
+
} catch {
|
|
106
|
+
return emitGateError({
|
|
107
|
+
payload,
|
|
108
|
+
host,
|
|
109
|
+
source,
|
|
110
|
+
reason: buildGateErrorReason(source, `脚本返回了无法解析的 JSON:${truncateText(stdout)}`),
|
|
111
|
+
appendReplayEvent,
|
|
112
|
+
output,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (gateOutput.decision === 'block') {
|
|
117
|
+
appendReplayEvent(payload.cwd || process.cwd(), {
|
|
118
|
+
host,
|
|
119
|
+
event: blockEvent,
|
|
120
|
+
source,
|
|
121
|
+
reason: gateOutput.reason || '',
|
|
122
|
+
})
|
|
123
|
+
output(gateOutput)
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return false
|
|
128
|
+
}
|
package/scripts/notify-ui.mjs
CHANGED
|
@@ -18,6 +18,7 @@ const NOTIFY_MESSAGES = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const WIN_APPID = 'HelloAgents.Notification';
|
|
21
|
+
const DISABLE_OS_NOTIFICATIONS = process.env.HELLOAGENTS_DISABLE_OS_NOTIFICATIONS === '1';
|
|
21
22
|
|
|
22
23
|
function escapeToastText(value = '') {
|
|
23
24
|
return String(value)
|
|
@@ -55,6 +56,7 @@ function resolveWav(pkgRoot, event) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export function playSound(pkgRoot, event) {
|
|
59
|
+
if (DISABLE_OS_NOTIFICATIONS) return;
|
|
58
60
|
const wav = resolveWav(pkgRoot, event);
|
|
59
61
|
if (!wav) { process.stderr.write('\x07'); return; }
|
|
60
62
|
try {
|
|
@@ -83,6 +85,7 @@ function ensureWinAppId(pkgRoot) {
|
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
export function desktopNotify(pkgRoot, event, extra) {
|
|
88
|
+
if (DISABLE_OS_NOTIFICATIONS) return;
|
|
86
89
|
const notification = buildDesktopNotificationContent(event, extra);
|
|
87
90
|
try {
|
|
88
91
|
if (PLAT === 'win32') {
|
package/scripts/notify.mjs
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
-
import { spawnSync } from 'node:child_process';
|
|
8
7
|
import { fileURLToPath } from 'node:url';
|
|
9
8
|
import { homedir } from 'node:os';
|
|
10
9
|
import { playSound as _playSound, desktopNotify as _desktopNotify } from './notify-ui.mjs';
|
|
11
10
|
import { resolveNotificationSource } from './notify-source.mjs';
|
|
12
11
|
import { buildCompactionContext, buildInjectContext, buildRouteInstruction, buildSemanticRouteInstruction, resolveCanonicalCommandSkill } from './notify-context.mjs';
|
|
13
|
-
import {
|
|
12
|
+
import { shouldIgnoreCodexNotifyClient } from './notify-events.mjs';
|
|
13
|
+
import { runGateScript } from './notify-gates.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';
|
|
@@ -37,6 +37,17 @@ const EVENT_NAME = {
|
|
|
37
37
|
const playSound = (event) => _playSound(PKG_ROOT, event);
|
|
38
38
|
const desktopNotify = (event, extra) => _desktopNotify(PKG_ROOT, event, extra);
|
|
39
39
|
|
|
40
|
+
function normalizeNotifyLevel(value) {
|
|
41
|
+
const level = Number(value);
|
|
42
|
+
return [0, 1, 2, 3].includes(level) ? level : 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function notifyByLevel(event, extra, settings = getSettings()) {
|
|
46
|
+
const level = normalizeNotifyLevel(settings.notify_level ?? 0);
|
|
47
|
+
if (level === 2 || level === 3) playSound(event);
|
|
48
|
+
if (level === 1 || level === 3) desktopNotify(event, extra);
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
function buildNotifyExtra(payload = {}, options = {}) {
|
|
41
52
|
const source = resolveNotificationSource({
|
|
42
53
|
host: HOST,
|
|
@@ -56,63 +67,30 @@ function getSettings() {
|
|
|
56
67
|
function runRalphLoop(payload) {
|
|
57
68
|
const settings = getSettings();
|
|
58
69
|
if (settings.ralph_loop_enabled === false) return false;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (rlOut.decision === 'block') {
|
|
71
|
-
appendReplayEvent(payload.cwd || process.cwd(), {
|
|
72
|
-
host: HOST,
|
|
73
|
-
event: 'verify_gate_blocked',
|
|
74
|
-
source: 'ralph-loop',
|
|
75
|
-
reason: rlOut.reason || '',
|
|
76
|
-
});
|
|
77
|
-
output(rlOut);
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
} catch {}
|
|
82
|
-
return false;
|
|
70
|
+
return runGateScript({
|
|
71
|
+
payload,
|
|
72
|
+
host: HOST,
|
|
73
|
+
scriptPath: join(__dirname, 'ralph-loop.mjs'),
|
|
74
|
+
args: IS_GEMINI ? ['--gemini'] : HOST === 'codex' ? ['--codex'] : [],
|
|
75
|
+
source: 'ralph-loop',
|
|
76
|
+
blockEvent: 'verify_gate_blocked',
|
|
77
|
+
timeout: 120_000,
|
|
78
|
+
appendReplayEvent,
|
|
79
|
+
output,
|
|
80
|
+
});
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
function runDeliveryGate(payload) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (gateOut.decision === 'block') {
|
|
97
|
-
appendReplayEvent(payload.cwd || process.cwd(), {
|
|
98
|
-
host: HOST,
|
|
99
|
-
event: 'delivery_gate_blocked',
|
|
100
|
-
source: 'delivery-gate',
|
|
101
|
-
reason: gateOut.reason || '',
|
|
102
|
-
});
|
|
103
|
-
output(gateOut);
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} catch {}
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function readCompletionText(payload = {}) {
|
|
112
|
-
return payload['last-assistant-message']
|
|
113
|
-
|| payload.last_assistant_message
|
|
114
|
-
|| payload.lastAssistantMessage
|
|
115
|
-
|| '';
|
|
84
|
+
return runGateScript({
|
|
85
|
+
payload,
|
|
86
|
+
host: HOST,
|
|
87
|
+
scriptPath: join(__dirname, 'delivery-gate.mjs'),
|
|
88
|
+
source: 'delivery-gate',
|
|
89
|
+
blockEvent: 'delivery_gate_blocked',
|
|
90
|
+
timeout: 30_000,
|
|
91
|
+
appendReplayEvent,
|
|
92
|
+
output,
|
|
93
|
+
});
|
|
116
94
|
}
|
|
117
95
|
|
|
118
96
|
function readMainTurnState(cwd) {
|
|
@@ -124,9 +102,9 @@ function consumeMainTurnState(cwd, turnState) {
|
|
|
124
102
|
if (turnState?.role === 'main') clearTurnState(cwd);
|
|
125
103
|
}
|
|
126
104
|
|
|
127
|
-
function shouldProcessCloseout(turnState
|
|
105
|
+
function shouldProcessCloseout(turnState) {
|
|
128
106
|
if (turnState) return turnState.kind === 'complete';
|
|
129
|
-
return
|
|
107
|
+
return false;
|
|
130
108
|
}
|
|
131
109
|
|
|
132
110
|
function cmdPreCompact() {
|
|
@@ -217,29 +195,24 @@ function cmdInject() {
|
|
|
217
195
|
|
|
218
196
|
function cmdStop() {
|
|
219
197
|
const payload = readStdinJson();
|
|
220
|
-
const lastMsg = readCompletionText(payload);
|
|
221
198
|
const cwd = payload.cwd || process.cwd();
|
|
222
199
|
const turnState = readMainTurnState(cwd);
|
|
223
|
-
const shouldProcess = shouldProcessCloseout(turnState
|
|
200
|
+
const shouldProcess = shouldProcessCloseout(turnState);
|
|
224
201
|
clearRouteContext();
|
|
225
202
|
if (shouldProcess && runRalphLoop(payload)) {
|
|
226
203
|
consumeMainTurnState(cwd, turnState);
|
|
227
|
-
|
|
228
|
-
desktopNotify('warning', buildNotifyExtra(payload));
|
|
204
|
+
notifyByLevel('warning', buildNotifyExtra(payload));
|
|
229
205
|
return;
|
|
230
206
|
}
|
|
231
207
|
if (shouldProcess && runDeliveryGate(payload)) {
|
|
232
208
|
consumeMainTurnState(cwd, turnState);
|
|
233
|
-
|
|
234
|
-
desktopNotify('warning', buildNotifyExtra(payload));
|
|
209
|
+
notifyByLevel('warning', buildNotifyExtra(payload));
|
|
235
210
|
return;
|
|
236
211
|
}
|
|
237
212
|
|
|
238
213
|
const settings = getSettings();
|
|
239
|
-
const level = settings.notify_level ?? 0;
|
|
240
214
|
if (shouldProcess) {
|
|
241
|
-
|
|
242
|
-
if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(payload));
|
|
215
|
+
notifyByLevel('complete', buildNotifyExtra(payload), settings);
|
|
243
216
|
}
|
|
244
217
|
consumeMainTurnState(cwd, turnState);
|
|
245
218
|
emptySuppress();
|
|
@@ -262,8 +235,7 @@ function cmdCodexNotify() {
|
|
|
262
235
|
if (shouldIgnoreCodexNotifyClient(client)) return;
|
|
263
236
|
|
|
264
237
|
if (type === 'approval-requested') {
|
|
265
|
-
|
|
266
|
-
desktopNotify('confirm', buildNotifyExtra(data));
|
|
238
|
+
notifyByLevel('confirm', buildNotifyExtra(data));
|
|
267
239
|
return;
|
|
268
240
|
}
|
|
269
241
|
if (type !== 'agent-turn-complete') return;
|
|
@@ -279,20 +251,16 @@ function cmdCodexNotify() {
|
|
|
279
251
|
const settings = getSettings();
|
|
280
252
|
if (runRalphLoop(data)) {
|
|
281
253
|
consumeMainTurnState(cwd, turnState);
|
|
282
|
-
|
|
283
|
-
desktopNotify('warning', buildNotifyExtra(data));
|
|
254
|
+
notifyByLevel('warning', buildNotifyExtra(data), settings);
|
|
284
255
|
return;
|
|
285
256
|
}
|
|
286
257
|
if (runDeliveryGate(data)) {
|
|
287
258
|
consumeMainTurnState(cwd, turnState);
|
|
288
|
-
|
|
289
|
-
desktopNotify('warning', buildNotifyExtra(data));
|
|
259
|
+
notifyByLevel('warning', buildNotifyExtra(data), settings);
|
|
290
260
|
return;
|
|
291
261
|
}
|
|
292
262
|
|
|
293
|
-
|
|
294
|
-
if (level === 2 || level === 3) playSound('complete');
|
|
295
|
-
if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(data));
|
|
263
|
+
notifyByLevel('complete', buildNotifyExtra(data), settings);
|
|
296
264
|
consumeMainTurnState(cwd, turnState);
|
|
297
265
|
}
|
|
298
266
|
|
|
@@ -277,9 +277,9 @@ export function describeProjectStoreFile(cwd, relativePath = '') {
|
|
|
277
277
|
export function buildProjectStorageHint(cwd, options = {}) {
|
|
278
278
|
const summary = getProjectStoreSummary(cwd, options)
|
|
279
279
|
const hints = []
|
|
280
|
-
hints.push(
|
|
280
|
+
hints.push(`当前状态文件写入 \`${summary.promptStatePath}\``)
|
|
281
281
|
if (summary.stateSessionMode === 'default') {
|
|
282
|
-
hints.push(
|
|
282
|
+
hints.push(`当前宿主未提供稳定会话标识,因此使用分支默认位置 \`${summary.stateSessionToken}\``)
|
|
283
283
|
}
|
|
284
284
|
if (summary.usesSharedStore) {
|
|
285
285
|
hints.push(`项目存储:\`project_store_mode=repo-shared\`;本地激活/运行态目录仍是 \`${summary.promptActivationDir}\`,知识库/方案目录改为 \`${summary.promptStoreDir}\``)
|
|
@@ -307,12 +307,12 @@ export function buildProjectStorageBlock(cwd, options = {}) {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
const explanations = []
|
|
310
|
-
explanations.push('
|
|
310
|
+
explanations.push('说明:状态文件只使用 `state_path`。')
|
|
311
311
|
if (summary.stateSessionMode === 'default') {
|
|
312
|
-
explanations.push('
|
|
312
|
+
explanations.push('说明:当前宿主未提供稳定会话标识,因此使用分支默认位置。')
|
|
313
313
|
}
|
|
314
314
|
if (summary.usesSharedStore) {
|
|
315
|
-
explanations.push('
|
|
315
|
+
explanations.push('说明:状态文件与 `.ralph-*.json` 写本地激活目录;`context.md`、`guidelines.md`、`DESIGN.md`、`verify.yaml`、`modules/`、`plans/`、`archive/` 写知识库/方案目录。')
|
|
316
316
|
} else {
|
|
317
317
|
explanations.push('说明:当前使用项目本地 `.helloagents/` 作为激活目录、知识库目录和方案目录。')
|
|
318
318
|
}
|
|
@@ -16,9 +16,9 @@ export function getTargetPlans(snapshot) {
|
|
|
16
16
|
|
|
17
17
|
function describeStateLabel(state) {
|
|
18
18
|
if (state.stateSessionMode === 'default') {
|
|
19
|
-
return '
|
|
19
|
+
return '当前分支默认位置的状态文件'
|
|
20
20
|
}
|
|
21
|
-
return '
|
|
21
|
+
return '当前会话的状态文件'
|
|
22
22
|
}
|
|
23
23
|
export function classifyPlan(plan) {
|
|
24
24
|
if (!plan) {
|
|
@@ -140,12 +140,12 @@ export function buildVerifyModeHintFromSnapshot(snapshot) {
|
|
|
140
140
|
export function buildStateSyncHintFromSnapshot(snapshot) {
|
|
141
141
|
const issues = collectStateSyncIssues(snapshot)
|
|
142
142
|
if (issues.length === 0) return ''
|
|
143
|
-
return
|
|
143
|
+
return `状态文件提醒:${issues.join(';')};继续项目级流程、收尾或进入压缩前先同步状态文件。`
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
export function buildStateRoleHintFromSnapshot(snapshot) {
|
|
147
147
|
if (!snapshot.state.exists || snapshot.plans.length > 0) return ''
|
|
148
|
-
return `恢复约束:当前仅检测到${describeStateLabel(snapshot.state)}
|
|
148
|
+
return `恢复约束:当前仅检测到${describeStateLabel(snapshot.state)};先以当前用户消息、显式命令和代码事实确认当前任务。状态文件只用于找回上次停在哪,不是当前任务的自动授权或唯一判断依据。`
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
export function buildUiContractHint(cwd, snapshot) {
|
|
@@ -167,7 +167,7 @@ export function buildUiContractHint(cwd, snapshot) {
|
|
|
167
167
|
if (visualValidationRequired) {
|
|
168
168
|
extraHints.push('若当前 UI 契约要求视觉验收,收尾前需写 `.helloagents/.ralph-visual.json` 记录关键视口、状态与结论')
|
|
169
169
|
}
|
|
170
|
-
return `UI
|
|
170
|
+
return `UI 约束提示:如本次属于视觉/交互任务,设计决策优先级固定为:当前活跃 plan.md / prd/03-ui-design.md → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → hello-ui。${extraHints.length > 0 ? ` ${extraHints.join(';')}。` : ''}`
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
export { normalizeTaskFile, readStateSnapshot, listPlanPackages, getWorkflowSnapshot }
|
|
@@ -59,7 +59,7 @@ function buildConsolidateAction(recommendation) {
|
|
|
59
59
|
phase: 'consolidate',
|
|
60
60
|
mode: recommendation.mode,
|
|
61
61
|
routeHint: recommendation.guidance,
|
|
62
|
-
gateHint: '交付把关:审查与验证证据已满足;先写 `.helloagents/.ralph-closeout.json`
|
|
62
|
+
gateHint: '交付把关:审查与验证证据已满足;先写 `.helloagents/.ralph-closeout.json` 记录需求覆盖与交付清单,再更新 `state_path` 并归档后才可交付。',
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -67,7 +67,7 @@ function buildConsolidateAction(recommendation) {
|
|
|
67
67
|
phase: 'consolidate',
|
|
68
68
|
mode: recommendation.mode || 'ready',
|
|
69
69
|
routeHint: recommendation.guidance,
|
|
70
|
-
gateHint: '
|
|
70
|
+
gateHint: '交付把关:当前已具备收尾证据;更新 `state_path`、知识文件并归档后即可交付。',
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -239,8 +239,8 @@ function buildClosedRecommendation(scopeLabel, plan, cwd) {
|
|
|
239
239
|
? `${scopeLabel} "${plan.planName}" 的任务与交付证据已闭合。`
|
|
240
240
|
: `${scopeLabel} "${plan.planName}" 的任务、审查与验证已闭合。`,
|
|
241
241
|
guidance: closedPlanEvidence.closeoutReady
|
|
242
|
-
? '当前进入 CONSOLIDATE
|
|
243
|
-
: '当前进入 CONSOLIDATE:先写 `.helloagents/.ralph-closeout.json`
|
|
242
|
+
? '当前进入 CONSOLIDATE:更新 `state_path`、知识文件并归档方案后即可交付;不要无故重开新的方案包或重新跑一遍无关验证。'
|
|
243
|
+
: '当前进入 CONSOLIDATE:先写 `.helloagents/.ralph-closeout.json` 记录需求覆盖与交付清单,再更新 `state_path` 并归档后交付。',
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
@@ -58,7 +58,7 @@ function buildCommandRouteMessage(skillName, recommendation, verifyModeHint) {
|
|
|
58
58
|
if (skillName === 'auto') {
|
|
59
59
|
return recommendation.stage === 'consolidate'
|
|
60
60
|
? `当前工作流约束:${recommendation.summary} 当前建议下一阶段:CONSOLIDATE。${recommendation.guidance} 若本次明确使用 ~auto,则在未命中阻塞判定时直接完成当前收尾,不再额外停下询问。`
|
|
61
|
-
: `当前工作流约束:${recommendation.summary} 当前建议主路径:${recommendation.nextPath}。${recommendation.guidance} 若本次明确使用 ~auto
|
|
61
|
+
: `当前工作流约束:${recommendation.summary} 当前建议主路径:${recommendation.nextPath}。${recommendation.guidance} 若本次明确使用 ~auto,则命中主路径后继续执行后续阶段,除非触发阻塞判定,否则不要在方案/PRD 阶段额外停下。`
|
|
62
62
|
}
|
|
63
63
|
if (skillName === 'plan') {
|
|
64
64
|
if (recommendation.stage === 'consolidate') {
|