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.
@@ -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
- process.stdout.write(JSON.stringify({
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
- // ── Main ──────────────────────────────────────────────────────────────
168
- async function main() {
109
+ export function evaluateRalphLoop(data = {}, runtime = {}) {
169
110
  const settings = readSettings();
170
111
  if (settings.ralph_loop_enabled === false) {
171
- process.stdout.write(JSON.stringify({ suppressOutput: true }));
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
- process.stdout.write(JSON.stringify({ suppressOutput: true }));
183
- return;
122
+ return { suppressOutput: true };
184
123
  }
185
124
 
186
- if (IS_SUBAGENT) {
187
- commands = filterSubagentCommands(commands);
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) handleSuccess(cwd, IS_SUBAGENT, runtimeOptions);
193
- else handleFailure(failures, cwd, runtimeOptions);
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
- main().catch((error) => {
197
- process.stdout.write(JSON.stringify({
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] 验证脚本执行异常,已暂停完成通知。\n原因:${error?.message || error}`,
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 absolute = runGit(cwd, ['rev-parse', '--path-format=absolute', '--show-toplevel'])
59
- if (absolute) return normalize(resolve(absolute))
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
- const raw = runGit(cwd, ['rev-parse', '--show-toplevel'])
62
- return raw ? normalize(resolve(cwd, raw)) : ''
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 branchName = runGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD'])
67
- if (branchName && branchName !== 'HEAD') return branchName
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
- const symbolicName = runGit(cwd, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
70
- return symbolicName && symbolicName !== 'HEAD' ? symbolicName : ''
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
- return runGit(cwd, ['rev-parse', '--short', 'HEAD'])
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 branchName = resolveGitBranchName(cwd)
79
- if (branchName) return sanitizeRuntimeSegment(branchName, 'workspace')
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
- if (resolveGitTopLevel(cwd)) {
82
- const shortHead = sanitizeRuntimeSegment(resolveGitShortHead(cwd), '')
83
- return shortHead ? `detached-${shortHead}` : 'detached'
84
- }
104
+ if (resolveGitTopLevel(normalizedCwd)) {
105
+ const shortHead = sanitizeRuntimeSegment(resolveGitShortHead(normalizedCwd), '')
106
+ return shortHead ? `detached-${shortHead}` : 'detached'
107
+ }
85
108
 
86
- return 'workspace'
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
- cleanupUserRuntimeRoot()
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 { dirname, join } from 'node:path'
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
- const scriptPath = join(dirname(fileURLToPath(import.meta.url)), 'turn-state.mjs')
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 main() {
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
- process.stdout.write(JSON.stringify({ decision: 'continue' }))
101
- return
151
+ return { decision: 'continue' }
102
152
  }
103
153
 
104
- const reason = validateTurnState(routeContext, getMainTurnState(cwd, payload), cwd)
105
- process.stdout.write(JSON.stringify(reason ? { decision: 'block', reason } : { decision: 'continue' }))
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