helloagents 3.0.7 → 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 +65 -58
- package/README_CN.md +60 -53
- package/bootstrap-lite.md +46 -28
- package/bootstrap.md +51 -34
- package/gemini-extension.json +1 -1
- package/package.json +12 -2
- package/scripts/capability-registry.mjs +9 -9
- package/scripts/cli-codex-config.mjs +49 -55
- package/scripts/cli-codex.mjs +69 -77
- package/scripts/cli-doctor.mjs +26 -18
- package/scripts/cli-host-detect.mjs +18 -2
- package/scripts/cli-messages.mjs +1 -1
- package/scripts/cli-toml.mjs +30 -0
- package/scripts/delivery-gate.mjs +5 -4
- package/scripts/guard-rules.mjs +26 -1
- package/scripts/guard.mjs +43 -14
- package/scripts/notify-context.mjs +30 -33
- package/scripts/notify-route.mjs +5 -2
- package/scripts/notify-source.mjs +3 -60
- package/scripts/notify.mjs +43 -11
- package/scripts/project-storage.mjs +107 -15
- package/scripts/session-token.mjs +73 -0
- package/scripts/turn-state.mjs +173 -0
- package/scripts/workflow-core.mjs +19 -11
- package/scripts/workflow-plan-files.mjs +17 -6
- package/scripts/workflow-recommendation.mjs +14 -14
- package/scripts/workflow-state.mjs +14 -14
- package/skills/_meta/SKILL.md +1 -1
- package/skills/commands/auto/SKILL.md +24 -9
- package/skills/commands/build/SKILL.md +4 -4
- package/skills/commands/clean/SKILL.md +3 -3
- package/skills/commands/commit/SKILL.md +1 -1
- package/skills/commands/help/SKILL.md +3 -3
- package/skills/commands/idea/SKILL.md +2 -2
- package/skills/commands/init/SKILL.md +13 -8
- package/skills/commands/loop/SKILL.md +4 -4
- package/skills/commands/plan/SKILL.md +13 -11
- package/skills/commands/prd/SKILL.md +10 -8
- package/skills/commands/verify/SKILL.md +5 -5
- package/skills/commands/wiki/SKILL.md +9 -11
- package/skills/hello-review/SKILL.md +1 -1
- package/skills/hello-subagent/SKILL.md +3 -2
- package/skills/hello-ui/SKILL.md +13 -13
- package/skills/hello-verify/SKILL.md +6 -5
- package/skills/helloagents/SKILL.md +17 -12
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { buildCommandRouteHint, buildStateSyncHint, buildWorkflowRouteHint } from './workflow-state.mjs';
|
|
4
|
+
import { buildCommandRouteHint, buildStateSyncHint, buildWorkflowRouteHint, readStateSnapshot } from './workflow-state.mjs';
|
|
5
5
|
import { buildCapabilityHint } from './capability-registry.mjs';
|
|
6
6
|
import {
|
|
7
7
|
buildProjectStorageBlock,
|
|
@@ -31,11 +31,6 @@ function resolveStandbyHostRoot(host) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function resolveReadRoot({ cwd, pkgRoot, host, settings }) {
|
|
34
|
-
const projectRoot = join(cwd, 'skills', 'helloagents');
|
|
35
|
-
if (existsSync(projectRoot)) {
|
|
36
|
-
return { source: 'project', root: projectRoot };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
34
|
if (settings.install_mode === 'standby') {
|
|
40
35
|
const standbyRoot = resolveStandbyHostRoot(host);
|
|
41
36
|
if (standbyRoot && existsSync(standbyRoot)) {
|
|
@@ -48,7 +43,7 @@ function resolveReadRoot({ cwd, pkgRoot, host, settings }) {
|
|
|
48
43
|
|
|
49
44
|
function buildReadRootBlock(readRoot) {
|
|
50
45
|
if (!readRoot?.root) return '';
|
|
51
|
-
return `##
|
|
46
|
+
return `## 本轮 HelloAGENTS 读取根目录\n\`\`\`json\n${JSON.stringify(readRoot, null, 2)}\n\`\`\``;
|
|
52
47
|
}
|
|
53
48
|
|
|
54
49
|
export function resolveCanonicalCommandSkill(skillName) {
|
|
@@ -74,16 +69,14 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
|
|
|
74
69
|
summaryParts.push('以下信息在上下文压缩前保存,确保压缩后不丢失关键状态。');
|
|
75
70
|
|
|
76
71
|
const cwd = payload.cwd || process.cwd();
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
summaryParts.push(stateContent);
|
|
86
|
-
} catch {}
|
|
72
|
+
const workflowOptions = { payload };
|
|
73
|
+
const stateSnapshot = readStateSnapshot(cwd, workflowOptions);
|
|
74
|
+
const stateSyncHint = buildStateSyncHint(cwd, workflowOptions);
|
|
75
|
+
if (stateSnapshot.exists && stateSnapshot.content) {
|
|
76
|
+
summaryParts.push('');
|
|
77
|
+
summaryParts.push(`## 恢复快照(从 ${stateSnapshot.statePath.replace(/\\/g, '/')} 读取,只用于找回上次停在哪)`);
|
|
78
|
+
summaryParts.push('恢复时先看当前用户消息,确认仍是同一任务再按 STATE.md 接续。');
|
|
79
|
+
summaryParts.push(stateSnapshot.content);
|
|
87
80
|
}
|
|
88
81
|
|
|
89
82
|
let bootstrap = '';
|
|
@@ -108,7 +101,7 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
|
|
|
108
101
|
summaryParts.push(readRootBlock);
|
|
109
102
|
}
|
|
110
103
|
|
|
111
|
-
const projectStorageBlock = buildProjectStorageBlock(cwd);
|
|
104
|
+
const projectStorageBlock = buildProjectStorageBlock(cwd, workflowOptions);
|
|
112
105
|
if (projectStorageBlock) {
|
|
113
106
|
summaryParts.push('');
|
|
114
107
|
summaryParts.push(projectStorageBlock);
|
|
@@ -128,13 +121,15 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
|
|
|
128
121
|
return summaryParts.join('\n');
|
|
129
122
|
}
|
|
130
123
|
|
|
131
|
-
export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host, cwd }) {
|
|
124
|
+
export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host, cwd, payload = {} }) {
|
|
125
|
+
const workflowOptions = { payload };
|
|
132
126
|
const packageRootBlock = buildPackageRootBlock(pkgRoot);
|
|
133
127
|
const readRootBlock = buildReadRootBlock(resolveReadRoot({ cwd, pkgRoot, host, settings }));
|
|
134
|
-
const workflowHint = buildWorkflowRouteHint(cwd);
|
|
135
|
-
const capabilityHint = buildCapabilityHint({ cwd });
|
|
136
|
-
const projectStorageBlock = buildProjectStorageBlock(cwd);
|
|
137
|
-
const
|
|
128
|
+
const workflowHint = buildWorkflowRouteHint(cwd, workflowOptions);
|
|
129
|
+
const capabilityHint = buildCapabilityHint({ cwd, options: workflowOptions });
|
|
130
|
+
const projectStorageBlock = buildProjectStorageBlock(cwd, workflowOptions);
|
|
131
|
+
const stateSnapshot = readStateSnapshot(cwd, workflowOptions);
|
|
132
|
+
const stateSyncHint = buildStateSyncHint(cwd, workflowOptions);
|
|
138
133
|
const settingsBlock = Object.keys(settings).length
|
|
139
134
|
? `\n\n## 当前用户设置\n\`\`\`json\n${JSON.stringify(settings, null, 2)}\n\`\`\``
|
|
140
135
|
: '';
|
|
@@ -148,31 +143,33 @@ export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host,
|
|
|
148
143
|
if (stateSyncHint) context += `\n\n## STATE.md 提醒\n${stateSyncHint}`;
|
|
149
144
|
context += settingsBlock;
|
|
150
145
|
if (source === 'resume' || source === 'compact') {
|
|
151
|
-
context +=
|
|
146
|
+
context += `\n\n> ⚠️ 会话已恢复/压缩,请先读取当前 \`state_path\` 指向的 \`${stateSnapshot.statePath.replace(/\\/g, '/')}\` 恢复工作状态;先看当前用户消息确认仍是同一任务,再按 STATE.md 接续。`;
|
|
152
147
|
}
|
|
153
148
|
return context;
|
|
154
149
|
}
|
|
155
150
|
|
|
156
|
-
export function buildRouteInstruction({ skillName, extraRules = '', cwd, pkgRoot, host, settings }) {
|
|
151
|
+
export function buildRouteInstruction({ skillName, extraRules = '', cwd, pkgRoot, host, settings, payload = {} }) {
|
|
152
|
+
const workflowOptions = { payload };
|
|
157
153
|
const readRoot = resolveReadRoot({ cwd, pkgRoot, host, settings });
|
|
158
154
|
const canonicalSkillName = resolveCanonicalCommandSkill(skillName);
|
|
159
155
|
const skillPath = join(readRoot.root, 'skills', 'commands', canonicalSkillName, 'SKILL.md');
|
|
160
156
|
const aliasNote = buildAliasRouteNote(skillName);
|
|
161
|
-
const commandHint = buildCommandRouteHint(canonicalSkillName, cwd);
|
|
162
|
-
const capabilityHint = buildCapabilityHint({ cwd, skillName: canonicalSkillName });
|
|
163
|
-
const projectStorageHint = buildProjectStorageHint(cwd);
|
|
157
|
+
const commandHint = buildCommandRouteHint(canonicalSkillName, cwd, workflowOptions);
|
|
158
|
+
const capabilityHint = buildCapabilityHint({ cwd, skillName: canonicalSkillName, options: workflowOptions });
|
|
159
|
+
const projectStorageHint = buildProjectStorageHint(cwd, workflowOptions);
|
|
164
160
|
return `用户使用了 ~${skillName} 命令。当前命令技能文件已解析为:${skillPath}。请直接读取这个 SKILL.md;不要再探测其他 helloagents 路径。${aliasNote ? ` ${aliasNote}` : ''}${projectStorageHint ? ` ${projectStorageHint}` : ''}${commandHint ? ` ${commandHint}` : ''}${capabilityHint ? ` ${capabilityHint}` : ''}${extraRules}`;
|
|
165
161
|
}
|
|
166
162
|
|
|
167
|
-
export function buildSemanticRouteInstruction(cwd) {
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
const
|
|
163
|
+
export function buildSemanticRouteInstruction(cwd, payload = {}) {
|
|
164
|
+
const workflowOptions = { payload };
|
|
165
|
+
const workflowHint = buildWorkflowRouteHint(cwd, workflowOptions);
|
|
166
|
+
const capabilityHint = buildCapabilityHint({ cwd, options: workflowOptions });
|
|
167
|
+
const projectStorageHint = buildProjectStorageHint(cwd, workflowOptions);
|
|
171
168
|
return [
|
|
172
169
|
'当前消息未使用 ~command。',
|
|
173
170
|
'请根据用户请求的真实意图选路,不依赖关键词表。',
|
|
174
171
|
'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆链路。',
|
|
175
|
-
'路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto
|
|
172
|
+
'路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动编排并自动衔接后续阶段。',
|
|
176
173
|
'若判定为 T3,默认先走 ~plan / ~prd;纯审查/验证请求才优先 ~verify。',
|
|
177
174
|
`涉及 UI 任务时,设计决策优先级:当前活跃 plan / PRD → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → 通用 UI 规则。`,
|
|
178
175
|
projectStorageHint,
|
package/scripts/notify-route.mjs
CHANGED
|
@@ -17,6 +17,7 @@ function buildHelpExtraRules(skillName) {
|
|
|
17
17
|
|
|
18
18
|
function routeExplicitCommand({
|
|
19
19
|
prompt,
|
|
20
|
+
payload,
|
|
20
21
|
cwd,
|
|
21
22
|
host,
|
|
22
23
|
pkgRoot,
|
|
@@ -51,6 +52,7 @@ function routeExplicitCommand({
|
|
|
51
52
|
pkgRoot,
|
|
52
53
|
host,
|
|
53
54
|
settings,
|
|
55
|
+
payload,
|
|
54
56
|
}))
|
|
55
57
|
return true
|
|
56
58
|
}
|
|
@@ -80,6 +82,7 @@ export function handleRouteCommand({
|
|
|
80
82
|
|
|
81
83
|
if (routeExplicitCommand({
|
|
82
84
|
prompt,
|
|
85
|
+
payload,
|
|
83
86
|
cwd,
|
|
84
87
|
host,
|
|
85
88
|
pkgRoot,
|
|
@@ -100,9 +103,9 @@ export function handleRouteCommand({
|
|
|
100
103
|
host,
|
|
101
104
|
event: 'semantic_route_prompted',
|
|
102
105
|
source: 'route',
|
|
103
|
-
recommendation: getWorkflowRecommendation(cwd),
|
|
106
|
+
recommendation: getWorkflowRecommendation(cwd, { payload }),
|
|
104
107
|
})
|
|
105
|
-
suppress(buildSemanticRouteInstruction(cwd))
|
|
108
|
+
suppress(buildSemanticRouteInstruction(cwd, payload))
|
|
106
109
|
return
|
|
107
110
|
}
|
|
108
111
|
|
|
@@ -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
|
@@ -10,11 +10,12 @@ import { homedir } from 'node:os';
|
|
|
10
10
|
import { playSound as _playSound, desktopNotify as _desktopNotify } from './notify-ui.mjs';
|
|
11
11
|
import { resolveNotificationSource } from './notify-source.mjs';
|
|
12
12
|
import { buildCompactionContext, buildInjectContext, buildRouteInstruction, buildSemanticRouteInstruction, resolveCanonicalCommandSkill } from './notify-context.mjs';
|
|
13
|
-
import { claimsTaskComplete, shouldIgnoreCodexNotifyClient
|
|
13
|
+
import { claimsTaskComplete, shouldIgnoreCodexNotifyClient } from './notify-events.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';
|
|
17
17
|
import { appendReplayEvent, startReplaySession } from './replay-state.mjs';
|
|
18
|
+
import { clearTurnState, readTurnState } from './turn-state.mjs';
|
|
18
19
|
import { getWorkflowRecommendation } from './workflow-state.mjs';
|
|
19
20
|
|
|
20
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -114,6 +115,20 @@ function readCompletionText(payload = {}) {
|
|
|
114
115
|
|| '';
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
function readMainTurnState(cwd) {
|
|
119
|
+
const turnState = readTurnState(cwd);
|
|
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';
|
|
129
|
+
return claimsTaskComplete(lastMsg);
|
|
130
|
+
}
|
|
131
|
+
|
|
117
132
|
function cmdPreCompact() {
|
|
118
133
|
const payload = readStdinJson();
|
|
119
134
|
const cwd = payload.cwd || process.cwd();
|
|
@@ -140,6 +155,7 @@ function cmdPreCompact() {
|
|
|
140
155
|
|
|
141
156
|
function cmdRoute() {
|
|
142
157
|
const payload = readStdinJson();
|
|
158
|
+
clearTurnState(payload.cwd || process.cwd());
|
|
143
159
|
handleRouteCommand({
|
|
144
160
|
payload,
|
|
145
161
|
host: HOST,
|
|
@@ -192,8 +208,10 @@ function cmdInject() {
|
|
|
192
208
|
pkgRoot: PKG_ROOT,
|
|
193
209
|
host: HOST,
|
|
194
210
|
cwd,
|
|
211
|
+
payload,
|
|
195
212
|
});
|
|
196
213
|
clearRouteContext();
|
|
214
|
+
clearTurnState(cwd);
|
|
197
215
|
suppressedOutput(EVENT_NAME.SessionStart, context || undefined);
|
|
198
216
|
}
|
|
199
217
|
|
|
@@ -201,13 +219,17 @@ function cmdStop() {
|
|
|
201
219
|
const payload = readStdinJson();
|
|
202
220
|
const lastMsg = readCompletionText(payload);
|
|
203
221
|
const cwd = payload.cwd || process.cwd();
|
|
222
|
+
const turnState = readMainTurnState(cwd);
|
|
223
|
+
const shouldProcess = shouldProcessCloseout(turnState, lastMsg);
|
|
204
224
|
clearRouteContext();
|
|
205
|
-
if (runRalphLoop(payload)) {
|
|
225
|
+
if (shouldProcess && runRalphLoop(payload)) {
|
|
226
|
+
consumeMainTurnState(cwd, turnState);
|
|
206
227
|
playSound('warning');
|
|
207
228
|
desktopNotify('warning', buildNotifyExtra(payload));
|
|
208
229
|
return;
|
|
209
230
|
}
|
|
210
|
-
if (
|
|
231
|
+
if (shouldProcess && runDeliveryGate(payload)) {
|
|
232
|
+
consumeMainTurnState(cwd, turnState);
|
|
211
233
|
playSound('warning');
|
|
212
234
|
desktopNotify('warning', buildNotifyExtra(payload));
|
|
213
235
|
return;
|
|
@@ -215,8 +237,11 @@ function cmdStop() {
|
|
|
215
237
|
|
|
216
238
|
const settings = getSettings();
|
|
217
239
|
const level = settings.notify_level ?? 0;
|
|
218
|
-
if (
|
|
219
|
-
|
|
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);
|
|
220
245
|
emptySuppress();
|
|
221
246
|
}
|
|
222
247
|
|
|
@@ -243,17 +268,23 @@ function cmdCodexNotify() {
|
|
|
243
268
|
}
|
|
244
269
|
if (type !== 'agent-turn-complete') return;
|
|
245
270
|
|
|
246
|
-
const lastMsg = data['last-assistant-message'] || '';
|
|
247
|
-
const settings = getSettings();
|
|
248
|
-
if (shouldIgnoreFormattedSubagent(lastMsg, settings.output_format !== false)) return;
|
|
249
|
-
|
|
250
271
|
const cwd = data.cwd || process.cwd();
|
|
251
|
-
|
|
272
|
+
const turnState = readMainTurnState(cwd);
|
|
273
|
+
if (!turnState) return;
|
|
274
|
+
if (turnState.kind !== 'complete') {
|
|
275
|
+
consumeMainTurnState(cwd, turnState);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const settings = getSettings();
|
|
280
|
+
if (runRalphLoop(data)) {
|
|
281
|
+
consumeMainTurnState(cwd, turnState);
|
|
252
282
|
playSound('warning');
|
|
253
283
|
desktopNotify('warning', buildNotifyExtra(data));
|
|
254
284
|
return;
|
|
255
285
|
}
|
|
256
|
-
if (
|
|
286
|
+
if (runDeliveryGate(data)) {
|
|
287
|
+
consumeMainTurnState(cwd, turnState);
|
|
257
288
|
playSound('warning');
|
|
258
289
|
desktopNotify('warning', buildNotifyExtra(data));
|
|
259
290
|
return;
|
|
@@ -262,6 +293,7 @@ function cmdCodexNotify() {
|
|
|
262
293
|
const level = settings.notify_level ?? 0;
|
|
263
294
|
if (level === 2 || level === 3) playSound('complete');
|
|
264
295
|
if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(data));
|
|
296
|
+
consumeMainTurnState(cwd, turnState);
|
|
265
297
|
}
|
|
266
298
|
|
|
267
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 }
|