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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +6 -6
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +8 -8
- package/README_CN.md +8 -8
- package/bootstrap-lite.md +4 -3
- package/bootstrap.md +5 -4
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/scripts/capability-registry.mjs +5 -5
- package/scripts/cli-codex.mjs +2 -0
- package/scripts/cli-doctor.mjs +6 -1
- package/scripts/cli-host-detect.mjs +18 -2
- package/scripts/delivery-gate.mjs +5 -4
- package/scripts/guard.mjs +5 -4
- package/scripts/notify-context.mjs +28 -26
- package/scripts/notify-route.mjs +5 -2
- package/scripts/notify-source.mjs +3 -60
- package/scripts/notify.mjs +33 -12
- package/scripts/project-storage.mjs +107 -15
- package/scripts/session-token.mjs +73 -0
- package/scripts/workflow-core.mjs +14 -6
- package/scripts/workflow-plan-files.mjs +17 -6
- package/scripts/workflow-state.mjs +12 -12
- package/skills/commands/build/SKILL.md +2 -2
- package/skills/commands/clean/SKILL.md +1 -1
- package/skills/commands/commit/SKILL.md +1 -1
- package/skills/commands/idea/SKILL.md +1 -1
- package/skills/commands/init/SKILL.md +2 -2
- package/skills/commands/loop/SKILL.md +4 -4
- package/skills/commands/plan/SKILL.md +3 -3
- package/skills/commands/prd/SKILL.md +3 -3
- package/skills/commands/wiki/SKILL.md +2 -2
- package/skills/hello-ui/SKILL.md +1 -1
- package/skills/helloagents/SKILL.md +1 -1
|
@@ -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)
|
package/scripts/notify.mjs
CHANGED
|
@@ -115,11 +115,17 @@ function readCompletionText(payload = {}) {
|
|
|
115
115
|
|| '';
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
function
|
|
118
|
+
function readMainTurnState(cwd) {
|
|
119
119
|
const turnState = readTurnState(cwd);
|
|
120
|
-
|
|
121
|
-
|
|
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 (
|
|
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 (
|
|
230
|
-
|
|
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 =
|
|
259
|
-
if (!turnState
|
|
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 (
|
|
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 (
|
|
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
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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(
|
|
114
|
+
issues.push(`当前已存在方案包,但${stateLabel} 缺失`)
|
|
107
115
|
return issues
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
if (!state.referencedPlanDir) {
|
|
111
|
-
issues.push(
|
|
119
|
+
issues.push(`${stateLabel} 未记录活跃方案路径`)
|
|
112
120
|
}
|
|
113
121
|
if (!state.sections['主线目标']) {
|
|
114
|
-
issues.push(
|
|
122
|
+
issues.push(`${stateLabel} 缺少“主线目标”`)
|
|
115
123
|
}
|
|
116
124
|
if (!state.sections['正在做什么']) {
|
|
117
|
-
issues.push(
|
|
125
|
+
issues.push(`${stateLabel} 缺少“正在做什么”`)
|
|
118
126
|
}
|
|
119
127
|
if (!state.sections['下一步']) {
|
|
120
|
-
issues.push(
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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`
|
|
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`
|
|
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`
|
|
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.
|
|
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
|
- 不默认使用子代理
|