helloagents 3.0.8-beta.1 → 3.0.10-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 +18 -9
- package/README_CN.md +42 -33
- package/bootstrap-lite.md +18 -17
- package/bootstrap.md +22 -21
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/scripts/capability-registry.mjs +5 -5
- package/scripts/cli-codex-config.mjs +8 -2
- package/scripts/cli-codex.mjs +7 -3
- package/scripts/cli-doctor.mjs +6 -1
- package/scripts/cli-host-detect.mjs +18 -2
- package/scripts/cli-lifecycle.mjs +11 -0
- package/scripts/cli-messages.mjs +3 -3
- package/scripts/cli-toml-values.mjs +25 -0
- package/scripts/cli-toml.mjs +15 -0
- package/scripts/delivery-gate.mjs +5 -4
- package/scripts/guard.mjs +7 -6
- package/scripts/notify-context.mjs +32 -30
- package/scripts/notify-events.mjs +0 -8
- package/scripts/notify-gates.mjs +128 -0
- package/scripts/notify-route.mjs +5 -2
- package/scripts/notify-source.mjs +3 -60
- package/scripts/notify-ui.mjs +3 -0
- package/scripts/notify.mjs +72 -83
- package/scripts/project-storage.mjs +107 -15
- package/scripts/session-token.mjs +73 -0
- package/scripts/workflow-core.mjs +16 -8
- package/scripts/workflow-plan-files.mjs +17 -6
- package/scripts/workflow-recommendation.mjs +4 -4
- package/scripts/workflow-state.mjs +13 -13
- package/skills/commands/auto/SKILL.md +13 -13
- package/skills/commands/build/SKILL.md +5 -5
- package/skills/commands/clean/SKILL.md +6 -6
- package/skills/commands/commit/SKILL.md +2 -2
- package/skills/commands/help/SKILL.md +2 -2
- package/skills/commands/idea/SKILL.md +5 -5
- package/skills/commands/init/SKILL.md +4 -4
- package/skills/commands/loop/SKILL.md +4 -4
- package/skills/commands/plan/SKILL.md +13 -13
- package/skills/commands/prd/SKILL.md +13 -13
- package/skills/commands/verify/SKILL.md +3 -3
- package/skills/commands/wiki/SKILL.md +5 -5
- package/skills/hello-subagent/SKILL.md +1 -1
- package/skills/hello-ui/SKILL.md +3 -3
- package/skills/helloagents/SKILL.md +3 -2
- package/templates/plans/contract.json +2 -2
|
@@ -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-ui.mjs
CHANGED
|
@@ -18,6 +18,7 @@ const NOTIFY_MESSAGES = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const WIN_APPID = 'HelloAgents.Notification';
|
|
21
|
+
const DISABLE_OS_NOTIFICATIONS = process.env.HELLOAGENTS_DISABLE_OS_NOTIFICATIONS === '1';
|
|
21
22
|
|
|
22
23
|
function escapeToastText(value = '') {
|
|
23
24
|
return String(value)
|
|
@@ -55,6 +56,7 @@ function resolveWav(pkgRoot, event) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export function playSound(pkgRoot, event) {
|
|
59
|
+
if (DISABLE_OS_NOTIFICATIONS) return;
|
|
58
60
|
const wav = resolveWav(pkgRoot, event);
|
|
59
61
|
if (!wav) { process.stderr.write('\x07'); return; }
|
|
60
62
|
try {
|
|
@@ -83,6 +85,7 @@ function ensureWinAppId(pkgRoot) {
|
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
export function desktopNotify(pkgRoot, event, extra) {
|
|
88
|
+
if (DISABLE_OS_NOTIFICATIONS) return;
|
|
86
89
|
const notification = buildDesktopNotificationContent(event, extra);
|
|
87
90
|
try {
|
|
88
91
|
if (PLAT === 'win32') {
|
package/scripts/notify.mjs
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
-
import { spawnSync } from 'node:child_process';
|
|
8
7
|
import { fileURLToPath } from 'node:url';
|
|
9
8
|
import { homedir } from 'node:os';
|
|
10
9
|
import { playSound as _playSound, desktopNotify as _desktopNotify } from './notify-ui.mjs';
|
|
11
10
|
import { resolveNotificationSource } from './notify-source.mjs';
|
|
12
11
|
import { buildCompactionContext, buildInjectContext, buildRouteInstruction, buildSemanticRouteInstruction, resolveCanonicalCommandSkill } from './notify-context.mjs';
|
|
13
|
-
import {
|
|
12
|
+
import { shouldIgnoreCodexNotifyClient } from './notify-events.mjs';
|
|
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
16
|
import { clearRouteContext, writeRouteContext } from './runtime-context.mjs';
|
|
@@ -37,6 +37,17 @@ const EVENT_NAME = {
|
|
|
37
37
|
const playSound = (event) => _playSound(PKG_ROOT, event);
|
|
38
38
|
const desktopNotify = (event, extra) => _desktopNotify(PKG_ROOT, event, extra);
|
|
39
39
|
|
|
40
|
+
function normalizeNotifyLevel(value) {
|
|
41
|
+
const level = Number(value);
|
|
42
|
+
return [0, 1, 2, 3].includes(level) ? level : 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function notifyByLevel(event, extra, settings = getSettings()) {
|
|
46
|
+
const level = normalizeNotifyLevel(settings.notify_level ?? 0);
|
|
47
|
+
if (level === 2 || level === 3) playSound(event);
|
|
48
|
+
if (level === 1 || level === 3) desktopNotify(event, extra);
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
function buildNotifyExtra(payload = {}, options = {}) {
|
|
41
52
|
const source = resolveNotificationSource({
|
|
42
53
|
host: HOST,
|
|
@@ -56,71 +67,44 @@ function getSettings() {
|
|
|
56
67
|
function runRalphLoop(payload) {
|
|
57
68
|
const settings = getSettings();
|
|
58
69
|
if (settings.ralph_loop_enabled === false) return false;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (rlOut.decision === 'block') {
|
|
71
|
-
appendReplayEvent(payload.cwd || process.cwd(), {
|
|
72
|
-
host: HOST,
|
|
73
|
-
event: 'verify_gate_blocked',
|
|
74
|
-
source: 'ralph-loop',
|
|
75
|
-
reason: rlOut.reason || '',
|
|
76
|
-
});
|
|
77
|
-
output(rlOut);
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
} catch {}
|
|
82
|
-
return false;
|
|
70
|
+
return runGateScript({
|
|
71
|
+
payload,
|
|
72
|
+
host: HOST,
|
|
73
|
+
scriptPath: join(__dirname, 'ralph-loop.mjs'),
|
|
74
|
+
args: IS_GEMINI ? ['--gemini'] : HOST === 'codex' ? ['--codex'] : [],
|
|
75
|
+
source: 'ralph-loop',
|
|
76
|
+
blockEvent: 'verify_gate_blocked',
|
|
77
|
+
timeout: 120_000,
|
|
78
|
+
appendReplayEvent,
|
|
79
|
+
output,
|
|
80
|
+
});
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
function runDeliveryGate(payload) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (gateOut.decision === 'block') {
|
|
97
|
-
appendReplayEvent(payload.cwd || process.cwd(), {
|
|
98
|
-
host: HOST,
|
|
99
|
-
event: 'delivery_gate_blocked',
|
|
100
|
-
source: 'delivery-gate',
|
|
101
|
-
reason: gateOut.reason || '',
|
|
102
|
-
});
|
|
103
|
-
output(gateOut);
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} catch {}
|
|
108
|
-
return false;
|
|
84
|
+
return runGateScript({
|
|
85
|
+
payload,
|
|
86
|
+
host: HOST,
|
|
87
|
+
scriptPath: join(__dirname, 'delivery-gate.mjs'),
|
|
88
|
+
source: 'delivery-gate',
|
|
89
|
+
blockEvent: 'delivery_gate_blocked',
|
|
90
|
+
timeout: 30_000,
|
|
91
|
+
appendReplayEvent,
|
|
92
|
+
output,
|
|
93
|
+
});
|
|
109
94
|
}
|
|
110
95
|
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|| payload.lastAssistantMessage
|
|
115
|
-
|| '';
|
|
96
|
+
function readMainTurnState(cwd) {
|
|
97
|
+
const turnState = readTurnState(cwd);
|
|
98
|
+
return turnState?.role === 'main' ? turnState : null;
|
|
116
99
|
}
|
|
117
100
|
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
101
|
+
function consumeMainTurnState(cwd, turnState) {
|
|
102
|
+
if (turnState?.role === 'main') clearTurnState(cwd);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function shouldProcessCloseout(turnState) {
|
|
106
|
+
if (turnState) return turnState.kind === 'complete';
|
|
107
|
+
return false;
|
|
124
108
|
}
|
|
125
109
|
|
|
126
110
|
function cmdPreCompact() {
|
|
@@ -202,6 +186,7 @@ function cmdInject() {
|
|
|
202
186
|
pkgRoot: PKG_ROOT,
|
|
203
187
|
host: HOST,
|
|
204
188
|
cwd,
|
|
189
|
+
payload,
|
|
205
190
|
});
|
|
206
191
|
clearRouteContext();
|
|
207
192
|
clearTurnState(cwd);
|
|
@@ -210,24 +195,26 @@ function cmdInject() {
|
|
|
210
195
|
|
|
211
196
|
function cmdStop() {
|
|
212
197
|
const payload = readStdinJson();
|
|
213
|
-
const lastMsg = readCompletionText(payload);
|
|
214
198
|
const cwd = payload.cwd || process.cwd();
|
|
199
|
+
const turnState = readMainTurnState(cwd);
|
|
200
|
+
const shouldProcess = shouldProcessCloseout(turnState);
|
|
215
201
|
clearRouteContext();
|
|
216
|
-
if (runRalphLoop(payload)) {
|
|
217
|
-
|
|
218
|
-
|
|
202
|
+
if (shouldProcess && runRalphLoop(payload)) {
|
|
203
|
+
consumeMainTurnState(cwd, turnState);
|
|
204
|
+
notifyByLevel('warning', buildNotifyExtra(payload));
|
|
219
205
|
return;
|
|
220
206
|
}
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
|
|
207
|
+
if (shouldProcess && runDeliveryGate(payload)) {
|
|
208
|
+
consumeMainTurnState(cwd, turnState);
|
|
209
|
+
notifyByLevel('warning', buildNotifyExtra(payload));
|
|
224
210
|
return;
|
|
225
211
|
}
|
|
226
212
|
|
|
227
213
|
const settings = getSettings();
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
214
|
+
if (shouldProcess) {
|
|
215
|
+
notifyByLevel('complete', buildNotifyExtra(payload), settings);
|
|
216
|
+
}
|
|
217
|
+
consumeMainTurnState(cwd, turnState);
|
|
231
218
|
emptySuppress();
|
|
232
219
|
}
|
|
233
220
|
|
|
@@ -248,31 +235,33 @@ function cmdCodexNotify() {
|
|
|
248
235
|
if (shouldIgnoreCodexNotifyClient(client)) return;
|
|
249
236
|
|
|
250
237
|
if (type === 'approval-requested') {
|
|
251
|
-
|
|
252
|
-
desktopNotify('confirm', buildNotifyExtra(data));
|
|
238
|
+
notifyByLevel('confirm', buildNotifyExtra(data));
|
|
253
239
|
return;
|
|
254
240
|
}
|
|
255
241
|
if (type !== 'agent-turn-complete') return;
|
|
256
242
|
|
|
257
243
|
const cwd = data.cwd || process.cwd();
|
|
258
|
-
const turnState =
|
|
259
|
-
if (!turnState
|
|
244
|
+
const turnState = readMainTurnState(cwd);
|
|
245
|
+
if (!turnState) return;
|
|
246
|
+
if (turnState.kind !== 'complete') {
|
|
247
|
+
consumeMainTurnState(cwd, turnState);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
260
250
|
|
|
261
251
|
const settings = getSettings();
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
252
|
+
if (runRalphLoop(data)) {
|
|
253
|
+
consumeMainTurnState(cwd, turnState);
|
|
254
|
+
notifyByLevel('warning', buildNotifyExtra(data), settings);
|
|
265
255
|
return;
|
|
266
256
|
}
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
257
|
+
if (runDeliveryGate(data)) {
|
|
258
|
+
consumeMainTurnState(cwd, turnState);
|
|
259
|
+
notifyByLevel('warning', buildNotifyExtra(data), settings);
|
|
270
260
|
return;
|
|
271
261
|
}
|
|
272
262
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(data));
|
|
263
|
+
notifyByLevel('complete', buildNotifyExtra(data), settings);
|
|
264
|
+
consumeMainTurnState(cwd, turnState);
|
|
276
265
|
}
|
|
277
266
|
|
|
278
267
|
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`。')
|
|
311
|
+
if (summary.stateSessionMode === 'default') {
|
|
312
|
+
explanations.push('说明:当前宿主未提供稳定会话标识,因此使用分支默认位置。')
|
|
313
|
+
}
|
|
314
|
+
if (summary.usesSharedStore) {
|
|
315
|
+
explanations.push('说明:状态文件与 `.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 }
|