helloagents 3.0.12 → 3.0.15-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.
Files changed (72) hide show
  1. package/.claude-plugin/marketplace.json +6 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +169 -30
  5. package/README_CN.md +169 -30
  6. package/bootstrap-lite.md +27 -20
  7. package/bootstrap.md +30 -23
  8. package/cli.mjs +119 -11
  9. package/gemini-extension.json +1 -1
  10. package/install.ps1 +125 -0
  11. package/install.sh +118 -0
  12. package/package.json +23 -4
  13. package/scripts/advisor-state.mjs +36 -63
  14. package/scripts/capability-registry.mjs +3 -3
  15. package/scripts/cli-branch.mjs +84 -0
  16. package/scripts/cli-codex-config.mjs +11 -20
  17. package/scripts/cli-codex.mjs +32 -38
  18. package/scripts/cli-doctor-render.mjs +4 -0
  19. package/scripts/cli-doctor.mjs +40 -30
  20. package/scripts/cli-host-detect.mjs +0 -1
  21. package/scripts/cli-hosts.mjs +16 -8
  22. package/scripts/cli-lifecycle-hosts.mjs +92 -27
  23. package/scripts/cli-lifecycle.mjs +9 -7
  24. package/scripts/cli-messages.mjs +34 -16
  25. package/scripts/cli-runtime-carrier.mjs +36 -0
  26. package/scripts/cli-runtime-root.mjs +72 -0
  27. package/scripts/cli-toml.mjs +0 -79
  28. package/scripts/cli-utils.mjs +30 -4
  29. package/scripts/closeout-state.mjs +35 -62
  30. package/scripts/delivery-gate-messages.mjs +70 -0
  31. package/scripts/delivery-gate.mjs +9 -75
  32. package/scripts/guard-rules.mjs +42 -42
  33. package/scripts/guard.mjs +44 -24
  34. package/scripts/notify-context.mjs +19 -28
  35. package/scripts/notify-gates.mjs +2 -0
  36. package/scripts/notify-route.mjs +9 -7
  37. package/scripts/notify-ui.mjs +46 -33
  38. package/scripts/notify.mjs +60 -32
  39. package/scripts/project-storage.mjs +35 -66
  40. package/scripts/ralph-loop.mjs +36 -31
  41. package/scripts/replay-state.mjs +31 -128
  42. package/scripts/review-state.mjs +34 -61
  43. package/scripts/runtime-artifacts.mjs +95 -0
  44. package/scripts/runtime-context.mjs +35 -29
  45. package/scripts/runtime-scope.mjs +313 -0
  46. package/scripts/session-capsule.mjs +202 -0
  47. package/scripts/turn-state-cli.mjs +17 -0
  48. package/scripts/turn-state.mjs +185 -66
  49. package/scripts/turn-stop-gate.mjs +24 -6
  50. package/scripts/verify-state.mjs +34 -85
  51. package/scripts/visual-state.mjs +38 -65
  52. package/scripts/workflow-core.mjs +2 -2
  53. package/scripts/workflow-plan-files.mjs +1 -1
  54. package/scripts/workflow-recommendation.mjs +17 -13
  55. package/scripts/workflow-state.mjs +5 -5
  56. package/skills/commands/build/SKILL.md +1 -1
  57. package/skills/commands/commit/SKILL.md +1 -1
  58. package/skills/commands/help/SKILL.md +3 -3
  59. package/skills/commands/loop/SKILL.md +1 -1
  60. package/skills/commands/plan/SKILL.md +8 -6
  61. package/skills/commands/prd/SKILL.md +5 -3
  62. package/skills/commands/verify/SKILL.md +5 -5
  63. package/skills/hello-debug/SKILL.md +20 -3
  64. package/skills/hello-review/SKILL.md +2 -2
  65. package/skills/hello-subagent/SKILL.md +2 -2
  66. package/skills/hello-test/SKILL.md +6 -2
  67. package/skills/hello-ui/SKILL.md +4 -4
  68. package/skills/hello-verify/SKILL.md +10 -7
  69. package/skills/helloagents/SKILL.md +12 -7
  70. package/templates/context.md +6 -0
  71. package/templates/plans/plan.md +3 -0
  72. package/templates/plans/tasks.md +8 -3
@@ -3,7 +3,7 @@
3
3
  // Zero external dependencies, ES module, cross-platform
4
4
 
5
5
  import { join, dirname } from 'node:path';
6
- import { existsSync, readFileSync } from 'node:fs';
6
+ import { readFileSync } from 'node:fs';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { homedir } from 'node:os';
9
9
  import { playSound as _playSound, desktopNotify as _desktopNotify } from './notify-ui.mjs';
@@ -13,10 +13,12 @@ import { shouldIgnoreCodexNotifyClient } from './notify-events.mjs';
13
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
- import { clearRouteContext, writeRouteContext } from './runtime-context.mjs';
16
+ import { clearRouteContext, getApplicableRouteContext, writeRouteContext } from './runtime-context.mjs';
17
17
  import { appendReplayEvent, startReplaySession } from './replay-state.mjs';
18
18
  import { clearTurnState, readTurnState } from './turn-state.mjs';
19
19
  import { getWorkflowRecommendation } from './workflow-state.mjs';
20
+ import { resolveSessionToken } from './session-token.mjs';
21
+ import { isProjectRuntimeActive } from './runtime-scope.mjs';
20
22
 
21
23
  const __filename = fileURLToPath(import.meta.url);
22
24
  const __dirname = dirname(__filename);
@@ -33,6 +35,7 @@ const EVENT_NAME = {
33
35
  UserPromptSubmit: IS_GEMINI ? 'BeforeAgent' : 'UserPromptSubmit',
34
36
  PreCompact: IS_GEMINI ? 'BeforeAgent' : 'PreCompact',
35
37
  };
38
+ const RALPH_LOOP_ROUTE_COMMANDS = new Set(['verify', 'loop']);
36
39
 
37
40
  const playSound = (event) => _playSound(PKG_ROOT, event);
38
41
  const desktopNotify = (event, extra) => _desktopNotify(PKG_ROOT, event, extra);
@@ -64,9 +67,18 @@ function getSettings() {
64
67
  return readSettings(CONFIG_FILE);
65
68
  }
66
69
 
67
- function runRalphLoop(payload) {
70
+ function shouldRunRalphLoop(cwd, turnState, payload = {}) {
71
+ if (!turnState || turnState.kind !== 'complete') return false;
72
+ if (turnState.requiresDeliveryGate) return true;
73
+ const routeContext = getApplicableRouteContext({ cwd, payload });
74
+ return RALPH_LOOP_ROUTE_COMMANDS.has(routeContext?.skillName);
75
+ }
76
+
77
+ function runRalphLoop(payload, { turnState } = {}) {
68
78
  const settings = getSettings();
69
79
  if (settings.ralph_loop_enabled === false) return false;
80
+ const cwd = payload.cwd || process.cwd();
81
+ if (!shouldRunRalphLoop(cwd, turnState, payload)) return false;
70
82
  return runGateScript({
71
83
  payload,
72
84
  host: HOST,
@@ -106,13 +118,24 @@ function runTurnStopGate(payload) {
106
118
  });
107
119
  }
108
120
 
109
- function readMainTurnState(cwd) {
110
- const turnState = readTurnState(cwd);
121
+ function attachTurnSession(payload = {}, cwd = payload.cwd || process.cwd()) {
122
+ const sessionId = resolveSessionToken({
123
+ payload,
124
+ env: process.env,
125
+ ppid: process.ppid,
126
+ allowPpidFallback: !isProjectRuntimeActive(cwd),
127
+ });
128
+ if (!sessionId || payload.sessionId) return payload;
129
+ return { ...payload, sessionId };
130
+ }
131
+
132
+ function readMainTurnState(cwd, payload = {}) {
133
+ const turnState = readTurnState(cwd, { payload });
111
134
  return turnState?.role === 'main' ? turnState : null;
112
135
  }
113
136
 
114
- function consumeMainTurnState(cwd, turnState) {
115
- if (turnState?.role === 'main') clearTurnState(cwd);
137
+ function consumeMainTurnState(cwd, turnState, payload = {}) {
138
+ if (turnState?.role === 'main') clearTurnState(cwd, { payload });
116
139
  }
117
140
 
118
141
  function shouldProcessCloseout(turnState) {
@@ -136,6 +159,7 @@ function cmdPreCompact() {
136
159
  host: HOST,
137
160
  event: 'pre_compact_snapshot',
138
161
  source: 'pre-compact',
162
+ payload,
139
163
  details: {
140
164
  bootstrapFile,
141
165
  installMode: settings.install_mode || '',
@@ -146,7 +170,7 @@ function cmdPreCompact() {
146
170
 
147
171
  function cmdRoute() {
148
172
  const payload = readStdinJson();
149
- clearTurnState(payload.cwd || process.cwd());
173
+ clearTurnState(payload.cwd || process.cwd(), { payload });
150
174
  handleRouteCommand({
151
175
  payload,
152
176
  host: HOST,
@@ -181,15 +205,17 @@ function cmdInject() {
181
205
  source,
182
206
  bootstrapFile,
183
207
  installMode: settings.install_mode || '',
208
+ payload,
184
209
  });
185
210
  appendReplayEvent(cwd, {
186
211
  host: HOST,
187
212
  event: 'session_injected',
188
213
  source,
214
+ payload,
189
215
  details: {
190
216
  bootstrapFile,
191
217
  installMode: settings.install_mode || '',
192
- activatedProject: existsSync(join(cwd, '.helloagents')),
218
+ activatedProject: isProjectRuntimeActive(cwd),
193
219
  },
194
220
  });
195
221
  const context = buildInjectContext({
@@ -201,27 +227,28 @@ function cmdInject() {
201
227
  cwd,
202
228
  payload,
203
229
  });
204
- clearRouteContext();
205
- clearTurnState(cwd);
230
+ clearRouteContext({ cwd, payload });
231
+ clearTurnState(cwd, { payload });
206
232
  suppressedOutput(EVENT_NAME.SessionStart, context || undefined);
207
233
  }
208
234
 
209
235
  function cmdStop() {
210
236
  const payload = readStdinJson();
211
237
  const cwd = payload.cwd || process.cwd();
212
- const turnState = readMainTurnState(cwd);
213
- if (runTurnStopGate(payload)) {
214
- if (turnState && turnState.kind !== 'complete') consumeMainTurnState(cwd, turnState);
238
+ const turnPayload = attachTurnSession(payload, cwd);
239
+ const turnState = readMainTurnState(cwd, turnPayload);
240
+ if (runTurnStopGate(turnPayload)) {
241
+ if (turnState && turnState.kind !== 'complete') consumeMainTurnState(cwd, turnState, turnPayload);
215
242
  return;
216
243
  }
217
244
  const shouldProcess = shouldProcessCloseout(turnState);
218
- if (shouldProcess && runRalphLoop(payload)) {
219
- consumeMainTurnState(cwd, turnState);
245
+ if (shouldProcess && runRalphLoop(turnPayload, { turnState })) {
246
+ consumeMainTurnState(cwd, turnState, turnPayload);
220
247
  notifyByLevel('warning', buildNotifyExtra(payload));
221
248
  return;
222
249
  }
223
- if (shouldProcess && runDeliveryGate(payload)) {
224
- consumeMainTurnState(cwd, turnState);
250
+ if (shouldProcess && runDeliveryGate(turnPayload)) {
251
+ consumeMainTurnState(cwd, turnState, turnPayload);
225
252
  notifyByLevel('warning', buildNotifyExtra(payload));
226
253
  return;
227
254
  }
@@ -230,8 +257,8 @@ function cmdStop() {
230
257
  if (shouldProcess) {
231
258
  notifyByLevel('complete', buildNotifyExtra(payload), settings);
232
259
  }
233
- consumeMainTurnState(cwd, turnState);
234
- clearRouteContext();
260
+ consumeMainTurnState(cwd, turnState, turnPayload);
261
+ clearRouteContext({ cwd, payload: turnPayload });
235
262
  emptySuppress();
236
263
  }
237
264
 
@@ -246,6 +273,8 @@ function cmdDesktop() {
246
273
  function cmdCodexNotify() {
247
274
  let data = {};
248
275
  try { data = JSON.parse(process.argv[3] || '{}'); } catch {}
276
+ const cwd = data.cwd || process.cwd();
277
+ const turnPayload = attachTurnSession(data, cwd);
249
278
 
250
279
  const type = data.type || '';
251
280
  const client = data.client || '';
@@ -257,34 +286,33 @@ function cmdCodexNotify() {
257
286
  }
258
287
  if (type !== 'agent-turn-complete') return;
259
288
 
260
- const cwd = data.cwd || process.cwd();
261
- const turnState = readMainTurnState(cwd);
262
- if (runTurnStopGate(data)) {
263
- if (turnState && turnState.kind !== 'complete') consumeMainTurnState(cwd, turnState);
289
+ const turnState = readMainTurnState(cwd, turnPayload);
290
+ if (runTurnStopGate(turnPayload)) {
291
+ if (turnState && turnState.kind !== 'complete') consumeMainTurnState(cwd, turnState, turnPayload);
264
292
  return;
265
293
  }
266
294
  if (!turnState) return;
267
295
  if (turnState.kind !== 'complete') {
268
- consumeMainTurnState(cwd, turnState);
269
- clearRouteContext();
296
+ consumeMainTurnState(cwd, turnState, turnPayload);
297
+ clearRouteContext({ cwd, payload: turnPayload });
270
298
  return;
271
299
  }
272
300
 
273
301
  const settings = getSettings();
274
- if (runRalphLoop(data)) {
275
- consumeMainTurnState(cwd, turnState);
302
+ if (runRalphLoop(turnPayload, { turnState })) {
303
+ consumeMainTurnState(cwd, turnState, turnPayload);
276
304
  notifyByLevel('warning', buildNotifyExtra(data), settings);
277
305
  return;
278
306
  }
279
- if (runDeliveryGate(data)) {
280
- consumeMainTurnState(cwd, turnState);
307
+ if (runDeliveryGate(turnPayload)) {
308
+ consumeMainTurnState(cwd, turnState, turnPayload);
281
309
  notifyByLevel('warning', buildNotifyExtra(data), settings);
282
310
  return;
283
311
  }
284
312
 
285
313
  notifyByLevel('complete', buildNotifyExtra(data), settings);
286
- consumeMainTurnState(cwd, turnState);
287
- clearRouteContext();
314
+ consumeMainTurnState(cwd, turnState, turnPayload);
315
+ clearRouteContext({ cwd, payload: turnPayload });
288
316
  }
289
317
 
290
318
  const cmd = process.argv[2] || '';
@@ -5,13 +5,19 @@ import { homedir } from 'node:os'
5
5
  import { basename, dirname, isAbsolute, join, normalize, resolve } from 'node:path'
6
6
 
7
7
  import { DEFAULTS } from './cli-config.mjs'
8
- import { resolveSessionToken } from './session-token.mjs'
8
+ import {
9
+ PROJECT_DIR_NAME,
10
+ getProjectActivationDir,
11
+ getProjectSessionScope,
12
+ normalizeRuntimeOptions,
13
+ } from './runtime-scope.mjs'
14
+ import {
15
+ getSessionArtifactPath,
16
+ getSessionArtifactRelativePath,
17
+ } from './session-capsule.mjs'
9
18
 
10
- export const PROJECT_DIR_NAME = '.helloagents'
11
19
  const PROJECTS_DIR_NAME = 'projects'
12
- const PROJECT_SESSIONS_DIR_NAME = 'sessions'
13
20
  const PROJECT_STORE_MODES = new Set(['local', 'repo-shared'])
14
- const DEFAULT_STATE_SESSION_TOKEN = 'default'
15
21
 
16
22
  function safeJson(filePath) {
17
23
  try {
@@ -34,19 +40,6 @@ function runGitRevParse(cwd, args = []) {
34
40
  }
35
41
  }
36
42
 
37
- function runGitCommand(cwd, args = []) {
38
- try {
39
- return execFileSync('git', args, {
40
- cwd,
41
- encoding: 'utf-8',
42
- timeout: 5_000,
43
- stdio: ['ignore', 'pipe', 'ignore'],
44
- }).trim()
45
- } catch {
46
- return ''
47
- }
48
- }
49
-
50
43
  function resolveGitTopLevel(cwd) {
51
44
  const absolute = runGitRevParse(cwd, ['--path-format=absolute', '--show-toplevel'])
52
45
  if (absolute) return normalize(absolute)
@@ -70,16 +63,6 @@ function sanitizeRepoName(value = '') {
70
63
  return normalized || 'project'
71
64
  }
72
65
 
73
- function sanitizeStateScopeSegment(value = '', fallback = '') {
74
- const normalized = String(value)
75
- .trim()
76
- .toLowerCase()
77
- .replace(/[^a-z0-9._-]+/g, '-')
78
- .replace(/^-+|-+$/g, '')
79
- .slice(0, 48)
80
- return normalized || fallback
81
- }
82
-
83
66
  function buildProjectKey(cwd) {
84
67
  const repoRoot = resolveGitTopLevel(cwd)
85
68
  const commonDir = resolveGitCommonDir(cwd, repoRoot)
@@ -113,15 +96,6 @@ function formatPromptPath(pathValue = '') {
113
96
  return pathValue ? normalize(pathValue).replace(/\\/g, '/') : ''
114
97
  }
115
98
 
116
- function resolveGitBranchName(cwd) {
117
- const branchName = runGitRevParse(cwd, ['--abbrev-ref', 'HEAD'])
118
- if (branchName && branchName !== 'HEAD') return branchName
119
-
120
- const symbolicBranchName = runGitCommand(cwd, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
121
- if (symbolicBranchName && symbolicBranchName !== 'HEAD') return symbolicBranchName
122
- return ''
123
- }
124
-
125
99
  export function normalizeProjectStoreMode(value) {
126
100
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
127
101
  return PROJECT_STORE_MODES.has(normalized) ? normalized : DEFAULTS.project_store_mode
@@ -136,37 +110,16 @@ export function getProjectStoreMode() {
136
110
  return normalizeProjectStoreMode(settings.project_store_mode)
137
111
  }
138
112
 
139
- export function getProjectActivationDir(cwd) {
140
- return join(cwd, PROJECT_DIR_NAME)
141
- }
142
-
143
- export function getProjectSessionStateScope(cwd, {
144
- payload = {},
145
- env = process.env,
146
- ppid = process.ppid,
147
- } = {}) {
148
- const rawSessionToken = resolveSessionToken({
149
- payload,
150
- env,
151
- ppid,
152
- allowPpidFallback: false,
153
- })
154
- const branchName = sanitizeStateScopeSegment(resolveGitBranchName(cwd), 'detached')
155
- const sessionToken = sanitizeStateScopeSegment(rawSessionToken, DEFAULT_STATE_SESSION_TOKEN)
156
- const sessionDir = join(
157
- getProjectActivationDir(cwd),
158
- PROJECT_SESSIONS_DIR_NAME,
159
- branchName,
160
- sessionToken,
161
- )
113
+ export function getProjectSessionStateScope(cwd, options = {}) {
114
+ const scope = getProjectSessionScope(cwd, normalizeRuntimeOptions(options))
162
115
 
163
116
  return {
164
117
  stateScope: 'session',
165
- stateSessionToken: sessionToken,
166
- stateSessionMode: rawSessionToken ? 'host-session' : 'default',
167
- stateBranch: branchName,
168
- sessionDir,
169
- statePath: join(sessionDir, 'STATE.md'),
118
+ stateSessionToken: scope.session,
119
+ stateSessionMode: scope.sessionMode,
120
+ stateBranch: scope.branch,
121
+ sessionDir: scope.sessionDir,
122
+ statePath: scope.statePath,
170
123
  }
171
124
  }
172
125
 
@@ -174,6 +127,18 @@ export function getProjectStatePath(cwd, options = {}) {
174
127
  return getProjectSessionStateScope(cwd, options).statePath
175
128
  }
176
129
 
130
+ export function getProjectEvidenceDir(cwd, options = {}) {
131
+ return getProjectSessionScope(cwd, normalizeRuntimeOptions(options)).artifactsDir
132
+ }
133
+
134
+ export function getProjectEvidencePath(cwd, fileName, options = {}) {
135
+ return getSessionArtifactPath(cwd, fileName, options)
136
+ }
137
+
138
+ export function getProjectEvidenceRelativePath(cwd, fileName, options = {}) {
139
+ return getSessionArtifactRelativePath(cwd, fileName, options)
140
+ }
141
+
177
142
  export function isRepoSharedProjectStore(cwd) {
178
143
  return getProjectStoreMode(cwd) === 'repo-shared'
179
144
  }
@@ -191,6 +156,7 @@ export function getProjectStoreSummary(cwd, options = {}) {
191
156
  const activationDir = getProjectActivationDir(cwd)
192
157
  const storeDir = getProjectStoreDir(cwd)
193
158
  const stateScope = getProjectSessionStateScope(cwd, options)
159
+ const artifactsDir = getProjectEvidenceDir(cwd, options)
194
160
  const projectKey = buildProjectKey(cwd)
195
161
  const projectStoreMode = getProjectStoreMode(cwd)
196
162
 
@@ -204,6 +170,7 @@ export function getProjectStoreSummary(cwd, options = {}) {
204
170
  stateSessionMode: stateScope.stateSessionMode,
205
171
  stateBranch: stateScope.stateBranch,
206
172
  sessionStateDir: stateScope.sessionDir,
173
+ artifactsDir,
207
174
  usesSharedStore: projectStoreMode === 'repo-shared',
208
175
  projectKey: projectKey.key,
209
176
  repoRoot: projectKey.repoRoot,
@@ -212,6 +179,7 @@ export function getProjectStoreSummary(cwd, options = {}) {
212
179
  promptStoreDir: formatPromptPath(storeDir),
213
180
  promptStatePath: formatPromptPath(stateScope.statePath),
214
181
  promptSessionStateDir: formatPromptPath(stateScope.sessionDir),
182
+ promptArtifactsDir: formatPromptPath(artifactsDir),
215
183
  }
216
184
  }
217
185
 
@@ -282,7 +250,7 @@ export function buildProjectStorageHint(cwd, options = {}) {
282
250
  hints.push(`当前宿主未提供稳定会话标识,因此使用分支默认位置 \`${summary.stateSessionToken}\``)
283
251
  }
284
252
  if (summary.usesSharedStore) {
285
- hints.push(`项目存储:\`project_store_mode=repo-shared\`;本地激活/运行态目录仍是 \`${summary.promptActivationDir}\`,知识库/方案目录改为 \`${summary.promptStoreDir}\``)
253
+ hints.push(`项目存储:\`project_store_mode=repo-shared\`;本地激活/会话运行态目录仍是 \`${summary.promptActivationDir}\`,知识库/方案目录改为 \`${summary.promptStoreDir}\``)
286
254
  }
287
255
  return hints.join('。') + (hints.length > 0 ? '。' : '')
288
256
  }
@@ -302,6 +270,7 @@ export function buildProjectStorageBlock(cwd, options = {}) {
302
270
  state_session_token: summary.stateSessionToken,
303
271
  state_session_mode: summary.stateSessionMode,
304
272
  session_state_dir: summary.promptSessionStateDir,
273
+ artifacts_dir: summary.promptArtifactsDir,
305
274
  knowledge_base_dir: summary.promptStoreDir,
306
275
  uses_shared_store: summary.usesSharedStore,
307
276
  }
@@ -312,7 +281,7 @@ export function buildProjectStorageBlock(cwd, options = {}) {
312
281
  explanations.push('说明:当前宿主未提供稳定会话标识,因此使用分支默认位置。')
313
282
  }
314
283
  if (summary.usesSharedStore) {
315
- explanations.push('说明:状态文件与 `.ralph-*.json` 写本地激活目录;`context.md`、`guidelines.md`、`DESIGN.md`、`verify.yaml`、`modules/`、`plans/`、`archive/` 写知识库/方案目录。')
284
+ explanations.push('说明:状态文件与会话产物写本地激活目录;`context.md`、`guidelines.md`、`DESIGN.md`、`verify.yaml`、`modules/`、`plans/`、`archive/` 写知识库/方案目录。')
316
285
  } else {
317
286
  explanations.push('说明:当前使用项目本地 `.helloagents/` 作为激活目录、知识库目录和方案目录。')
318
287
  }
@@ -4,11 +4,16 @@
4
4
  * Runs on SubagentStop (Claude Code) and Stop (Codex CLI).
5
5
  * Auto-detects lint/test commands and blocks if they fail.
6
6
  */
7
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
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
11
  import { clearVerifyEvidence, detectCommands, hasUnsafeVerifyCommand, writeVerifyEvidence } from './verify-state.mjs';
12
+ import {
13
+ getRuntimeEvidencePath,
14
+ readRuntimeEvidence,
15
+ writeRuntimeEvidence,
16
+ } from './runtime-artifacts.mjs';
12
17
 
13
18
  const CONFIG_FILE = join(homedir(), '.helloagents', 'helloagents.json');
14
19
  const CMD_TIMEOUT = 60_000; // 60s
@@ -27,28 +32,23 @@ function readSettings() {
27
32
  }
28
33
 
29
34
  // ── Circuit Breaker (consecutive failure tracking) ───────────────────
30
- const BREAKER_FILE_NAME = '.ralph-breaker.json';
35
+ const BREAKER_FILE_NAME = 'loop-breaker.json';
31
36
 
32
- function getBreakerPath(cwd) {
33
- return join(cwd, '.helloagents', BREAKER_FILE_NAME);
37
+ function getBreakerPath(cwd, options = {}) {
38
+ return getRuntimeEvidencePath(cwd, BREAKER_FILE_NAME, options);
34
39
  }
35
40
 
36
- function readBreaker(cwd) {
37
- try {
38
- return JSON.parse(readFileSync(getBreakerPath(cwd), 'utf-8'));
39
- } catch {
40
- return { consecutive_failures: 0, last_failure: null };
41
- }
41
+ function readBreaker(cwd, options = {}) {
42
+ return readRuntimeEvidence(cwd, BREAKER_FILE_NAME, options)
43
+ || { consecutive_failures: 0, last_failure: null };
42
44
  }
43
45
 
44
- function writeBreaker(cwd, state) {
45
- const dir = join(cwd, '.helloagents');
46
- try { mkdirSync(dir, { recursive: true }); } catch {}
47
- writeFileSync(getBreakerPath(cwd), JSON.stringify(state, null, 2));
46
+ function writeBreaker(cwd, state, options = {}) {
47
+ writeRuntimeEvidence(cwd, BREAKER_FILE_NAME, state, options);
48
48
  }
49
49
 
50
- function resetBreaker(cwd) {
51
- writeBreaker(cwd, { consecutive_failures: 0, last_failure: null });
50
+ function resetBreaker(cwd, options = {}) {
51
+ writeBreaker(cwd, { consecutive_failures: 0, last_failure: null }, options);
52
52
  }
53
53
 
54
54
  // ── Progress Detection (git diff check) ──────────────────────────────
@@ -74,7 +74,7 @@ function runVerify(commands, cwd) {
74
74
  const failures = [];
75
75
  for (const cmd of commands) {
76
76
  if (hasUnsafeVerifyCommand([cmd])) {
77
- failures.push({ cmd, output: 'Blocked: shell operators not allowed in verify commands' });
77
+ failures.push({ cmd, output: '已阻止:验证命令不允许使用 shell 组合操作符' });
78
78
  continue;
79
79
  }
80
80
  try {
@@ -85,8 +85,8 @@ function runVerify(commands, cwd) {
85
85
  continue;
86
86
  }
87
87
  let output = ((err.stdout || '') + (err.stderr || '')).trim();
88
- if (output.length > 1000) output = output.slice(0, 1000) + '\n...(truncated)';
89
- failures.push({ cmd, output: output || `exit code ${err.status}` });
88
+ if (output.length > 1000) output = output.slice(0, 1000) + '\n…已截断';
89
+ failures.push({ cmd, output: output || `退出码 ${err.status}` });
90
90
  }
91
91
  }
92
92
  return failures;
@@ -94,13 +94,13 @@ function runVerify(commands, cwd) {
94
94
 
95
95
  // ── Result Handlers ──────────────────────────────────────────────────
96
96
 
97
- function handleSuccess(cwd, isSubagent) {
98
- resetBreaker(cwd);
97
+ function handleSuccess(cwd, isSubagent, options = {}) {
98
+ resetBreaker(cwd, options);
99
99
  writeVerifyEvidence(cwd, {
100
100
  commands: detectCommands(cwd),
101
101
  fastOnly: isSubagent,
102
102
  source: isSubagent ? 'subagent' : 'stop',
103
- });
103
+ }, options);
104
104
 
105
105
  if (isSubagent) {
106
106
  process.stdout.write(JSON.stringify({
@@ -127,12 +127,12 @@ function handleSuccess(cwd, isSubagent) {
127
127
  }
128
128
  }
129
129
 
130
- function handleFailure(failures, cwd) {
131
- clearVerifyEvidence(cwd);
132
- const breaker = readBreaker(cwd);
130
+ function handleFailure(failures, cwd, options = {}) {
131
+ clearVerifyEvidence(cwd, options);
132
+ const breaker = readBreaker(cwd, options);
133
133
  breaker.consecutive_failures += 1;
134
134
  breaker.last_failure = new Date().toISOString();
135
- writeBreaker(cwd, breaker);
135
+ writeBreaker(cwd, breaker, options);
136
136
 
137
137
  const breakerWarning = breaker.consecutive_failures >= 3
138
138
  ? `\n\n⚠️ [断路器] 已连续 ${breaker.consecutive_failures} 次验证失败。当前修复思路可能有误,建议:\n 1. 重新分析根因,不要继续在同一方向上硬修\n 2. 检查是否存在架构层面的问题\n 3. 考虑回退到上一个正常状态重新开始`
@@ -141,7 +141,7 @@ function handleFailure(failures, cwd) {
141
141
  const details = failures.map(f => `\u2717 ${f.cmd}\n${f.output}`).join('\n\n');
142
142
  process.stdout.write(JSON.stringify({
143
143
  decision: 'block',
144
- reason: `[Ralph Loop] Verification failed:\n\n${details}\n\nFix the issues above before completing.${breakerWarning}`,
144
+ reason: `[Ralph Loop] 验证失败:\n\n${details}\n\n请先修复以上问题,再报告完成。${breakerWarning}`,
145
145
  suppressOutput: true,
146
146
  }));
147
147
  }
@@ -175,6 +175,7 @@ async function main() {
175
175
  let data = {};
176
176
  try { data = JSON.parse(readFileSync(0, 'utf-8')); } catch {}
177
177
  const cwd = data.cwd || process.cwd();
178
+ const runtimeOptions = { payload: data };
178
179
 
179
180
  let commands = detectCommands(cwd);
180
181
  if (!commands?.length) {
@@ -188,10 +189,14 @@ async function main() {
188
189
  }
189
190
 
190
191
  const failures = runVerify(commands, cwd);
191
- if (failures.length === 0) handleSuccess(cwd, IS_SUBAGENT);
192
- else handleFailure(failures, cwd);
192
+ if (failures.length === 0) handleSuccess(cwd, IS_SUBAGENT, runtimeOptions);
193
+ else handleFailure(failures, cwd, runtimeOptions);
193
194
  }
194
195
 
195
- main().catch(() => {
196
- process.stdout.write(JSON.stringify({ suppressOutput: true }));
196
+ main().catch((error) => {
197
+ process.stdout.write(JSON.stringify({
198
+ decision: 'block',
199
+ reason: `[Ralph Loop] 验证脚本执行异常,已暂停完成通知。\n原因:${error?.message || error}`,
200
+ suppressOutput: true,
201
+ }));
197
202
  });