helloagents 3.0.12 → 3.0.16-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 (73) 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 +182 -35
  5. package/README_CN.md +184 -37
  6. package/bootstrap-lite.md +32 -26
  7. package/bootstrap.md +35 -29
  8. package/cli.mjs +119 -11
  9. package/gemini-extension.json +1 -1
  10. package/install.ps1 +128 -0
  11. package/install.sh +121 -0
  12. package/package.json +23 -4
  13. package/scripts/advisor-state.mjs +36 -63
  14. package/scripts/capability-registry.mjs +4 -4
  15. package/scripts/cli-branch.mjs +84 -0
  16. package/scripts/cli-codex-config.mjs +14 -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 +119 -32
  23. package/scripts/cli-lifecycle.mjs +24 -13
  24. package/scripts/cli-messages.mjs +34 -16
  25. package/scripts/cli-runtime-carrier.mjs +15 -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-events.mjs +3 -1
  36. package/scripts/notify-gates.mjs +2 -0
  37. package/scripts/notify-route.mjs +9 -7
  38. package/scripts/notify-ui.mjs +42 -32
  39. package/scripts/notify.mjs +72 -36
  40. package/scripts/project-storage.mjs +35 -66
  41. package/scripts/ralph-loop.mjs +36 -31
  42. package/scripts/replay-state.mjs +31 -128
  43. package/scripts/review-state.mjs +34 -61
  44. package/scripts/runtime-artifacts.mjs +95 -0
  45. package/scripts/runtime-context.mjs +35 -29
  46. package/scripts/runtime-scope.mjs +313 -0
  47. package/scripts/session-capsule.mjs +202 -0
  48. package/scripts/turn-state-cli.mjs +17 -0
  49. package/scripts/turn-state.mjs +185 -66
  50. package/scripts/turn-stop-gate.mjs +24 -6
  51. package/scripts/verify-state.mjs +34 -85
  52. package/scripts/visual-state.mjs +38 -65
  53. package/scripts/workflow-core.mjs +3 -3
  54. package/scripts/workflow-plan-files.mjs +1 -1
  55. package/scripts/workflow-recommendation.mjs +17 -13
  56. package/scripts/workflow-state.mjs +5 -5
  57. package/skills/commands/build/SKILL.md +1 -1
  58. package/skills/commands/commit/SKILL.md +1 -1
  59. package/skills/commands/help/SKILL.md +5 -3
  60. package/skills/commands/loop/SKILL.md +1 -1
  61. package/skills/commands/plan/SKILL.md +8 -6
  62. package/skills/commands/prd/SKILL.md +5 -3
  63. package/skills/commands/verify/SKILL.md +5 -5
  64. package/skills/hello-debug/SKILL.md +20 -3
  65. package/skills/hello-review/SKILL.md +2 -2
  66. package/skills/hello-subagent/SKILL.md +2 -2
  67. package/skills/hello-test/SKILL.md +6 -2
  68. package/skills/hello-ui/SKILL.md +7 -7
  69. package/skills/hello-verify/SKILL.md +10 -7
  70. package/skills/helloagents/SKILL.md +14 -9
  71. package/templates/context.md +6 -0
  72. package/templates/plans/plan.md +3 -0
  73. package/templates/plans/tasks.md +8 -3
@@ -55,47 +55,50 @@ function resolveWav(pkgRoot, event) {
55
55
  return existsSync(p) ? p : null;
56
56
  }
57
57
 
58
+ function runSync(command, args) {
59
+ try {
60
+ const result = spawnSync(command, args, {
61
+ stdio: 'ignore',
62
+ windowsHide: true,
63
+ });
64
+ return !result.error && result.status === 0;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
58
70
  export function playSound(pkgRoot, event) {
59
71
  if (DISABLE_OS_NOTIFICATIONS) return;
60
72
  const wav = resolveWav(pkgRoot, event);
61
73
  if (!wav) { process.stderr.write('\x07'); return; }
62
74
  try {
63
75
  if (PLAT === 'win32') {
64
- spawnSync('powershell', ['-NoProfile', '-c',
65
- `(New-Object Media.SoundPlayer '${wav.replace(/'/g, "''")}').PlaySync()`],
66
- { stdio: 'ignore', windowsHide: true });
76
+ runSync('powershell', [
77
+ '-NoProfile',
78
+ '-c',
79
+ `(New-Object Media.SoundPlayer '${wav.replace(/'/g, "''")}').PlaySync()`,
80
+ ]);
67
81
  } else if (PLAT === 'darwin') {
68
- spawnSync('afplay', [wav], { stdio: 'ignore' });
82
+ runSync('afplay', [wav]);
69
83
  } else {
70
- const result = spawnSync('aplay', ['-q', wav], { stdio: 'ignore' });
71
- if (result.status !== 0) {
72
- const pa = spawnSync('paplay', [wav], { stdio: 'ignore' });
73
- if (pa.status !== 0) process.stderr.write('\x07');
74
- }
84
+ if (!runSync('aplay', ['-q', wav]) && !runSync('paplay', [wav])) process.stderr.write('\x07');
75
85
  }
76
86
  } catch { process.stderr.write('\x07'); }
77
87
  }
78
88
 
79
- function ensureWinAppId(pkgRoot) {
80
- if (PLAT !== 'win32') return;
89
+ function buildWindowsToastScript(notification, iconPath) {
81
90
  const regKey = `HKCU:\\Software\\Classes\\AppUserModelId\\${WIN_APPID}`;
82
- spawnSync('powershell', ['-NoProfile', '-c',
83
- `if (-not (Test-Path '${regKey}')) { New-Item -Path '${regKey}' -Force | Out-Null; Set-ItemProperty -Path '${regKey}' -Name 'DisplayName' -Value 'HelloAgents 通知' -Force }`],
84
- { stdio: 'ignore', windowsHide: true });
91
+ const iconXml = existsSync(iconPath)
92
+ ? `<image placement="appLogoOverride" src="${escapeToastText(iconPath)}" />`
93
+ : '';
94
+ const textXml = notification.toastLines
95
+ .map((line) => `<text>${escapeToastText(line)}</text>`)
96
+ .join('\n ');
97
+ return `
98
+ if (-not (Test-Path '${regKey}')) {
99
+ New-Item -Path '${regKey}' -Force | Out-Null
100
+ Set-ItemProperty -Path '${regKey}' -Name 'DisplayName' -Value 'HelloAgents 通知' -Force
85
101
  }
86
-
87
- export function desktopNotify(pkgRoot, event, extra) {
88
- if (DISABLE_OS_NOTIFICATIONS) return;
89
- const notification = buildDesktopNotificationContent(event, extra);
90
- try {
91
- if (PLAT === 'win32') {
92
- ensureWinAppId(pkgRoot);
93
- const iconPath = join(pkgRoot, 'assets', 'icons', 'icon.png').replace(/\//g, '\\');
94
- const iconXml = existsSync(iconPath) ? `<image placement="appLogoOverride" src="${iconPath}" />` : '';
95
- const textXml = notification.toastLines
96
- .map((line) => `<text>${escapeToastText(line)}</text>`)
97
- .join('\n ');
98
- const ps = `
99
102
  [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
100
103
  [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
101
104
  $xml = @"
@@ -113,17 +116,24 @@ $doc.LoadXml($xml)
113
116
  $toast = [Windows.UI.Notifications.ToastNotification]::new($doc)
114
117
  [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${WIN_APPID}').Show($toast)
115
118
  `.trim();
116
- spawnSync('powershell', ['-NoProfile', '-c', ps], { stdio: 'ignore', windowsHide: true });
119
+ }
120
+
121
+ export function desktopNotify(pkgRoot, event, extra) {
122
+ if (DISABLE_OS_NOTIFICATIONS) return;
123
+ const notification = buildDesktopNotificationContent(event, extra);
124
+ try {
125
+ if (PLAT === 'win32') {
126
+ const iconPath = join(pkgRoot, 'assets', 'icons', 'icon.png').replace(/\//g, '\\');
127
+ runSync('powershell', ['-NoProfile', '-c', buildWindowsToastScript(notification, iconPath)]);
117
128
  } else if (PLAT === 'darwin') {
118
129
  const subtitle = notification.sourceLabel
119
130
  ? ` subtitle "${escapeAppleScriptText(notification.sourceLabel)}"`
120
131
  : '';
121
- spawnSync('osascript', ['-e',
132
+ runSync('osascript', ['-e',
122
133
  `display notification "${escapeAppleScriptText(notification.message)}" with title "${escapeAppleScriptText(notification.title)}"${subtitle}`],
123
- { stdio: 'ignore' });
134
+ );
124
135
  } else {
125
- const result = spawnSync('notify-send', [notification.title, notification.body], { stdio: 'ignore' });
126
- if (result.status !== 0) process.stderr.write('\x07');
136
+ if (!runSync('notify-send', [notification.title, notification.body])) process.stderr.write('\x07');
127
137
  }
128
138
  } catch { process.stderr.write('\x07'); }
129
139
  }
@@ -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);
@@ -44,8 +47,12 @@ function normalizeNotifyLevel(value) {
44
47
 
45
48
  function notifyByLevel(event, extra, settings = getSettings()) {
46
49
  const level = normalizeNotifyLevel(settings.notify_level ?? 0);
47
- if (level === 2 || level === 3) playSound(event);
48
- if (level === 1 || level === 3) desktopNotify(event, extra);
50
+ if (level === 1) desktopNotify(event, extra);
51
+ if (level === 2) playSound(event);
52
+ if (level === 3) {
53
+ desktopNotify(event, extra);
54
+ playSound(event);
55
+ }
49
56
  }
50
57
 
51
58
  function buildNotifyExtra(payload = {}, options = {}) {
@@ -64,9 +71,18 @@ function getSettings() {
64
71
  return readSettings(CONFIG_FILE);
65
72
  }
66
73
 
67
- function runRalphLoop(payload) {
74
+ function shouldRunRalphLoop(cwd, turnState, payload = {}) {
75
+ if (!turnState || turnState.kind !== 'complete') return false;
76
+ if (turnState.requiresDeliveryGate) return true;
77
+ const routeContext = getApplicableRouteContext({ cwd, payload });
78
+ return RALPH_LOOP_ROUTE_COMMANDS.has(routeContext?.skillName);
79
+ }
80
+
81
+ function runRalphLoop(payload, { turnState } = {}) {
68
82
  const settings = getSettings();
69
83
  if (settings.ralph_loop_enabled === false) return false;
84
+ const cwd = payload.cwd || process.cwd();
85
+ if (!shouldRunRalphLoop(cwd, turnState, payload)) return false;
70
86
  return runGateScript({
71
87
  payload,
72
88
  host: HOST,
@@ -106,13 +122,24 @@ function runTurnStopGate(payload) {
106
122
  });
107
123
  }
108
124
 
109
- function readMainTurnState(cwd) {
110
- const turnState = readTurnState(cwd);
125
+ function attachTurnSession(payload = {}, cwd = payload.cwd || process.cwd()) {
126
+ const sessionId = resolveSessionToken({
127
+ payload,
128
+ env: process.env,
129
+ ppid: process.ppid,
130
+ allowPpidFallback: !isProjectRuntimeActive(cwd),
131
+ });
132
+ if (!sessionId || payload.sessionId) return payload;
133
+ return { ...payload, sessionId };
134
+ }
135
+
136
+ function readMainTurnState(cwd, payload = {}) {
137
+ const turnState = readTurnState(cwd, { payload });
111
138
  return turnState?.role === 'main' ? turnState : null;
112
139
  }
113
140
 
114
- function consumeMainTurnState(cwd, turnState) {
115
- if (turnState?.role === 'main') clearTurnState(cwd);
141
+ function consumeMainTurnState(cwd, turnState, payload = {}) {
142
+ if (turnState?.role === 'main') clearTurnState(cwd, { payload });
116
143
  }
117
144
 
118
145
  function shouldProcessCloseout(turnState) {
@@ -136,6 +163,7 @@ function cmdPreCompact() {
136
163
  host: HOST,
137
164
  event: 'pre_compact_snapshot',
138
165
  source: 'pre-compact',
166
+ payload,
139
167
  details: {
140
168
  bootstrapFile,
141
169
  installMode: settings.install_mode || '',
@@ -146,7 +174,7 @@ function cmdPreCompact() {
146
174
 
147
175
  function cmdRoute() {
148
176
  const payload = readStdinJson();
149
- clearTurnState(payload.cwd || process.cwd());
177
+ clearTurnState(payload.cwd || process.cwd(), { payload });
150
178
  handleRouteCommand({
151
179
  payload,
152
180
  host: HOST,
@@ -181,15 +209,17 @@ function cmdInject() {
181
209
  source,
182
210
  bootstrapFile,
183
211
  installMode: settings.install_mode || '',
212
+ payload,
184
213
  });
185
214
  appendReplayEvent(cwd, {
186
215
  host: HOST,
187
216
  event: 'session_injected',
188
217
  source,
218
+ payload,
189
219
  details: {
190
220
  bootstrapFile,
191
221
  installMode: settings.install_mode || '',
192
- activatedProject: existsSync(join(cwd, '.helloagents')),
222
+ activatedProject: isProjectRuntimeActive(cwd),
193
223
  },
194
224
  });
195
225
  const context = buildInjectContext({
@@ -201,37 +231,38 @@ function cmdInject() {
201
231
  cwd,
202
232
  payload,
203
233
  });
204
- clearRouteContext();
205
- clearTurnState(cwd);
234
+ clearRouteContext({ cwd, payload });
235
+ clearTurnState(cwd, { payload });
206
236
  suppressedOutput(EVENT_NAME.SessionStart, context || undefined);
207
237
  }
208
238
 
209
239
  function cmdStop() {
210
240
  const payload = readStdinJson();
211
241
  const cwd = payload.cwd || process.cwd();
212
- const turnState = readMainTurnState(cwd);
213
- if (runTurnStopGate(payload)) {
214
- if (turnState && turnState.kind !== 'complete') consumeMainTurnState(cwd, turnState);
242
+ const turnPayload = attachTurnSession(payload, cwd);
243
+ const turnState = readMainTurnState(cwd, turnPayload);
244
+ if (runTurnStopGate(turnPayload)) {
245
+ if (turnState && turnState.kind !== 'complete') consumeMainTurnState(cwd, turnState, turnPayload);
215
246
  return;
216
247
  }
217
248
  const shouldProcess = shouldProcessCloseout(turnState);
218
- if (shouldProcess && runRalphLoop(payload)) {
219
- consumeMainTurnState(cwd, turnState);
249
+ if (shouldProcess && runRalphLoop(turnPayload, { turnState })) {
250
+ consumeMainTurnState(cwd, turnState, turnPayload);
220
251
  notifyByLevel('warning', buildNotifyExtra(payload));
221
252
  return;
222
253
  }
223
- if (shouldProcess && runDeliveryGate(payload)) {
224
- consumeMainTurnState(cwd, turnState);
254
+ if (shouldProcess && runDeliveryGate(turnPayload)) {
255
+ consumeMainTurnState(cwd, turnState, turnPayload);
225
256
  notifyByLevel('warning', buildNotifyExtra(payload));
226
257
  return;
227
258
  }
228
259
 
229
260
  const settings = getSettings();
230
- if (shouldProcess) {
261
+ if (shouldProcess || !turnState) {
231
262
  notifyByLevel('complete', buildNotifyExtra(payload), settings);
232
263
  }
233
- consumeMainTurnState(cwd, turnState);
234
- clearRouteContext();
264
+ consumeMainTurnState(cwd, turnState, turnPayload);
265
+ clearRouteContext({ cwd, payload: turnPayload });
235
266
  emptySuppress();
236
267
  }
237
268
 
@@ -246,6 +277,8 @@ function cmdDesktop() {
246
277
  function cmdCodexNotify() {
247
278
  let data = {};
248
279
  try { data = JSON.parse(process.argv[3] || '{}'); } catch {}
280
+ const cwd = data.cwd || process.cwd();
281
+ const turnPayload = attachTurnSession(data, cwd);
249
282
 
250
283
  const type = data.type || '';
251
284
  const client = data.client || '';
@@ -257,34 +290,37 @@ function cmdCodexNotify() {
257
290
  }
258
291
  if (type !== 'agent-turn-complete') return;
259
292
 
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);
293
+ const turnState = readMainTurnState(cwd, turnPayload);
294
+ if (runTurnStopGate(turnPayload)) {
295
+ if (turnState && turnState.kind !== 'complete') consumeMainTurnState(cwd, turnState, turnPayload);
296
+ return;
297
+ }
298
+ if (!turnState) {
299
+ notifyByLevel('complete', buildNotifyExtra(data), getSettings());
300
+ clearRouteContext({ cwd, payload: turnPayload });
264
301
  return;
265
302
  }
266
- if (!turnState) return;
267
303
  if (turnState.kind !== 'complete') {
268
- consumeMainTurnState(cwd, turnState);
269
- clearRouteContext();
304
+ consumeMainTurnState(cwd, turnState, turnPayload);
305
+ clearRouteContext({ cwd, payload: turnPayload });
270
306
  return;
271
307
  }
272
308
 
273
309
  const settings = getSettings();
274
- if (runRalphLoop(data)) {
275
- consumeMainTurnState(cwd, turnState);
310
+ if (runRalphLoop(turnPayload, { turnState })) {
311
+ consumeMainTurnState(cwd, turnState, turnPayload);
276
312
  notifyByLevel('warning', buildNotifyExtra(data), settings);
277
313
  return;
278
314
  }
279
- if (runDeliveryGate(data)) {
280
- consumeMainTurnState(cwd, turnState);
315
+ if (runDeliveryGate(turnPayload)) {
316
+ consumeMainTurnState(cwd, turnState, turnPayload);
281
317
  notifyByLevel('warning', buildNotifyExtra(data), settings);
282
318
  return;
283
319
  }
284
320
 
285
321
  notifyByLevel('complete', buildNotifyExtra(data), settings);
286
- consumeMainTurnState(cwd, turnState);
287
- clearRouteContext();
322
+ consumeMainTurnState(cwd, turnState, turnPayload);
323
+ clearRouteContext({ cwd, payload: turnPayload });
288
324
  }
289
325
 
290
326
  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
  });