helloagents 3.0.22 → 3.0.25
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/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +19 -11
- package/README_CN.md +19 -11
- package/bootstrap-lite.md +2 -2
- package/bootstrap.md +2 -2
- package/gemini-extension.json +1 -1
- package/install.ps1 +11 -11
- package/package.json +1 -1
- package/scripts/cli-codex-config.mjs +50 -3
- package/scripts/cli-codex-hooks-state.mjs +264 -0
- package/scripts/cli-codex.mjs +21 -14
- package/scripts/cli-doctor-codex.mjs +26 -3
- package/scripts/cli-host-detect.mjs +3 -3
- package/scripts/cli-lifecycle.mjs +14 -7
- package/scripts/cli-messages.mjs +1 -1
- package/scripts/cli-utils.mjs +4 -3
- package/scripts/delivery-gate.mjs +20 -11
- package/scripts/notify-closeout.mjs +22 -2
- package/scripts/notify-route.mjs +22 -15
- package/scripts/notify-sound.mjs +94 -0
- package/scripts/notify-ui.mjs +43 -11
- package/scripts/notify.mjs +241 -66
- package/scripts/project-session-cleanup.mjs +27 -1
- package/scripts/ralph-loop.mjs +76 -81
- package/scripts/runtime-scope.mjs +45 -17
- package/scripts/session-capsule.mjs +1 -0
- package/scripts/turn-state-cli.mjs +24 -2
- package/scripts/turn-stop-gate.mjs +61 -7
- package/skills/commands/help/SKILL.md +1 -1
- package/skills/helloagents/SKILL.md +1 -1
package/scripts/ralph-loop.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { readFileSync } from 'node:fs';
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { execSync } from 'node:child_process';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
11
12
|
import { clearVerifyEvidence, detectCommands, hasUnsafeVerifyCommand, writeVerifyEvidence } from './verify-state.mjs';
|
|
12
13
|
import {
|
|
13
14
|
getRuntimeEvidencePath,
|
|
@@ -34,10 +35,6 @@ function readSettings() {
|
|
|
34
35
|
// ── Circuit Breaker (consecutive failure tracking) ───────────────────
|
|
35
36
|
const BREAKER_FILE_NAME = 'loop-breaker.json';
|
|
36
37
|
|
|
37
|
-
function getBreakerPath(cwd, options = {}) {
|
|
38
|
-
return getRuntimeEvidencePath(cwd, BREAKER_FILE_NAME, options);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
38
|
function readBreaker(cwd, options = {}) {
|
|
42
39
|
return readRuntimeEvidence(cwd, BREAKER_FILE_NAME, options)
|
|
43
40
|
|| { consecutive_failures: 0, last_failure: null };
|
|
@@ -92,111 +89,109 @@ function runVerify(commands, cwd) {
|
|
|
92
89
|
return failures;
|
|
93
90
|
}
|
|
94
91
|
|
|
95
|
-
// ── Result Handlers ──────────────────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
function handleSuccess(cwd, isSubagent, options = {}) {
|
|
98
|
-
resetBreaker(cwd, options);
|
|
99
|
-
writeVerifyEvidence(cwd, {
|
|
100
|
-
commands: detectCommands(cwd),
|
|
101
|
-
fastOnly: isSubagent,
|
|
102
|
-
source: isSubagent ? 'subagent' : 'stop',
|
|
103
|
-
}, options);
|
|
104
|
-
|
|
105
|
-
if (isSubagent) {
|
|
106
|
-
process.stdout.write(JSON.stringify({
|
|
107
|
-
hookSpecificOutput: {
|
|
108
|
-
hookEventName: HOOK_EVENT,
|
|
109
|
-
additionalContext: '子代理快速验证通过(lint/typecheck)。请控制器审查变更后继续。',
|
|
110
|
-
},
|
|
111
|
-
suppressOutput: true,
|
|
112
|
-
}));
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Progress detection: warn if claiming done but no git changes
|
|
117
|
-
if (!hasGitChanges(cwd)) {
|
|
118
|
-
process.stdout.write(JSON.stringify({
|
|
119
|
-
hookSpecificOutput: {
|
|
120
|
-
hookEventName: HOOK_EVENT,
|
|
121
|
-
additionalContext: '⚠️ [Ralph Loop] 验证通过但未检测到代码变更(git diff 为空)。如果确实完成了编码任务,请确认变更已保存。',
|
|
122
|
-
},
|
|
123
|
-
suppressOutput: true,
|
|
124
|
-
}));
|
|
125
|
-
} else {
|
|
126
|
-
process.stdout.write(JSON.stringify({ suppressOutput: true }));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function handleFailure(failures, cwd, options = {}) {
|
|
131
|
-
clearVerifyEvidence(cwd, options);
|
|
132
|
-
const breaker = readBreaker(cwd, options);
|
|
133
|
-
breaker.consecutive_failures += 1;
|
|
134
|
-
breaker.last_failure = new Date().toISOString();
|
|
135
|
-
writeBreaker(cwd, breaker, options);
|
|
136
|
-
|
|
137
|
-
const breakerWarning = breaker.consecutive_failures >= 3
|
|
138
|
-
? `\n\n⚠️ [断路器] 已连续 ${breaker.consecutive_failures} 次验证失败。当前修复思路可能有误,先处理:\n 1. 重新分析根因,不要继续在同一方向上硬修\n 2. 检查是否存在架构层面的问题\n 3. 考虑回退到上一个正常状态重新开始`
|
|
139
|
-
: '';
|
|
140
|
-
|
|
141
|
-
const details = failures.map(f => `\u2717 ${f.cmd}\n${f.output}`).join('\n\n');
|
|
142
|
-
process.stdout.write(JSON.stringify({
|
|
143
|
-
decision: 'block',
|
|
144
|
-
reason: `[Ralph Loop] 验证失败:\n\n${details}\n\n请先修复以上问题,再报告完成。${breakerWarning}`,
|
|
145
|
-
suppressOutput: true,
|
|
146
|
-
}));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
92
|
/** Filter commands to fast checks only for subagent mode. Returns null if no fast commands found. */
|
|
150
93
|
function filterSubagentCommands(commands) {
|
|
151
94
|
const fast = commands.filter(cmd =>
|
|
152
95
|
/lint|typecheck|type-check|ruff check|mypy|eslint|tsc/.test(cmd)
|
|
153
96
|
);
|
|
154
97
|
if (fast.length === 0) {
|
|
155
|
-
|
|
98
|
+
return {
|
|
156
99
|
hookSpecificOutput: {
|
|
157
100
|
hookEventName: HOOK_EVENT,
|
|
158
101
|
additionalContext: '子代理完成。未找到快速验证命令,请控制器手动审查变更。',
|
|
159
102
|
},
|
|
160
103
|
suppressOutput: true,
|
|
161
|
-
}
|
|
162
|
-
return null;
|
|
104
|
+
};
|
|
163
105
|
}
|
|
164
|
-
return fast;
|
|
106
|
+
return { commands: fast };
|
|
165
107
|
}
|
|
166
108
|
|
|
167
|
-
|
|
168
|
-
async function main() {
|
|
109
|
+
export function evaluateRalphLoop(data = {}, runtime = {}) {
|
|
169
110
|
const settings = readSettings();
|
|
170
111
|
if (settings.ralph_loop_enabled === false) {
|
|
171
|
-
|
|
172
|
-
return;
|
|
112
|
+
return { suppressOutput: true };
|
|
173
113
|
}
|
|
174
114
|
|
|
175
|
-
let data = {};
|
|
176
|
-
try { data = JSON.parse(readFileSync(0, 'utf-8')); } catch {}
|
|
177
115
|
const cwd = data.cwd || process.cwd();
|
|
178
116
|
const runtimeOptions = { payload: data };
|
|
117
|
+
const isSubagent = runtime.isSubagent ?? IS_SUBAGENT;
|
|
118
|
+
const hookEventName = runtime.hookEventName || HOOK_EVENT;
|
|
179
119
|
|
|
180
120
|
let commands = detectCommands(cwd);
|
|
181
121
|
if (!commands?.length) {
|
|
182
|
-
|
|
183
|
-
return;
|
|
122
|
+
return { suppressOutput: true };
|
|
184
123
|
}
|
|
185
124
|
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
if (!commands) return;
|
|
125
|
+
if (isSubagent) {
|
|
126
|
+
const filtered = filterSubagentCommands(commands);
|
|
127
|
+
if (!filtered?.commands) return filtered || { suppressOutput: true };
|
|
128
|
+
commands = filtered.commands;
|
|
189
129
|
}
|
|
190
130
|
|
|
191
131
|
const failures = runVerify(commands, cwd);
|
|
192
|
-
if (failures.length === 0)
|
|
193
|
-
|
|
194
|
-
|
|
132
|
+
if (failures.length === 0) {
|
|
133
|
+
resetBreaker(cwd, runtimeOptions);
|
|
134
|
+
writeVerifyEvidence(cwd, {
|
|
135
|
+
commands: detectCommands(cwd),
|
|
136
|
+
fastOnly: isSubagent,
|
|
137
|
+
source: isSubagent ? 'subagent' : 'stop',
|
|
138
|
+
}, runtimeOptions);
|
|
139
|
+
|
|
140
|
+
if (isSubagent) {
|
|
141
|
+
return {
|
|
142
|
+
hookSpecificOutput: {
|
|
143
|
+
hookEventName,
|
|
144
|
+
additionalContext: '子代理快速验证通过(lint/typecheck)。请控制器审查变更后继续。',
|
|
145
|
+
},
|
|
146
|
+
suppressOutput: true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
195
149
|
|
|
196
|
-
|
|
197
|
-
|
|
150
|
+
if (!hasGitChanges(cwd)) {
|
|
151
|
+
return {
|
|
152
|
+
hookSpecificOutput: {
|
|
153
|
+
hookEventName,
|
|
154
|
+
additionalContext: '⚠️ [Ralph Loop] 验证通过但未检测到代码变更(git diff 为空)。如果确实完成了编码任务,请确认变更已保存。',
|
|
155
|
+
},
|
|
156
|
+
suppressOutput: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { suppressOutput: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
clearVerifyEvidence(cwd, runtimeOptions);
|
|
164
|
+
const breaker = readBreaker(cwd, runtimeOptions);
|
|
165
|
+
breaker.consecutive_failures += 1;
|
|
166
|
+
breaker.last_failure = new Date().toISOString();
|
|
167
|
+
writeBreaker(cwd, breaker, runtimeOptions);
|
|
168
|
+
|
|
169
|
+
const breakerWarning = breaker.consecutive_failures >= 3
|
|
170
|
+
? `\n\n⚠️ [断路器] 已连续 ${breaker.consecutive_failures} 次验证失败。当前修复思路可能有误,先处理:\n 1. 重新分析根因,不要继续在同一方向上硬修\n 2. 检查是否存在架构层面的问题\n 3. 考虑回退到上一个正常状态重新开始`
|
|
171
|
+
: '';
|
|
172
|
+
const details = failures.map(f => `\u2717 ${f.cmd}\n${f.output}`).join('\n\n');
|
|
173
|
+
return {
|
|
198
174
|
decision: 'block',
|
|
199
|
-
reason: `[Ralph Loop]
|
|
175
|
+
reason: `[Ralph Loop] 验证失败:\n\n${details}\n\n请先修复以上问题,再报告完成。${breakerWarning}`,
|
|
200
176
|
suppressOutput: true,
|
|
201
|
-
}
|
|
202
|
-
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Main ──────────────────────────────────────────────────────────────
|
|
181
|
+
function main() {
|
|
182
|
+
let data = {};
|
|
183
|
+
try { data = JSON.parse(readFileSync(0, 'utf-8')); } catch {}
|
|
184
|
+
process.stdout.write(JSON.stringify(evaluateRalphLoop(data)));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
188
|
+
try {
|
|
189
|
+
main();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
process.stdout.write(JSON.stringify({
|
|
192
|
+
decision: 'block',
|
|
193
|
+
reason: `[Ralph Loop] 验证脚本执行异常,已暂停完成通知。\n原因:${error?.message || error}`,
|
|
194
|
+
suppressOutput: true,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -18,6 +18,12 @@ export const DEFAULT_STATE_SESSION_TOKEN = 'default'
|
|
|
18
18
|
export const USER_RUNTIME_DIR_NAME = 'runtime'
|
|
19
19
|
export { cleanupUserRuntimeRoot, getUserRuntimeRoot, USER_RUNTIME_MAX_AGE_MS }
|
|
20
20
|
|
|
21
|
+
const gitTopLevelCache = new Map()
|
|
22
|
+
const gitBranchNameCache = new Map()
|
|
23
|
+
const gitShortHeadCache = new Map()
|
|
24
|
+
const workspaceNameCache = new Map()
|
|
25
|
+
let userRuntimeCleanupDone = false
|
|
26
|
+
|
|
21
27
|
function normalizePath(filePath = '') {
|
|
22
28
|
return filePath ? normalize(resolve(filePath)) : ''
|
|
23
29
|
}
|
|
@@ -35,6 +41,13 @@ function runGit(cwd, args = []) {
|
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
function readCachedValue(cache, key, loader) {
|
|
45
|
+
if (cache.has(key)) return cache.get(key)
|
|
46
|
+
const value = loader()
|
|
47
|
+
cache.set(key, value)
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
function getHomeDir(env = process.env) {
|
|
39
52
|
return env.HOME || env.USERPROFILE || homedir()
|
|
40
53
|
}
|
|
@@ -55,35 +68,46 @@ function samePath(left, right) {
|
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
function resolveGitTopLevel(cwd) {
|
|
58
|
-
const
|
|
59
|
-
|
|
71
|
+
const normalizedCwd = normalizePath(cwd || process.cwd())
|
|
72
|
+
return readCachedValue(gitTopLevelCache, normalizedCwd, () => {
|
|
73
|
+
const absolute = runGit(normalizedCwd, ['rev-parse', '--path-format=absolute', '--show-toplevel'])
|
|
74
|
+
if (absolute) return normalize(resolve(absolute))
|
|
60
75
|
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
const raw = runGit(normalizedCwd, ['rev-parse', '--show-toplevel'])
|
|
77
|
+
return raw ? normalize(resolve(normalizedCwd, raw)) : ''
|
|
78
|
+
})
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
function resolveGitBranchName(cwd) {
|
|
66
|
-
const
|
|
67
|
-
|
|
82
|
+
const normalizedCwd = normalizePath(cwd || process.cwd())
|
|
83
|
+
return readCachedValue(gitBranchNameCache, normalizedCwd, () => {
|
|
84
|
+
const branchName = runGit(normalizedCwd, ['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
85
|
+
if (branchName && branchName !== 'HEAD') return branchName
|
|
68
86
|
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
const symbolicName = runGit(normalizedCwd, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
|
|
88
|
+
return symbolicName && symbolicName !== 'HEAD' ? symbolicName : ''
|
|
89
|
+
})
|
|
71
90
|
}
|
|
72
91
|
|
|
73
92
|
function resolveGitShortHead(cwd) {
|
|
74
|
-
|
|
93
|
+
const normalizedCwd = normalizePath(cwd || process.cwd())
|
|
94
|
+
return readCachedValue(gitShortHeadCache, normalizedCwd, () =>
|
|
95
|
+
runGit(normalizedCwd, ['rev-parse', '--short', 'HEAD']))
|
|
75
96
|
}
|
|
76
97
|
|
|
77
98
|
function resolveWorkspaceName(cwd) {
|
|
78
|
-
const
|
|
79
|
-
|
|
99
|
+
const normalizedCwd = normalizePath(cwd || process.cwd())
|
|
100
|
+
return readCachedValue(workspaceNameCache, normalizedCwd, () => {
|
|
101
|
+
const branchName = resolveGitBranchName(normalizedCwd)
|
|
102
|
+
if (branchName) return sanitizeRuntimeSegment(branchName, 'workspace')
|
|
80
103
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
if (resolveGitTopLevel(normalizedCwd)) {
|
|
105
|
+
const shortHead = sanitizeRuntimeSegment(resolveGitShortHead(normalizedCwd), '')
|
|
106
|
+
return shortHead ? `detached-${shortHead}` : 'detached'
|
|
107
|
+
}
|
|
85
108
|
|
|
86
|
-
|
|
109
|
+
return 'workspace'
|
|
110
|
+
})
|
|
87
111
|
}
|
|
88
112
|
|
|
89
113
|
export function sanitizeRuntimeSegment(value = '', fallback = '') {
|
|
@@ -245,6 +269,7 @@ export function writeActiveProjectSession(scope, { host = '', source = '', env =
|
|
|
245
269
|
host,
|
|
246
270
|
source,
|
|
247
271
|
aliases,
|
|
272
|
+
...(current.cleanupCheckedAt ? { cleanupCheckedAt: current.cleanupCheckedAt } : {}),
|
|
248
273
|
updatedAt: new Date().toISOString(),
|
|
249
274
|
})
|
|
250
275
|
return activePath
|
|
@@ -323,7 +348,10 @@ function buildTransientRuntimeDir(cwd, options = {}) {
|
|
|
323
348
|
.update(`${normalizedCwd.toLowerCase()}::${token}`)
|
|
324
349
|
.digest('hex')
|
|
325
350
|
.slice(0, 16)
|
|
326
|
-
|
|
351
|
+
if (!userRuntimeCleanupDone) {
|
|
352
|
+
cleanupUserRuntimeRoot()
|
|
353
|
+
userRuntimeCleanupDone = true
|
|
354
|
+
}
|
|
327
355
|
|
|
328
356
|
return {
|
|
329
357
|
cwd: normalizedCwd,
|
|
@@ -142,6 +142,7 @@ export function clearCapsuleSection(cwd, section, options = {}) {
|
|
|
142
142
|
|
|
143
143
|
const capsule = readSessionCapsule(cwd, options)
|
|
144
144
|
if (!Object.prototype.hasOwnProperty.call(capsule, section)) return false
|
|
145
|
+
if (capsule[section] == null) return false
|
|
145
146
|
capsule[section] = null
|
|
146
147
|
capsule[`${section}UpdatedAt`] = new Date().toISOString()
|
|
147
148
|
writeSessionCapsule(cwd, capsule, options)
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process'
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { dirname, join, resolve } from 'node:path'
|
|
4
6
|
import { fileURLToPath } from 'node:url'
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
function normalizePath(filePath = '') {
|
|
9
|
+
const resolved = resolve(filePath)
|
|
10
|
+
try {
|
|
11
|
+
return realpathSync(resolved)
|
|
12
|
+
} catch {
|
|
13
|
+
return resolved
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function samePath(left, right) {
|
|
18
|
+
const a = normalizePath(left)
|
|
19
|
+
const b = normalizePath(right)
|
|
20
|
+
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const localScriptPath = join(dirname(fileURLToPath(import.meta.url)), 'turn-state.mjs')
|
|
24
|
+
const runtimeScriptPath = join(homedir(), '.helloagents', 'helloagents', 'scripts', 'turn-state.mjs')
|
|
25
|
+
const scriptPath = existsSync(runtimeScriptPath) && !samePath(runtimeScriptPath, localScriptPath)
|
|
26
|
+
? runtimeScriptPath
|
|
27
|
+
: localScriptPath
|
|
28
|
+
|
|
7
29
|
const result = spawnSync(process.execPath, [scriptPath, ...process.argv.slice(2)], {
|
|
8
30
|
stdio: 'inherit',
|
|
9
31
|
windowsHide: true,
|
|
@@ -48,6 +48,56 @@ function buildBlockReason(routeContext, detail, cwd) {
|
|
|
48
48
|
].filter(Boolean).join('\n')
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function getLastAssistantMessage(payload = {}) {
|
|
52
|
+
return String(
|
|
53
|
+
payload.lastAssistantMessage
|
|
54
|
+
|| payload.last_assistant_message
|
|
55
|
+
|| payload['last-assistant-message']
|
|
56
|
+
|| '',
|
|
57
|
+
).trim()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function countMatches(text, pattern) {
|
|
61
|
+
const matches = text.match(pattern)
|
|
62
|
+
return matches ? matches.length : 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateFormattedCloseoutMessage(routeContext, payload, cwd) {
|
|
66
|
+
const message = getLastAssistantMessage(payload)
|
|
67
|
+
if (!message || !message.includes('【HelloAGENTS】')) return ''
|
|
68
|
+
|
|
69
|
+
const firstNonEmptyLine = message
|
|
70
|
+
.split(/\r?\n/)
|
|
71
|
+
.map((line) => line.trim())
|
|
72
|
+
.find(Boolean)
|
|
73
|
+
|
|
74
|
+
if (!firstNonEmptyLine || !/^[💡⚡🔵✅❓⚠️❌]【HelloAGENTS】- /.test(firstNonEmptyLine)) {
|
|
75
|
+
return buildBlockReason(
|
|
76
|
+
routeContext,
|
|
77
|
+
'最终收尾消息使用了 HelloAGENTS 外层格式,但首个非空行不是规范标题行。',
|
|
78
|
+
cwd,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (countMatches(message, /[💡⚡🔵✅❓⚠️❌]【HelloAGENTS】-/g) > 1) {
|
|
83
|
+
return buildBlockReason(
|
|
84
|
+
routeContext,
|
|
85
|
+
'最终收尾消息重复输出了 HelloAGENTS 标题;请把所有内容合并到同一个外层块内。',
|
|
86
|
+
cwd,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (countMatches(message, /^🔄 下一步:/gm) > 1) {
|
|
91
|
+
return buildBlockReason(
|
|
92
|
+
routeContext,
|
|
93
|
+
'最终收尾消息重复输出了 `🔄 下一步`;请只保留一个真实下一步。',
|
|
94
|
+
cwd,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return ''
|
|
99
|
+
}
|
|
100
|
+
|
|
51
101
|
function getMainTurnState(cwd, payload = {}) {
|
|
52
102
|
const turnState = readTurnState(cwd, { payload })
|
|
53
103
|
return turnState?.role === 'main' ? turnState : null
|
|
@@ -64,11 +114,13 @@ function hasStructuredBlocker(turnState) {
|
|
|
64
114
|
)
|
|
65
115
|
}
|
|
66
116
|
|
|
67
|
-
function validateTurnState(routeContext, turnState, cwd) {
|
|
117
|
+
function validateTurnState(routeContext, turnState, cwd, payload = {}) {
|
|
68
118
|
if (!turnState) {
|
|
69
119
|
return buildBlockReason(routeContext, '缺少主代理 turn-state。', cwd)
|
|
70
120
|
}
|
|
71
121
|
if (turnState.kind === 'complete') {
|
|
122
|
+
const formatReason = validateFormattedCloseoutMessage(routeContext, payload, cwd)
|
|
123
|
+
if (formatReason) return formatReason
|
|
72
124
|
return ''
|
|
73
125
|
}
|
|
74
126
|
if (turnState.kind === 'waiting' || turnState.kind === 'blocked') {
|
|
@@ -91,18 +143,20 @@ function validateTurnState(routeContext, turnState, cwd) {
|
|
|
91
143
|
return buildBlockReason(routeContext, `当前 turn-state 为 \`${turnState.kind}\`,不能作为本轮结束状态。`, cwd)
|
|
92
144
|
}
|
|
93
145
|
|
|
94
|
-
function
|
|
95
|
-
const payload = readStdinJson()
|
|
146
|
+
export function evaluateTurnStopGate(payload = {}) {
|
|
96
147
|
const cwd = payload.cwd || process.cwd()
|
|
97
148
|
const routeContext = getApplicableRouteContext({ cwd, payload })
|
|
98
149
|
|
|
99
150
|
if (!routeContext || !ENFORCED_COMMANDS.has(routeContext.skillName)) {
|
|
100
|
-
|
|
101
|
-
return
|
|
151
|
+
return { decision: 'continue' }
|
|
102
152
|
}
|
|
103
153
|
|
|
104
|
-
const reason = validateTurnState(routeContext, getMainTurnState(cwd, payload), cwd)
|
|
105
|
-
|
|
154
|
+
const reason = validateTurnState(routeContext, getMainTurnState(cwd, payload), cwd, payload)
|
|
155
|
+
return reason ? { decision: 'block', reason } : { decision: 'continue' }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function main() {
|
|
159
|
+
process.stdout.write(JSON.stringify(evaluateTurnStopGate(readStdinJson())))
|
|
106
160
|
}
|
|
107
161
|
|
|
108
162
|
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
@@ -39,7 +39,7 @@ Trigger: ~help
|
|
|
39
39
|
完成时:hello-verify, hello-reflect
|
|
40
40
|
|
|
41
41
|
### 当前设置
|
|
42
|
-
优先使用当前会话上下文中已注入的“当前用户设置”、该配置文件原始 JSON 或此前读取结果摘要显示;若会话上下文不存在该信息,或缺少下表任一配置项,才读取一次 `~/.helloagents/helloagents.json
|
|
42
|
+
优先使用当前会话上下文中已注入的“当前用户设置”、该配置文件原始 JSON 或此前读取结果摘要显示;若会话上下文不存在该信息,或缺少下表任一配置项,才读取一次 `~/.helloagents/helloagents.json`,并在后续轮次复用。对 Codex 来说,首次对话前若当前上下文仍缺少这些配置项,或刚经历压缩/恢复后的首次对话,同样先读取一次再继续。
|
|
43
43
|
如果当前 CLI 存在工作区限制导致家目录不可读,则明确说明“无法直接读取配置文件,以下按已注入设置或默认值展示”,不要改用无关工具或伪造已读取结果。
|
|
44
44
|
| 配置项 | 默认值 | 作用 | 适用 CLI |
|
|
45
45
|
|--------|-------|------|---------|
|
|
@@ -5,7 +5,7 @@ description: 按任务类型适用 — 建立质量驱动工作流,通过技
|
|
|
5
5
|
|
|
6
6
|
# HelloAGENTS
|
|
7
7
|
|
|
8
|
-
主代理触发或读取任意 skill 时,只有本轮最终收尾消息才按通用输出格式包装;流式内容、进度或状态汇报、中间文本,以及任何仍将继续执行的文本,都保持自然输出。最终收尾中的 `🔄 下一步`
|
|
8
|
+
主代理触发或读取任意 skill 时,只有本轮最终收尾消息才按通用输出格式包装;流式内容、进度或状态汇报、中间文本,以及任何仍将继续执行的文本,都保持自然输出。最终收尾中的 `🔄 下一步` 写真实动作,不写当前状态;等待用户授权时使用等待输入态收尾,已获授权且可继续执行时不得收尾。同一条最终收尾消息只包装一次;若需要分段,在同一个外层块内展开,不在正文里再次输出 `【HelloAGENTS】` 或第二个 `🔄 下一步`。
|
|
9
9
|
子代理只豁免路由与收尾要求,直接执行任务;安全、质量、验证和失败处理规则仍持续生效,且不得包装 HelloAGENTS 外层输出格式。
|
|
10
10
|
只有运行时必须识别本轮“完成 / 等待输入 / 阻塞”时,主代理才写 turn-state;普通问候、普通问答、T0 只读分析和一次性解释不调用。必须调用场景:显式 `~auto` / `~loop`、非只读任务完成验证并进入收尾、需要 delivery gate / Ralph Loop / closeout evidence、需要等待或阻塞且运行时必须识别状态、已进入项目连续流程或方案包闭环。首选 `helloagents-turn-state write --kind complete --role main`;等待或阻塞时写 `kind=waiting` / `kind=blocked`,并同时写 `reasonCategory` 与 `reason`。显式 `~auto` / `~loop` 下,还必须写入 `blocker.target`、`blocker.evidence`、`blocker.requiredAction`。不要查找、读取或拼接 `turn-state.mjs` 源码路径。子代理不得写 turn-state。
|
|
11
11
|
|