helloagents 3.0.23 → 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,
@@ -143,18 +143,20 @@ function validateTurnState(routeContext, turnState, cwd, payload = {}) {
143
143
  return buildBlockReason(routeContext, `当前 turn-state 为 \`${turnState.kind}\`,不能作为本轮结束状态。`, cwd)
144
144
  }
145
145
 
146
- function main() {
147
- const payload = readStdinJson()
146
+ export function evaluateTurnStopGate(payload = {}) {
148
147
  const cwd = payload.cwd || process.cwd()
149
148
  const routeContext = getApplicableRouteContext({ cwd, payload })
150
149
 
151
150
  if (!routeContext || !ENFORCED_COMMANDS.has(routeContext.skillName)) {
152
- process.stdout.write(JSON.stringify({ decision: 'continue' }))
153
- return
151
+ return { decision: 'continue' }
154
152
  }
155
153
 
156
154
  const reason = validateTurnState(routeContext, getMainTurnState(cwd, payload), cwd, payload)
157
- process.stdout.write(JSON.stringify(reason ? { decision: 'block', reason } : { decision: 'continue' }))
155
+ return reason ? { decision: 'block', reason } : { decision: 'continue' }
156
+ }
157
+
158
+ function main() {
159
+ process.stdout.write(JSON.stringify(evaluateTurnStopGate(readStdinJson())))
158
160
  }
159
161
 
160
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
  |--------|-------|------|---------|