helloagents 3.0.8-beta.1 → 3.0.9-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.
@@ -1,42 +1,13 @@
1
1
  import { basename, normalize, resolve } from 'node:path'
2
2
 
3
+ import { resolveSessionToken } from './session-token.mjs'
4
+
3
5
  const HOST_LABELS = {
4
6
  codex: 'Codex',
5
7
  claude: 'Claude Code',
6
8
  gemini: 'Gemini',
7
9
  }
8
10
 
9
- const PAYLOAD_SESSION_KEYS = [
10
- 'sessionId',
11
- 'session_id',
12
- 'session',
13
- 'conversationId',
14
- 'conversation_id',
15
- 'conversation',
16
- 'threadId',
17
- 'thread_id',
18
- 'thread',
19
- 'windowId',
20
- 'window_id',
21
- 'window',
22
- 'tabId',
23
- 'tab_id',
24
- 'tab',
25
- 'requestId',
26
- 'request_id',
27
- ]
28
-
29
- const ENV_SESSION_KEYS = [
30
- 'HELLOAGENTS_NOTIFY_SESSION_ID',
31
- 'WT_SESSION',
32
- 'TERM_SESSION_ID',
33
- 'KITTY_WINDOW_ID',
34
- 'ALACRITTY_WINDOW_ID',
35
- 'WINDOWID',
36
- 'WEZTERM_PANE',
37
- 'TAB_ID',
38
- ]
39
-
40
11
  function normalizePath(filePath = '') {
41
12
  if (!filePath) return ''
42
13
  try {
@@ -61,34 +32,6 @@ function resolveProjectLabel(cwd = '') {
61
32
  return label || normalized.replace(/\\/g, '/')
62
33
  }
63
34
 
64
- function sanitizeSessionToken(value = '') {
65
- const raw = String(value).trim().replace(/^[#:\s]+/, '')
66
- const segments = raw
67
- .split(/[^a-zA-Z0-9]+/)
68
- .filter(Boolean)
69
- const cleaned = segments.length > 1
70
- ? segments[segments.length - 1]
71
- : raw.replace(/[^a-zA-Z0-9_-]/g, '')
72
-
73
- if (!cleaned) return ''
74
- if (/^\d+$/.test(cleaned)) return cleaned
75
- return cleaned.slice(0, 8)
76
- }
77
-
78
- function resolveSessionToken(payload, env, ppid) {
79
- for (const key of PAYLOAD_SESSION_KEYS) {
80
- const value = sanitizeSessionToken(readStringCandidate(payload, key))
81
- if (value) return value
82
- }
83
-
84
- for (const key of ENV_SESSION_KEYS) {
85
- const value = sanitizeSessionToken(env?.[key] || '')
86
- if (value) return value
87
- }
88
-
89
- return ppid ? String(ppid) : ''
90
- }
91
-
92
35
  export function resolveNotificationSource({
93
36
  host = '',
94
37
  cwd = '',
@@ -98,7 +41,7 @@ export function resolveNotificationSource({
98
41
  } = {}) {
99
42
  const hostLabel = HOST_LABELS[host] || 'Agent'
100
43
  const projectLabel = resolveProjectLabel(readStringCandidate(payload, 'cwd') || cwd)
101
- const sessionToken = resolveSessionToken(payload, env, ppid)
44
+ const sessionToken = resolveSessionToken({ payload, env, ppid })
102
45
  const parts = [hostLabel]
103
46
 
104
47
  if (projectLabel) parts.push(projectLabel)
@@ -115,11 +115,17 @@ function readCompletionText(payload = {}) {
115
115
  || '';
116
116
  }
117
117
 
118
- function shouldRunDeliveryGate(cwd, lastMsg) {
118
+ function readMainTurnState(cwd) {
119
119
  const turnState = readTurnState(cwd);
120
- if (turnState?.role === 'main') {
121
- return turnState.kind === 'complete';
122
- }
120
+ return turnState?.role === 'main' ? turnState : null;
121
+ }
122
+
123
+ function consumeMainTurnState(cwd, turnState) {
124
+ if (turnState?.role === 'main') clearTurnState(cwd);
125
+ }
126
+
127
+ function shouldProcessCloseout(turnState, lastMsg) {
128
+ if (turnState) return turnState.kind === 'complete';
123
129
  return claimsTaskComplete(lastMsg);
124
130
  }
125
131
 
@@ -202,6 +208,7 @@ function cmdInject() {
202
208
  pkgRoot: PKG_ROOT,
203
209
  host: HOST,
204
210
  cwd,
211
+ payload,
205
212
  });
206
213
  clearRouteContext();
207
214
  clearTurnState(cwd);
@@ -212,13 +219,17 @@ function cmdStop() {
212
219
  const payload = readStdinJson();
213
220
  const lastMsg = readCompletionText(payload);
214
221
  const cwd = payload.cwd || process.cwd();
222
+ const turnState = readMainTurnState(cwd);
223
+ const shouldProcess = shouldProcessCloseout(turnState, lastMsg);
215
224
  clearRouteContext();
216
- if (runRalphLoop(payload)) {
225
+ if (shouldProcess && runRalphLoop(payload)) {
226
+ consumeMainTurnState(cwd, turnState);
217
227
  playSound('warning');
218
228
  desktopNotify('warning', buildNotifyExtra(payload));
219
229
  return;
220
230
  }
221
- if (shouldRunDeliveryGate(cwd, lastMsg) && runDeliveryGate(payload)) {
231
+ if (shouldProcess && runDeliveryGate(payload)) {
232
+ consumeMainTurnState(cwd, turnState);
222
233
  playSound('warning');
223
234
  desktopNotify('warning', buildNotifyExtra(payload));
224
235
  return;
@@ -226,8 +237,11 @@ function cmdStop() {
226
237
 
227
238
  const settings = getSettings();
228
239
  const level = settings.notify_level ?? 0;
229
- if (level === 2 || level === 3) playSound('complete');
230
- if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(payload));
240
+ if (shouldProcess) {
241
+ if (level === 2 || level === 3) playSound('complete');
242
+ if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(payload));
243
+ }
244
+ consumeMainTurnState(cwd, turnState);
231
245
  emptySuppress();
232
246
  }
233
247
 
@@ -255,16 +269,22 @@ function cmdCodexNotify() {
255
269
  if (type !== 'agent-turn-complete') return;
256
270
 
257
271
  const cwd = data.cwd || process.cwd();
258
- const turnState = readTurnState(cwd);
259
- if (!turnState || turnState.role !== 'main') return;
272
+ const turnState = readMainTurnState(cwd);
273
+ if (!turnState) return;
274
+ if (turnState.kind !== 'complete') {
275
+ consumeMainTurnState(cwd, turnState);
276
+ return;
277
+ }
260
278
 
261
279
  const settings = getSettings();
262
- if (turnState.kind === 'complete' && runRalphLoop({ cwd })) {
280
+ if (runRalphLoop(data)) {
281
+ consumeMainTurnState(cwd, turnState);
263
282
  playSound('warning');
264
283
  desktopNotify('warning', buildNotifyExtra(data));
265
284
  return;
266
285
  }
267
- if (turnState.kind === 'complete' && runDeliveryGate({ cwd })) {
286
+ if (runDeliveryGate(data)) {
287
+ consumeMainTurnState(cwd, turnState);
268
288
  playSound('warning');
269
289
  desktopNotify('warning', buildNotifyExtra(data));
270
290
  return;
@@ -273,6 +293,7 @@ function cmdCodexNotify() {
273
293
  const level = settings.notify_level ?? 0;
274
294
  if (level === 2 || level === 3) playSound('complete');
275
295
  if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(data));
296
+ consumeMainTurnState(cwd, turnState);
276
297
  }
277
298
 
278
299
  const cmd = process.argv[2] || '';
@@ -5,10 +5,13 @@ 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
9
 
9
10
  export const PROJECT_DIR_NAME = '.helloagents'
10
11
  const PROJECTS_DIR_NAME = 'projects'
12
+ const PROJECT_SESSIONS_DIR_NAME = 'sessions'
11
13
  const PROJECT_STORE_MODES = new Set(['local', 'repo-shared'])
14
+ const DEFAULT_STATE_SESSION_TOKEN = 'default'
12
15
 
13
16
  function safeJson(filePath) {
14
17
  try {
@@ -31,6 +34,19 @@ function runGitRevParse(cwd, args = []) {
31
34
  }
32
35
  }
33
36
 
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
+
34
50
  function resolveGitTopLevel(cwd) {
35
51
  const absolute = runGitRevParse(cwd, ['--path-format=absolute', '--show-toplevel'])
36
52
  if (absolute) return normalize(absolute)
@@ -54,6 +70,16 @@ function sanitizeRepoName(value = '') {
54
70
  return normalized || 'project'
55
71
  }
56
72
 
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
+
57
83
  function buildProjectKey(cwd) {
58
84
  const repoRoot = resolveGitTopLevel(cwd)
59
85
  const commonDir = resolveGitCommonDir(cwd, repoRoot)
@@ -87,6 +113,15 @@ function formatPromptPath(pathValue = '') {
87
113
  return pathValue ? normalize(pathValue).replace(/\\/g, '/') : ''
88
114
  }
89
115
 
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
+
90
125
  export function normalizeProjectStoreMode(value) {
91
126
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
92
127
  return PROJECT_STORE_MODES.has(normalized) ? normalized : DEFAULTS.project_store_mode
@@ -105,8 +140,38 @@ export function getProjectActivationDir(cwd) {
105
140
  return join(cwd, PROJECT_DIR_NAME)
106
141
  }
107
142
 
108
- export function getProjectStatePath(cwd) {
109
- return join(getProjectActivationDir(cwd), 'STATE.md')
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
+ )
162
+
163
+ return {
164
+ stateScope: 'session',
165
+ stateSessionToken: sessionToken,
166
+ stateSessionMode: rawSessionToken ? 'host-session' : 'default',
167
+ stateBranch: branchName,
168
+ sessionDir,
169
+ statePath: join(sessionDir, 'STATE.md'),
170
+ }
171
+ }
172
+
173
+ export function getProjectStatePath(cwd, options = {}) {
174
+ return getProjectSessionStateScope(cwd, options).statePath
110
175
  }
111
176
 
112
177
  export function isRepoSharedProjectStore(cwd) {
@@ -122,10 +187,10 @@ export function getProjectStoreDir(cwd) {
122
187
  return join(homedir(), PROJECT_DIR_NAME, PROJECTS_DIR_NAME, projectKey.key)
123
188
  }
124
189
 
125
- export function getProjectStoreSummary(cwd) {
190
+ export function getProjectStoreSummary(cwd, options = {}) {
126
191
  const activationDir = getProjectActivationDir(cwd)
127
192
  const storeDir = getProjectStoreDir(cwd)
128
- const statePath = getProjectStatePath(cwd)
193
+ const stateScope = getProjectSessionStateScope(cwd, options)
129
194
  const projectKey = buildProjectKey(cwd)
130
195
  const projectStoreMode = getProjectStoreMode(cwd)
131
196
 
@@ -133,14 +198,20 @@ export function getProjectStoreSummary(cwd) {
133
198
  projectStoreMode,
134
199
  activationDir,
135
200
  storeDir,
136
- statePath,
201
+ statePath: stateScope.statePath,
202
+ stateScope: stateScope.stateScope,
203
+ stateSessionToken: stateScope.stateSessionToken,
204
+ stateSessionMode: stateScope.stateSessionMode,
205
+ stateBranch: stateScope.stateBranch,
206
+ sessionStateDir: stateScope.sessionDir,
137
207
  usesSharedStore: projectStoreMode === 'repo-shared',
138
208
  projectKey: projectKey.key,
139
209
  repoRoot: projectKey.repoRoot,
140
210
  commonDir: projectKey.commonDir,
141
211
  promptActivationDir: formatPromptPath(activationDir),
142
212
  promptStoreDir: formatPromptPath(storeDir),
143
- promptStatePath: formatPromptPath(statePath),
213
+ promptStatePath: formatPromptPath(stateScope.statePath),
214
+ promptSessionStateDir: formatPromptPath(stateScope.sessionDir),
144
215
  }
145
216
  }
146
217
 
@@ -203,14 +274,21 @@ export function describeProjectStoreFile(cwd, relativePath = '') {
203
274
  return `逻辑路径 \`${logicalPath}\`(实际存储:\`${actualPath}\`)`
204
275
  }
205
276
 
206
- export function buildProjectStorageHint(cwd) {
207
- const summary = getProjectStoreSummary(cwd)
208
- if (!summary.usesSharedStore) return ''
209
- return `项目存储:\`project_store_mode=repo-shared\`;本地激活/运行态目录仍是 \`${summary.promptActivationDir}\`,知识库/方案目录改为 \`${summary.promptStoreDir}\`。`
277
+ export function buildProjectStorageHint(cwd, options = {}) {
278
+ const summary = getProjectStoreSummary(cwd, options)
279
+ const hints = []
280
+ hints.push(`当前恢复快照统一写入 \`${summary.promptStatePath}\``)
281
+ if (summary.stateSessionMode === 'default') {
282
+ hints.push(`当前宿主未提供稳定会话标识,因此落到分支级默认会话槽位 \`${summary.stateSessionToken}\``)
283
+ }
284
+ if (summary.usesSharedStore) {
285
+ hints.push(`项目存储:\`project_store_mode=repo-shared\`;本地激活/运行态目录仍是 \`${summary.promptActivationDir}\`,知识库/方案目录改为 \`${summary.promptStoreDir}\``)
286
+ }
287
+ return hints.join('。') + (hints.length > 0 ? '。' : '')
210
288
  }
211
289
 
212
- export function buildProjectStorageBlock(cwd) {
213
- const summary = getProjectStoreSummary(cwd)
290
+ export function buildProjectStorageBlock(cwd, options = {}) {
291
+ const summary = getProjectStoreSummary(cwd, options)
214
292
  if (!summary.usesSharedStore && !existsSync(summary.activationDir)) {
215
293
  return ''
216
294
  }
@@ -218,18 +296,32 @@ export function buildProjectStorageBlock(cwd) {
218
296
  const details = {
219
297
  project_store_mode: summary.projectStoreMode,
220
298
  activation_dir: summary.promptActivationDir,
299
+ state_scope: summary.stateScope,
221
300
  state_path: summary.promptStatePath,
301
+ state_branch: summary.stateBranch,
302
+ state_session_token: summary.stateSessionToken,
303
+ state_session_mode: summary.stateSessionMode,
304
+ session_state_dir: summary.promptSessionStateDir,
222
305
  knowledge_base_dir: summary.promptStoreDir,
223
306
  uses_shared_store: summary.usesSharedStore,
224
307
  }
225
308
 
309
+ const explanations = []
310
+ explanations.push('说明:恢复快照只认 `state_path` 这一个权威路径,不再读写旧的项目级 `.helloagents/STATE.md`。')
311
+ if (summary.stateSessionMode === 'default') {
312
+ explanations.push('说明:当前宿主未提供稳定会话标识,因此自动使用分支级默认会话槽位,仍保持新目录结构。')
313
+ }
314
+ if (summary.usesSharedStore) {
315
+ explanations.push('说明:`STATE.md` 与 `.ralph-*.json` 继续写本地激活目录;`context.md`、`guidelines.md`、`DESIGN.md`、`verify.yaml`、`modules/`、`plans/`、`archive/` 写知识库/方案目录。')
316
+ } else {
317
+ explanations.push('说明:当前使用项目本地 `.helloagents/` 作为激活目录、知识库目录和方案目录。')
318
+ }
319
+
226
320
  return [
227
321
  '## 当前项目存储',
228
322
  '```json',
229
323
  JSON.stringify(details, null, 2),
230
324
  '```',
231
- summary.usesSharedStore
232
- ? '说明:`STATE.md` 与 `.ralph-*.json` 继续写本地激活目录;`context.md`、`guidelines.md`、`DESIGN.md`、`verify.yaml`、`modules/`、`plans/`、`archive/` 写知识库/方案目录。'
233
- : '说明:当前使用项目本地 `.helloagents/` 作为激活目录、知识库目录和方案目录。',
325
+ ...explanations,
234
326
  ].join('\n')
235
327
  }
@@ -0,0 +1,73 @@
1
+ const PAYLOAD_SESSION_KEYS = [
2
+ 'sessionId',
3
+ 'session_id',
4
+ 'session',
5
+ 'conversationId',
6
+ 'conversation_id',
7
+ 'conversation',
8
+ 'threadId',
9
+ 'thread_id',
10
+ 'thread',
11
+ 'windowId',
12
+ 'window_id',
13
+ 'window',
14
+ 'tabId',
15
+ 'tab_id',
16
+ 'tab',
17
+ 'requestId',
18
+ 'request_id',
19
+ ]
20
+
21
+ const ENV_SESSION_KEYS = [
22
+ 'HELLOAGENTS_NOTIFY_SESSION_ID',
23
+ 'WT_SESSION',
24
+ 'TERM_SESSION_ID',
25
+ 'KITTY_WINDOW_ID',
26
+ 'ALACRITTY_WINDOW_ID',
27
+ 'WINDOWID',
28
+ 'WEZTERM_PANE',
29
+ 'TAB_ID',
30
+ ]
31
+
32
+ function readStringCandidate(input, key) {
33
+ if (!input || typeof input !== 'object') return ''
34
+ const value = input[key]
35
+ if (typeof value === 'string') return value.trim()
36
+ if (typeof value === 'number') return String(value)
37
+ return ''
38
+ }
39
+
40
+ export function sanitizeSessionToken(value = '') {
41
+ const raw = String(value).trim().replace(/^[#:\s]+/, '')
42
+ const segments = raw
43
+ .split(/[^a-zA-Z0-9]+/)
44
+ .filter(Boolean)
45
+ const cleaned = segments.length > 1
46
+ ? segments[segments.length - 1]
47
+ : raw.replace(/[^a-zA-Z0-9_-]/g, '')
48
+
49
+ if (!cleaned) return ''
50
+ if (/^\d+$/.test(cleaned)) return cleaned
51
+ return cleaned.slice(0, 8)
52
+ }
53
+
54
+ export function resolveSessionToken({
55
+ payload = {},
56
+ env = process.env,
57
+ ppid = process.ppid,
58
+ allowPpidFallback = true,
59
+ } = {}) {
60
+ for (const key of PAYLOAD_SESSION_KEYS) {
61
+ const value = sanitizeSessionToken(readStringCandidate(payload, key))
62
+ if (value) return value
63
+ }
64
+
65
+ for (const key of ENV_SESSION_KEYS) {
66
+ const value = sanitizeSessionToken(env?.[key] || '')
67
+ if (value) return value
68
+ }
69
+
70
+ return allowPpidFallback && ppid ? String(ppid) : ''
71
+ }
72
+
73
+ export { ENV_SESSION_KEYS, PAYLOAD_SESSION_KEYS }
@@ -13,6 +13,13 @@ import { describeProjectStoreFile, getProjectDesignContractPath } from './projec
13
13
  export function getTargetPlans(snapshot) {
14
14
  return snapshot.activePlans.length > 0 ? snapshot.activePlans : snapshot.plans
15
15
  }
16
+
17
+ function describeStateLabel(state) {
18
+ if (state.stateSessionMode === 'default') {
19
+ return '当前分支默认会话槽位的 `STATE.md`'
20
+ }
21
+ return '当前会话的 `STATE.md`'
22
+ }
16
23
  export function classifyPlan(plan) {
17
24
  if (!plan) {
18
25
  return {
@@ -97,27 +104,28 @@ function collectStateSyncIssues(snapshot) {
97
104
  const issues = []
98
105
  const hasPlans = snapshot.plans.length > 0
99
106
  const state = snapshot.state
107
+ const stateLabel = describeStateLabel(state)
100
108
 
101
109
  if (!hasPlans) {
102
110
  return issues
103
111
  }
104
112
 
105
113
  if (!state.exists) {
106
- issues.push('当前已存在方案包,但 `.helloagents/STATE.md` 缺失')
114
+ issues.push(`当前已存在方案包,但${stateLabel} 缺失`)
107
115
  return issues
108
116
  }
109
117
 
110
118
  if (!state.referencedPlanDir) {
111
- issues.push('当前已存在方案包,但 `STATE.md` 未记录活跃方案路径')
119
+ issues.push(`${stateLabel} 未记录活跃方案路径`)
112
120
  }
113
121
  if (!state.sections['主线目标']) {
114
- issues.push('`STATE.md` 缺少“主线目标”')
122
+ issues.push(`${stateLabel} 缺少“主线目标”`)
115
123
  }
116
124
  if (!state.sections['正在做什么']) {
117
- issues.push('`STATE.md` 缺少“正在做什么”')
125
+ issues.push(`${stateLabel} 缺少“正在做什么”`)
118
126
  }
119
127
  if (!state.sections['下一步']) {
120
- issues.push('`STATE.md` 缺少“下一步”')
128
+ issues.push(`${stateLabel} 缺少“下一步”`)
121
129
  }
122
130
 
123
131
  return issues
@@ -137,7 +145,7 @@ export function buildStateSyncHintFromSnapshot(snapshot) {
137
145
 
138
146
  export function buildStateRoleHintFromSnapshot(snapshot) {
139
147
  if (!snapshot.state.exists || snapshot.plans.length > 0) return ''
140
- return '恢复约束:当前仅检测到 `.helloagents/STATE.md`;先以当前用户消息、显式命令和代码事实确认主线,STATE.md 只用于找回上次停在哪,不是当前任务的自动授权或唯一判断依据。'
148
+ return `恢复约束:当前仅检测到${describeStateLabel(snapshot.state)};先以当前用户消息、显式命令和代码事实确认主线,STATE.md 只用于找回上次停在哪,不是当前任务的自动授权或唯一判断依据。`
141
149
  }
142
150
 
143
151
  export function buildUiContractHint(cwd, snapshot) {
@@ -2,7 +2,11 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
2
  import { isAbsolute, join, normalize } from 'node:path'
3
3
 
4
4
  import { getPlanContractIssues, readPlanContract } from './plan-contract.mjs'
5
- import { getProjectPlansDir, getProjectStatePath, resolveProjectPlanDir } from './project-storage.mjs'
5
+ import {
6
+ getProjectPlansDir,
7
+ getProjectSessionStateScope,
8
+ resolveProjectPlanDir,
9
+ } from './project-storage.mjs'
6
10
 
7
11
  const PLAN_TEMPLATE_MARKERS = {
8
12
  'requirements.md': [
@@ -170,15 +174,22 @@ function comparePlanEntries(a, b) {
170
174
  return a.planName.localeCompare(b.planName)
171
175
  }
172
176
 
173
- export function readStateSnapshot(cwd) {
174
- const statePath = getProjectStatePath(cwd)
177
+ export function readStateSnapshot(cwd, options = {}) {
178
+ const stateScope = getProjectSessionStateScope(cwd, options)
179
+ const statePath = stateScope.statePath
180
+ const exists = existsSync(statePath)
175
181
  const content = readText(statePath)
176
182
  const sections = parseMarkdownSections(content)
177
183
  const referencedPlanDir = resolvePlanDir(cwd, sections['方案'])
178
184
 
179
185
  return {
180
186
  statePath,
181
- exists: existsSync(statePath),
187
+ stateScope: stateScope.stateScope,
188
+ stateSessionToken: stateScope.stateSessionToken,
189
+ stateSessionMode: stateScope.stateSessionMode,
190
+ stateBranch: stateScope.stateBranch,
191
+ sessionScoped: stateScope.stateScope === 'session',
192
+ exists,
182
193
  content,
183
194
  sections,
184
195
  referencedPlanDir,
@@ -230,8 +241,8 @@ export function listPlanPackages(cwd) {
230
241
  .sort(comparePlanEntries)
231
242
  }
232
243
 
233
- export function getWorkflowSnapshot(cwd) {
234
- const state = readStateSnapshot(cwd)
244
+ export function getWorkflowSnapshot(cwd, options = {}) {
245
+ const state = readStateSnapshot(cwd, options)
235
246
  const plans = listPlanPackages(cwd).map((entry) => ({
236
247
  ...entry,
237
248
  referencedByState: state.referencedPlanDir ? normalize(entry.dirPath) === normalize(state.referencedPlanDir) : false,
@@ -16,27 +16,27 @@ import {
16
16
  buildRecommendation,
17
17
  } from './workflow-recommendation.mjs'
18
18
 
19
- export function getDeliveryAction(cwd) {
20
- const snapshot = getWorkflowSnapshot(cwd)
19
+ export function getDeliveryAction(cwd, options = {}) {
20
+ const snapshot = getWorkflowSnapshot(cwd, options)
21
21
  const recommendation = buildRecommendation(snapshot, cwd)
22
22
  return buildDeliveryActionFromSnapshot(snapshot, cwd, recommendation)
23
23
  }
24
24
 
25
- export function getWorkflowRecommendation(cwd) {
26
- return buildRecommendation(getWorkflowSnapshot(cwd), cwd)
25
+ export function getWorkflowRecommendation(cwd, options = {}) {
26
+ return buildRecommendation(getWorkflowSnapshot(cwd, options), cwd)
27
27
  }
28
28
 
29
- export function buildStateSyncHint(cwd) {
30
- return buildStateSyncHintFromSnapshot(getWorkflowSnapshot(cwd))
29
+ export function buildStateSyncHint(cwd, options = {}) {
30
+ return buildStateSyncHintFromSnapshot(getWorkflowSnapshot(cwd, options))
31
31
  }
32
32
 
33
- export function buildDeliveryGateHint(cwd) {
34
- const snapshot = getWorkflowSnapshot(cwd)
33
+ export function buildDeliveryGateHint(cwd, options = {}) {
34
+ const snapshot = getWorkflowSnapshot(cwd, options)
35
35
  return buildDeliveryGateHintFromSnapshot(snapshot, cwd, buildRecommendation(snapshot, cwd))
36
36
  }
37
37
 
38
- export function buildWorkflowRouteHint(cwd) {
39
- const snapshot = getWorkflowSnapshot(cwd)
38
+ export function buildWorkflowRouteHint(cwd, options = {}) {
39
+ const snapshot = getWorkflowSnapshot(cwd, options)
40
40
  const recommendation = buildRecommendation(snapshot, cwd)
41
41
  const stateSyncHint = buildStateSyncHintFromSnapshot(snapshot)
42
42
  const stateRoleHint = buildStateRoleHintFromSnapshot(snapshot)
@@ -87,8 +87,8 @@ function buildCommandRouteMessage(skillName, recommendation, verifyModeHint) {
87
87
  return `当前工作流约束:${recommendation.summary} 当前建议下一命令:~${recommendation.nextCommand}。${recommendation.guidance}`
88
88
  }
89
89
 
90
- export function buildCommandRouteHint(skillName, cwd) {
91
- const snapshot = getWorkflowSnapshot(cwd)
90
+ export function buildCommandRouteHint(skillName, cwd, options = {}) {
91
+ const snapshot = getWorkflowSnapshot(cwd, options)
92
92
  const recommendation = buildRecommendation(snapshot, cwd)
93
93
  const contextHints = [
94
94
  buildStateRoleHintFromSnapshot(snapshot),
@@ -8,7 +8,7 @@ Trigger: ~build [description]
8
8
 
9
9
  `~build` 是执行实现命令。它负责读取现有需求、方案包与项目上下文,完成实现、局部验证、修复循环,并把结果衔接到后续验证与收尾。
10
10
  执行 `~build` 时,通用阶段边界按当前已加载 bootstrap 执行;本 skill 负责补充实现前定位、实现约束,以及进入 `~verify` / 收尾前的实现边界。
11
- `.helloagents/` 在本 skill 中统一按项目级存储路径理解:`STATE.md` 与 `.ralph-*.json` 保持项目本地;若 `project_store_mode=repo-shared`,知识库、`DESIGN.md`、`verify.yaml` 与方案包按当前上下文中已注入的项目知识/方案目录解析。
11
+ `.helloagents/` 在本 skill 中统一按项目级存储路径理解:`STATE.md` 与 `.ralph-*.json` 保持项目本地;若当前上下文中的“当前项目存储”给出 `state_path`,本轮恢复快照统一读写该路径;若 `project_store_mode=repo-shared`,知识库、`DESIGN.md`、`verify.yaml` 与方案包按当前上下文中已注入的项目知识/方案目录解析。
12
12
 
13
13
  ## 铁律
14
14
  - 默认先定位上下文与范围,再修改代码
@@ -20,7 +20,7 @@ Trigger: ~build [description]
20
20
 
21
21
  ### 1. 恢复与定位
22
22
 
23
- - 优先按当前已加载 bootstrap 的“.helloagents/ 文件读取优先级”恢复当前链路;若当前消息显式继续既有链路,或会话刚经历恢复 / 压缩,先读取 `.helloagents/STATE.md` 作为恢复快照,再用当前用户消息、活跃方案包 / PRD 与代码事实校正主线
23
+ - 优先按当前已加载 bootstrap 的“.helloagents/ 文件读取优先级”恢复当前链路;若当前消息显式继续既有链路,或会话刚经历恢复 / 压缩,先读取 `.helloagents/STATE.md` 作为恢复快照(若当前项目存储给出 `state_path`,则优先读取该当前会话的 `STATE.md`),再用当前用户消息、活跃方案包 / PRD 与代码事实校正主线
24
24
  - 若存在最近的活跃方案包,读取对应的:
25
25
  - `requirements.md`
26
26
  - `plan.md`
@@ -7,7 +7,7 @@ policy:
7
7
  Trigger: ~clean
8
8
 
9
9
  执行 `~clean` 时,方案包归档、临时文件清理和 `STATE.md` 更新边界按当前已加载 bootstrap 执行;本命令只负责判定哪些方案包可以清理,以及输出清理摘要。
10
- `.helloagents/` 在本 skill 中统一按项目级存储路径理解:`STATE.md` 和临时运行态文件保持项目本地;若 `project_store_mode=repo-shared`,`plans/` 与 `archive/` 按当前上下文中已注入的项目知识/方案目录解析。
10
+ `.helloagents/` 在本 skill 中统一按项目级存储路径理解:`STATE.md` 和临时运行态文件保持项目本地;若当前上下文中的“当前项目存储”给出 `state_path`,本轮恢复快照统一读写该路径;若 `project_store_mode=repo-shared`,`plans/` 与 `archive/` 按当前上下文中已注入的项目知识/方案目录解析。
11
11
 
12
12
  ## 流程
13
13
 
@@ -20,7 +20,7 @@ Trigger: ~commit [message]
20
20
  - ""(空,默认)→ 不添加归属
21
21
  - 有内容(如 "Co-Authored-By: HelloAGENTS")→ 添加该内容到 commit message
22
22
  6. 执行 git commit
23
- 7. 若项目已有 `.helloagents/STATE.md`,按 bootstrap 的“已有则更新”规则同步当前已提交状态
23
+ 7. 若当前 `STATE.md` 已存在(优先取当前项目存储中的 `state_path`,未注入时回退 `.helloagents/STATE.md`),按 bootstrap 的“已有则更新”规则同步当前已提交状态
24
24
 
25
25
  ## 知识库同步
26
26
  提交后,继续复用上方已解析的同一份设置获取 `kb_create_mode`,不要再次读取 `~/.helloagents/helloagents.json`:
@@ -11,7 +11,7 @@ Trigger: ~idea [description]
11
11
  ## 铁律
12
12
  - 只讨论,不编写实现代码,不创建项目文件,不执行实现操作
13
13
  - 不创建 `.helloagents/`
14
- - 不创建或更新 `.helloagents/STATE.md`、知识库文件、方案包或项目级规则文件
14
+ - 不创建或更新 `.helloagents/STATE.md`;若当前项目存储给出 `state_path`,同样禁止更新该当前 `STATE.md`、知识库文件、方案包或项目级规则文件
15
15
  - 不生成方案包
16
16
  - 不执行会改变工作区或外部状态的命令
17
17
  - 不默认使用子代理