helloagents 3.0.12 → 3.0.16-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +6 -4
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +182 -35
- package/README_CN.md +184 -37
- package/bootstrap-lite.md +32 -26
- package/bootstrap.md +35 -29
- package/cli.mjs +119 -11
- package/gemini-extension.json +1 -1
- package/install.ps1 +128 -0
- package/install.sh +121 -0
- package/package.json +23 -4
- package/scripts/advisor-state.mjs +36 -63
- package/scripts/capability-registry.mjs +4 -4
- package/scripts/cli-branch.mjs +84 -0
- package/scripts/cli-codex-config.mjs +14 -20
- package/scripts/cli-codex.mjs +32 -38
- package/scripts/cli-doctor-render.mjs +4 -0
- package/scripts/cli-doctor.mjs +40 -30
- package/scripts/cli-host-detect.mjs +0 -1
- package/scripts/cli-hosts.mjs +16 -8
- package/scripts/cli-lifecycle-hosts.mjs +119 -32
- package/scripts/cli-lifecycle.mjs +24 -13
- package/scripts/cli-messages.mjs +34 -16
- package/scripts/cli-runtime-carrier.mjs +15 -0
- package/scripts/cli-runtime-root.mjs +72 -0
- package/scripts/cli-toml.mjs +0 -79
- package/scripts/cli-utils.mjs +30 -4
- package/scripts/closeout-state.mjs +35 -62
- package/scripts/delivery-gate-messages.mjs +70 -0
- package/scripts/delivery-gate.mjs +9 -75
- package/scripts/guard-rules.mjs +42 -42
- package/scripts/guard.mjs +44 -24
- package/scripts/notify-context.mjs +19 -28
- package/scripts/notify-events.mjs +3 -1
- package/scripts/notify-gates.mjs +2 -0
- package/scripts/notify-route.mjs +9 -7
- package/scripts/notify-ui.mjs +42 -32
- package/scripts/notify.mjs +72 -36
- package/scripts/project-storage.mjs +35 -66
- package/scripts/ralph-loop.mjs +36 -31
- package/scripts/replay-state.mjs +31 -128
- package/scripts/review-state.mjs +34 -61
- package/scripts/runtime-artifacts.mjs +95 -0
- package/scripts/runtime-context.mjs +35 -29
- package/scripts/runtime-scope.mjs +313 -0
- package/scripts/session-capsule.mjs +202 -0
- package/scripts/turn-state-cli.mjs +17 -0
- package/scripts/turn-state.mjs +185 -66
- package/scripts/turn-stop-gate.mjs +24 -6
- package/scripts/verify-state.mjs +34 -85
- package/scripts/visual-state.mjs +38 -65
- package/scripts/workflow-core.mjs +3 -3
- package/scripts/workflow-plan-files.mjs +1 -1
- package/scripts/workflow-recommendation.mjs +17 -13
- package/scripts/workflow-state.mjs +5 -5
- package/skills/commands/build/SKILL.md +1 -1
- package/skills/commands/commit/SKILL.md +1 -1
- package/skills/commands/help/SKILL.md +5 -3
- package/skills/commands/loop/SKILL.md +1 -1
- package/skills/commands/plan/SKILL.md +8 -6
- package/skills/commands/prd/SKILL.md +5 -3
- package/skills/commands/verify/SKILL.md +5 -5
- package/skills/hello-debug/SKILL.md +20 -3
- package/skills/hello-review/SKILL.md +2 -2
- package/skills/hello-subagent/SKILL.md +2 -2
- package/skills/hello-test/SKILL.md +6 -2
- package/skills/hello-ui/SKILL.md +7 -7
- package/skills/hello-verify/SKILL.md +10 -7
- package/skills/helloagents/SKILL.md +14 -9
- package/templates/context.md +6 -0
- package/templates/plans/plan.md +3 -0
- package/templates/plans/tasks.md +8 -3
package/scripts/notify-ui.mjs
CHANGED
|
@@ -55,47 +55,50 @@ function resolveWav(pkgRoot, event) {
|
|
|
55
55
|
return existsSync(p) ? p : null;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function runSync(command, args) {
|
|
59
|
+
try {
|
|
60
|
+
const result = spawnSync(command, args, {
|
|
61
|
+
stdio: 'ignore',
|
|
62
|
+
windowsHide: true,
|
|
63
|
+
});
|
|
64
|
+
return !result.error && result.status === 0;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
export function playSound(pkgRoot, event) {
|
|
59
71
|
if (DISABLE_OS_NOTIFICATIONS) return;
|
|
60
72
|
const wav = resolveWav(pkgRoot, event);
|
|
61
73
|
if (!wav) { process.stderr.write('\x07'); return; }
|
|
62
74
|
try {
|
|
63
75
|
if (PLAT === 'win32') {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
runSync('powershell', [
|
|
77
|
+
'-NoProfile',
|
|
78
|
+
'-c',
|
|
79
|
+
`(New-Object Media.SoundPlayer '${wav.replace(/'/g, "''")}').PlaySync()`,
|
|
80
|
+
]);
|
|
67
81
|
} else if (PLAT === 'darwin') {
|
|
68
|
-
|
|
82
|
+
runSync('afplay', [wav]);
|
|
69
83
|
} else {
|
|
70
|
-
|
|
71
|
-
if (result.status !== 0) {
|
|
72
|
-
const pa = spawnSync('paplay', [wav], { stdio: 'ignore' });
|
|
73
|
-
if (pa.status !== 0) process.stderr.write('\x07');
|
|
74
|
-
}
|
|
84
|
+
if (!runSync('aplay', ['-q', wav]) && !runSync('paplay', [wav])) process.stderr.write('\x07');
|
|
75
85
|
}
|
|
76
86
|
} catch { process.stderr.write('\x07'); }
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
function
|
|
80
|
-
if (PLAT !== 'win32') return;
|
|
89
|
+
function buildWindowsToastScript(notification, iconPath) {
|
|
81
90
|
const regKey = `HKCU:\\Software\\Classes\\AppUserModelId\\${WIN_APPID}`;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
const iconXml = existsSync(iconPath)
|
|
92
|
+
? `<image placement="appLogoOverride" src="${escapeToastText(iconPath)}" />`
|
|
93
|
+
: '';
|
|
94
|
+
const textXml = notification.toastLines
|
|
95
|
+
.map((line) => `<text>${escapeToastText(line)}</text>`)
|
|
96
|
+
.join('\n ');
|
|
97
|
+
return `
|
|
98
|
+
if (-not (Test-Path '${regKey}')) {
|
|
99
|
+
New-Item -Path '${regKey}' -Force | Out-Null
|
|
100
|
+
Set-ItemProperty -Path '${regKey}' -Name 'DisplayName' -Value 'HelloAgents 通知' -Force
|
|
85
101
|
}
|
|
86
|
-
|
|
87
|
-
export function desktopNotify(pkgRoot, event, extra) {
|
|
88
|
-
if (DISABLE_OS_NOTIFICATIONS) return;
|
|
89
|
-
const notification = buildDesktopNotificationContent(event, extra);
|
|
90
|
-
try {
|
|
91
|
-
if (PLAT === 'win32') {
|
|
92
|
-
ensureWinAppId(pkgRoot);
|
|
93
|
-
const iconPath = join(pkgRoot, 'assets', 'icons', 'icon.png').replace(/\//g, '\\');
|
|
94
|
-
const iconXml = existsSync(iconPath) ? `<image placement="appLogoOverride" src="${iconPath}" />` : '';
|
|
95
|
-
const textXml = notification.toastLines
|
|
96
|
-
.map((line) => `<text>${escapeToastText(line)}</text>`)
|
|
97
|
-
.join('\n ');
|
|
98
|
-
const ps = `
|
|
99
102
|
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
100
103
|
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
|
|
101
104
|
$xml = @"
|
|
@@ -113,17 +116,24 @@ $doc.LoadXml($xml)
|
|
|
113
116
|
$toast = [Windows.UI.Notifications.ToastNotification]::new($doc)
|
|
114
117
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${WIN_APPID}').Show($toast)
|
|
115
118
|
`.trim();
|
|
116
|
-
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function desktopNotify(pkgRoot, event, extra) {
|
|
122
|
+
if (DISABLE_OS_NOTIFICATIONS) return;
|
|
123
|
+
const notification = buildDesktopNotificationContent(event, extra);
|
|
124
|
+
try {
|
|
125
|
+
if (PLAT === 'win32') {
|
|
126
|
+
const iconPath = join(pkgRoot, 'assets', 'icons', 'icon.png').replace(/\//g, '\\');
|
|
127
|
+
runSync('powershell', ['-NoProfile', '-c', buildWindowsToastScript(notification, iconPath)]);
|
|
117
128
|
} else if (PLAT === 'darwin') {
|
|
118
129
|
const subtitle = notification.sourceLabel
|
|
119
130
|
? ` subtitle "${escapeAppleScriptText(notification.sourceLabel)}"`
|
|
120
131
|
: '';
|
|
121
|
-
|
|
132
|
+
runSync('osascript', ['-e',
|
|
122
133
|
`display notification "${escapeAppleScriptText(notification.message)}" with title "${escapeAppleScriptText(notification.title)}"${subtitle}`],
|
|
123
|
-
|
|
134
|
+
);
|
|
124
135
|
} else {
|
|
125
|
-
|
|
126
|
-
if (result.status !== 0) process.stderr.write('\x07');
|
|
136
|
+
if (!runSync('notify-send', [notification.title, notification.body])) process.stderr.write('\x07');
|
|
127
137
|
}
|
|
128
138
|
} catch { process.stderr.write('\x07'); }
|
|
129
139
|
}
|
package/scripts/notify.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Zero external dependencies, ES module, cross-platform
|
|
4
4
|
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
|
-
import {
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { playSound as _playSound, desktopNotify as _desktopNotify } from './notify-ui.mjs';
|
|
@@ -13,10 +13,12 @@ import { shouldIgnoreCodexNotifyClient } from './notify-events.mjs';
|
|
|
13
13
|
import { runGateScript } from './notify-gates.mjs';
|
|
14
14
|
import { handleRouteCommand, resolveBootstrapFile } from './notify-route.mjs';
|
|
15
15
|
import { readSettings, readStdinJson, output, suppressedOutput, emptySuppress } from './notify-shared.mjs';
|
|
16
|
-
import { clearRouteContext, writeRouteContext } from './runtime-context.mjs';
|
|
16
|
+
import { clearRouteContext, getApplicableRouteContext, writeRouteContext } from './runtime-context.mjs';
|
|
17
17
|
import { appendReplayEvent, startReplaySession } from './replay-state.mjs';
|
|
18
18
|
import { clearTurnState, readTurnState } from './turn-state.mjs';
|
|
19
19
|
import { getWorkflowRecommendation } from './workflow-state.mjs';
|
|
20
|
+
import { resolveSessionToken } from './session-token.mjs';
|
|
21
|
+
import { isProjectRuntimeActive } from './runtime-scope.mjs';
|
|
20
22
|
|
|
21
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
24
|
const __dirname = dirname(__filename);
|
|
@@ -33,6 +35,7 @@ const EVENT_NAME = {
|
|
|
33
35
|
UserPromptSubmit: IS_GEMINI ? 'BeforeAgent' : 'UserPromptSubmit',
|
|
34
36
|
PreCompact: IS_GEMINI ? 'BeforeAgent' : 'PreCompact',
|
|
35
37
|
};
|
|
38
|
+
const RALPH_LOOP_ROUTE_COMMANDS = new Set(['verify', 'loop']);
|
|
36
39
|
|
|
37
40
|
const playSound = (event) => _playSound(PKG_ROOT, event);
|
|
38
41
|
const desktopNotify = (event, extra) => _desktopNotify(PKG_ROOT, event, extra);
|
|
@@ -44,8 +47,12 @@ function normalizeNotifyLevel(value) {
|
|
|
44
47
|
|
|
45
48
|
function notifyByLevel(event, extra, settings = getSettings()) {
|
|
46
49
|
const level = normalizeNotifyLevel(settings.notify_level ?? 0);
|
|
47
|
-
if (level ===
|
|
48
|
-
if (level ===
|
|
50
|
+
if (level === 1) desktopNotify(event, extra);
|
|
51
|
+
if (level === 2) playSound(event);
|
|
52
|
+
if (level === 3) {
|
|
53
|
+
desktopNotify(event, extra);
|
|
54
|
+
playSound(event);
|
|
55
|
+
}
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
function buildNotifyExtra(payload = {}, options = {}) {
|
|
@@ -64,9 +71,18 @@ function getSettings() {
|
|
|
64
71
|
return readSettings(CONFIG_FILE);
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
function
|
|
74
|
+
function shouldRunRalphLoop(cwd, turnState, payload = {}) {
|
|
75
|
+
if (!turnState || turnState.kind !== 'complete') return false;
|
|
76
|
+
if (turnState.requiresDeliveryGate) return true;
|
|
77
|
+
const routeContext = getApplicableRouteContext({ cwd, payload });
|
|
78
|
+
return RALPH_LOOP_ROUTE_COMMANDS.has(routeContext?.skillName);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function runRalphLoop(payload, { turnState } = {}) {
|
|
68
82
|
const settings = getSettings();
|
|
69
83
|
if (settings.ralph_loop_enabled === false) return false;
|
|
84
|
+
const cwd = payload.cwd || process.cwd();
|
|
85
|
+
if (!shouldRunRalphLoop(cwd, turnState, payload)) return false;
|
|
70
86
|
return runGateScript({
|
|
71
87
|
payload,
|
|
72
88
|
host: HOST,
|
|
@@ -106,13 +122,24 @@ function runTurnStopGate(payload) {
|
|
|
106
122
|
});
|
|
107
123
|
}
|
|
108
124
|
|
|
109
|
-
function
|
|
110
|
-
const
|
|
125
|
+
function attachTurnSession(payload = {}, cwd = payload.cwd || process.cwd()) {
|
|
126
|
+
const sessionId = resolveSessionToken({
|
|
127
|
+
payload,
|
|
128
|
+
env: process.env,
|
|
129
|
+
ppid: process.ppid,
|
|
130
|
+
allowPpidFallback: !isProjectRuntimeActive(cwd),
|
|
131
|
+
});
|
|
132
|
+
if (!sessionId || payload.sessionId) return payload;
|
|
133
|
+
return { ...payload, sessionId };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readMainTurnState(cwd, payload = {}) {
|
|
137
|
+
const turnState = readTurnState(cwd, { payload });
|
|
111
138
|
return turnState?.role === 'main' ? turnState : null;
|
|
112
139
|
}
|
|
113
140
|
|
|
114
|
-
function consumeMainTurnState(cwd, turnState) {
|
|
115
|
-
if (turnState?.role === 'main') clearTurnState(cwd);
|
|
141
|
+
function consumeMainTurnState(cwd, turnState, payload = {}) {
|
|
142
|
+
if (turnState?.role === 'main') clearTurnState(cwd, { payload });
|
|
116
143
|
}
|
|
117
144
|
|
|
118
145
|
function shouldProcessCloseout(turnState) {
|
|
@@ -136,6 +163,7 @@ function cmdPreCompact() {
|
|
|
136
163
|
host: HOST,
|
|
137
164
|
event: 'pre_compact_snapshot',
|
|
138
165
|
source: 'pre-compact',
|
|
166
|
+
payload,
|
|
139
167
|
details: {
|
|
140
168
|
bootstrapFile,
|
|
141
169
|
installMode: settings.install_mode || '',
|
|
@@ -146,7 +174,7 @@ function cmdPreCompact() {
|
|
|
146
174
|
|
|
147
175
|
function cmdRoute() {
|
|
148
176
|
const payload = readStdinJson();
|
|
149
|
-
clearTurnState(payload.cwd || process.cwd());
|
|
177
|
+
clearTurnState(payload.cwd || process.cwd(), { payload });
|
|
150
178
|
handleRouteCommand({
|
|
151
179
|
payload,
|
|
152
180
|
host: HOST,
|
|
@@ -181,15 +209,17 @@ function cmdInject() {
|
|
|
181
209
|
source,
|
|
182
210
|
bootstrapFile,
|
|
183
211
|
installMode: settings.install_mode || '',
|
|
212
|
+
payload,
|
|
184
213
|
});
|
|
185
214
|
appendReplayEvent(cwd, {
|
|
186
215
|
host: HOST,
|
|
187
216
|
event: 'session_injected',
|
|
188
217
|
source,
|
|
218
|
+
payload,
|
|
189
219
|
details: {
|
|
190
220
|
bootstrapFile,
|
|
191
221
|
installMode: settings.install_mode || '',
|
|
192
|
-
activatedProject:
|
|
222
|
+
activatedProject: isProjectRuntimeActive(cwd),
|
|
193
223
|
},
|
|
194
224
|
});
|
|
195
225
|
const context = buildInjectContext({
|
|
@@ -201,37 +231,38 @@ function cmdInject() {
|
|
|
201
231
|
cwd,
|
|
202
232
|
payload,
|
|
203
233
|
});
|
|
204
|
-
clearRouteContext();
|
|
205
|
-
clearTurnState(cwd);
|
|
234
|
+
clearRouteContext({ cwd, payload });
|
|
235
|
+
clearTurnState(cwd, { payload });
|
|
206
236
|
suppressedOutput(EVENT_NAME.SessionStart, context || undefined);
|
|
207
237
|
}
|
|
208
238
|
|
|
209
239
|
function cmdStop() {
|
|
210
240
|
const payload = readStdinJson();
|
|
211
241
|
const cwd = payload.cwd || process.cwd();
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
242
|
+
const turnPayload = attachTurnSession(payload, cwd);
|
|
243
|
+
const turnState = readMainTurnState(cwd, turnPayload);
|
|
244
|
+
if (runTurnStopGate(turnPayload)) {
|
|
245
|
+
if (turnState && turnState.kind !== 'complete') consumeMainTurnState(cwd, turnState, turnPayload);
|
|
215
246
|
return;
|
|
216
247
|
}
|
|
217
248
|
const shouldProcess = shouldProcessCloseout(turnState);
|
|
218
|
-
if (shouldProcess && runRalphLoop(
|
|
219
|
-
consumeMainTurnState(cwd, turnState);
|
|
249
|
+
if (shouldProcess && runRalphLoop(turnPayload, { turnState })) {
|
|
250
|
+
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
220
251
|
notifyByLevel('warning', buildNotifyExtra(payload));
|
|
221
252
|
return;
|
|
222
253
|
}
|
|
223
|
-
if (shouldProcess && runDeliveryGate(
|
|
224
|
-
consumeMainTurnState(cwd, turnState);
|
|
254
|
+
if (shouldProcess && runDeliveryGate(turnPayload)) {
|
|
255
|
+
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
225
256
|
notifyByLevel('warning', buildNotifyExtra(payload));
|
|
226
257
|
return;
|
|
227
258
|
}
|
|
228
259
|
|
|
229
260
|
const settings = getSettings();
|
|
230
|
-
if (shouldProcess) {
|
|
261
|
+
if (shouldProcess || !turnState) {
|
|
231
262
|
notifyByLevel('complete', buildNotifyExtra(payload), settings);
|
|
232
263
|
}
|
|
233
|
-
consumeMainTurnState(cwd, turnState);
|
|
234
|
-
clearRouteContext();
|
|
264
|
+
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
265
|
+
clearRouteContext({ cwd, payload: turnPayload });
|
|
235
266
|
emptySuppress();
|
|
236
267
|
}
|
|
237
268
|
|
|
@@ -246,6 +277,8 @@ function cmdDesktop() {
|
|
|
246
277
|
function cmdCodexNotify() {
|
|
247
278
|
let data = {};
|
|
248
279
|
try { data = JSON.parse(process.argv[3] || '{}'); } catch {}
|
|
280
|
+
const cwd = data.cwd || process.cwd();
|
|
281
|
+
const turnPayload = attachTurnSession(data, cwd);
|
|
249
282
|
|
|
250
283
|
const type = data.type || '';
|
|
251
284
|
const client = data.client || '';
|
|
@@ -257,34 +290,37 @@ function cmdCodexNotify() {
|
|
|
257
290
|
}
|
|
258
291
|
if (type !== 'agent-turn-complete') return;
|
|
259
292
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
293
|
+
const turnState = readMainTurnState(cwd, turnPayload);
|
|
294
|
+
if (runTurnStopGate(turnPayload)) {
|
|
295
|
+
if (turnState && turnState.kind !== 'complete') consumeMainTurnState(cwd, turnState, turnPayload);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!turnState) {
|
|
299
|
+
notifyByLevel('complete', buildNotifyExtra(data), getSettings());
|
|
300
|
+
clearRouteContext({ cwd, payload: turnPayload });
|
|
264
301
|
return;
|
|
265
302
|
}
|
|
266
|
-
if (!turnState) return;
|
|
267
303
|
if (turnState.kind !== 'complete') {
|
|
268
|
-
consumeMainTurnState(cwd, turnState);
|
|
269
|
-
clearRouteContext();
|
|
304
|
+
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
305
|
+
clearRouteContext({ cwd, payload: turnPayload });
|
|
270
306
|
return;
|
|
271
307
|
}
|
|
272
308
|
|
|
273
309
|
const settings = getSettings();
|
|
274
|
-
if (runRalphLoop(
|
|
275
|
-
consumeMainTurnState(cwd, turnState);
|
|
310
|
+
if (runRalphLoop(turnPayload, { turnState })) {
|
|
311
|
+
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
276
312
|
notifyByLevel('warning', buildNotifyExtra(data), settings);
|
|
277
313
|
return;
|
|
278
314
|
}
|
|
279
|
-
if (runDeliveryGate(
|
|
280
|
-
consumeMainTurnState(cwd, turnState);
|
|
315
|
+
if (runDeliveryGate(turnPayload)) {
|
|
316
|
+
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
281
317
|
notifyByLevel('warning', buildNotifyExtra(data), settings);
|
|
282
318
|
return;
|
|
283
319
|
}
|
|
284
320
|
|
|
285
321
|
notifyByLevel('complete', buildNotifyExtra(data), settings);
|
|
286
|
-
consumeMainTurnState(cwd, turnState);
|
|
287
|
-
clearRouteContext();
|
|
322
|
+
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
323
|
+
clearRouteContext({ cwd, payload: turnPayload });
|
|
288
324
|
}
|
|
289
325
|
|
|
290
326
|
const cmd = process.argv[2] || '';
|
|
@@ -5,13 +5,19 @@ import { homedir } from 'node:os'
|
|
|
5
5
|
import { basename, dirname, isAbsolute, join, normalize, resolve } from 'node:path'
|
|
6
6
|
|
|
7
7
|
import { DEFAULTS } from './cli-config.mjs'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
PROJECT_DIR_NAME,
|
|
10
|
+
getProjectActivationDir,
|
|
11
|
+
getProjectSessionScope,
|
|
12
|
+
normalizeRuntimeOptions,
|
|
13
|
+
} from './runtime-scope.mjs'
|
|
14
|
+
import {
|
|
15
|
+
getSessionArtifactPath,
|
|
16
|
+
getSessionArtifactRelativePath,
|
|
17
|
+
} from './session-capsule.mjs'
|
|
9
18
|
|
|
10
|
-
export const PROJECT_DIR_NAME = '.helloagents'
|
|
11
19
|
const PROJECTS_DIR_NAME = 'projects'
|
|
12
|
-
const PROJECT_SESSIONS_DIR_NAME = 'sessions'
|
|
13
20
|
const PROJECT_STORE_MODES = new Set(['local', 'repo-shared'])
|
|
14
|
-
const DEFAULT_STATE_SESSION_TOKEN = 'default'
|
|
15
21
|
|
|
16
22
|
function safeJson(filePath) {
|
|
17
23
|
try {
|
|
@@ -34,19 +40,6 @@ function runGitRevParse(cwd, args = []) {
|
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
function runGitCommand(cwd, args = []) {
|
|
38
|
-
try {
|
|
39
|
-
return execFileSync('git', args, {
|
|
40
|
-
cwd,
|
|
41
|
-
encoding: 'utf-8',
|
|
42
|
-
timeout: 5_000,
|
|
43
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
44
|
-
}).trim()
|
|
45
|
-
} catch {
|
|
46
|
-
return ''
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
43
|
function resolveGitTopLevel(cwd) {
|
|
51
44
|
const absolute = runGitRevParse(cwd, ['--path-format=absolute', '--show-toplevel'])
|
|
52
45
|
if (absolute) return normalize(absolute)
|
|
@@ -70,16 +63,6 @@ function sanitizeRepoName(value = '') {
|
|
|
70
63
|
return normalized || 'project'
|
|
71
64
|
}
|
|
72
65
|
|
|
73
|
-
function sanitizeStateScopeSegment(value = '', fallback = '') {
|
|
74
|
-
const normalized = String(value)
|
|
75
|
-
.trim()
|
|
76
|
-
.toLowerCase()
|
|
77
|
-
.replace(/[^a-z0-9._-]+/g, '-')
|
|
78
|
-
.replace(/^-+|-+$/g, '')
|
|
79
|
-
.slice(0, 48)
|
|
80
|
-
return normalized || fallback
|
|
81
|
-
}
|
|
82
|
-
|
|
83
66
|
function buildProjectKey(cwd) {
|
|
84
67
|
const repoRoot = resolveGitTopLevel(cwd)
|
|
85
68
|
const commonDir = resolveGitCommonDir(cwd, repoRoot)
|
|
@@ -113,15 +96,6 @@ function formatPromptPath(pathValue = '') {
|
|
|
113
96
|
return pathValue ? normalize(pathValue).replace(/\\/g, '/') : ''
|
|
114
97
|
}
|
|
115
98
|
|
|
116
|
-
function resolveGitBranchName(cwd) {
|
|
117
|
-
const branchName = runGitRevParse(cwd, ['--abbrev-ref', 'HEAD'])
|
|
118
|
-
if (branchName && branchName !== 'HEAD') return branchName
|
|
119
|
-
|
|
120
|
-
const symbolicBranchName = runGitCommand(cwd, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
|
|
121
|
-
if (symbolicBranchName && symbolicBranchName !== 'HEAD') return symbolicBranchName
|
|
122
|
-
return ''
|
|
123
|
-
}
|
|
124
|
-
|
|
125
99
|
export function normalizeProjectStoreMode(value) {
|
|
126
100
|
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
127
101
|
return PROJECT_STORE_MODES.has(normalized) ? normalized : DEFAULTS.project_store_mode
|
|
@@ -136,37 +110,16 @@ export function getProjectStoreMode() {
|
|
|
136
110
|
return normalizeProjectStoreMode(settings.project_store_mode)
|
|
137
111
|
}
|
|
138
112
|
|
|
139
|
-
export function
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function getProjectSessionStateScope(cwd, {
|
|
144
|
-
payload = {},
|
|
145
|
-
env = process.env,
|
|
146
|
-
ppid = process.ppid,
|
|
147
|
-
} = {}) {
|
|
148
|
-
const rawSessionToken = resolveSessionToken({
|
|
149
|
-
payload,
|
|
150
|
-
env,
|
|
151
|
-
ppid,
|
|
152
|
-
allowPpidFallback: false,
|
|
153
|
-
})
|
|
154
|
-
const branchName = sanitizeStateScopeSegment(resolveGitBranchName(cwd), 'detached')
|
|
155
|
-
const sessionToken = sanitizeStateScopeSegment(rawSessionToken, DEFAULT_STATE_SESSION_TOKEN)
|
|
156
|
-
const sessionDir = join(
|
|
157
|
-
getProjectActivationDir(cwd),
|
|
158
|
-
PROJECT_SESSIONS_DIR_NAME,
|
|
159
|
-
branchName,
|
|
160
|
-
sessionToken,
|
|
161
|
-
)
|
|
113
|
+
export function getProjectSessionStateScope(cwd, options = {}) {
|
|
114
|
+
const scope = getProjectSessionScope(cwd, normalizeRuntimeOptions(options))
|
|
162
115
|
|
|
163
116
|
return {
|
|
164
117
|
stateScope: 'session',
|
|
165
|
-
stateSessionToken:
|
|
166
|
-
stateSessionMode:
|
|
167
|
-
stateBranch:
|
|
168
|
-
sessionDir,
|
|
169
|
-
statePath:
|
|
118
|
+
stateSessionToken: scope.session,
|
|
119
|
+
stateSessionMode: scope.sessionMode,
|
|
120
|
+
stateBranch: scope.branch,
|
|
121
|
+
sessionDir: scope.sessionDir,
|
|
122
|
+
statePath: scope.statePath,
|
|
170
123
|
}
|
|
171
124
|
}
|
|
172
125
|
|
|
@@ -174,6 +127,18 @@ export function getProjectStatePath(cwd, options = {}) {
|
|
|
174
127
|
return getProjectSessionStateScope(cwd, options).statePath
|
|
175
128
|
}
|
|
176
129
|
|
|
130
|
+
export function getProjectEvidenceDir(cwd, options = {}) {
|
|
131
|
+
return getProjectSessionScope(cwd, normalizeRuntimeOptions(options)).artifactsDir
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getProjectEvidencePath(cwd, fileName, options = {}) {
|
|
135
|
+
return getSessionArtifactPath(cwd, fileName, options)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getProjectEvidenceRelativePath(cwd, fileName, options = {}) {
|
|
139
|
+
return getSessionArtifactRelativePath(cwd, fileName, options)
|
|
140
|
+
}
|
|
141
|
+
|
|
177
142
|
export function isRepoSharedProjectStore(cwd) {
|
|
178
143
|
return getProjectStoreMode(cwd) === 'repo-shared'
|
|
179
144
|
}
|
|
@@ -191,6 +156,7 @@ export function getProjectStoreSummary(cwd, options = {}) {
|
|
|
191
156
|
const activationDir = getProjectActivationDir(cwd)
|
|
192
157
|
const storeDir = getProjectStoreDir(cwd)
|
|
193
158
|
const stateScope = getProjectSessionStateScope(cwd, options)
|
|
159
|
+
const artifactsDir = getProjectEvidenceDir(cwd, options)
|
|
194
160
|
const projectKey = buildProjectKey(cwd)
|
|
195
161
|
const projectStoreMode = getProjectStoreMode(cwd)
|
|
196
162
|
|
|
@@ -204,6 +170,7 @@ export function getProjectStoreSummary(cwd, options = {}) {
|
|
|
204
170
|
stateSessionMode: stateScope.stateSessionMode,
|
|
205
171
|
stateBranch: stateScope.stateBranch,
|
|
206
172
|
sessionStateDir: stateScope.sessionDir,
|
|
173
|
+
artifactsDir,
|
|
207
174
|
usesSharedStore: projectStoreMode === 'repo-shared',
|
|
208
175
|
projectKey: projectKey.key,
|
|
209
176
|
repoRoot: projectKey.repoRoot,
|
|
@@ -212,6 +179,7 @@ export function getProjectStoreSummary(cwd, options = {}) {
|
|
|
212
179
|
promptStoreDir: formatPromptPath(storeDir),
|
|
213
180
|
promptStatePath: formatPromptPath(stateScope.statePath),
|
|
214
181
|
promptSessionStateDir: formatPromptPath(stateScope.sessionDir),
|
|
182
|
+
promptArtifactsDir: formatPromptPath(artifactsDir),
|
|
215
183
|
}
|
|
216
184
|
}
|
|
217
185
|
|
|
@@ -282,7 +250,7 @@ export function buildProjectStorageHint(cwd, options = {}) {
|
|
|
282
250
|
hints.push(`当前宿主未提供稳定会话标识,因此使用分支默认位置 \`${summary.stateSessionToken}\``)
|
|
283
251
|
}
|
|
284
252
|
if (summary.usesSharedStore) {
|
|
285
|
-
hints.push(`项目存储:\`project_store_mode=repo-shared
|
|
253
|
+
hints.push(`项目存储:\`project_store_mode=repo-shared\`;本地激活/会话运行态目录仍是 \`${summary.promptActivationDir}\`,知识库/方案目录改为 \`${summary.promptStoreDir}\``)
|
|
286
254
|
}
|
|
287
255
|
return hints.join('。') + (hints.length > 0 ? '。' : '')
|
|
288
256
|
}
|
|
@@ -302,6 +270,7 @@ export function buildProjectStorageBlock(cwd, options = {}) {
|
|
|
302
270
|
state_session_token: summary.stateSessionToken,
|
|
303
271
|
state_session_mode: summary.stateSessionMode,
|
|
304
272
|
session_state_dir: summary.promptSessionStateDir,
|
|
273
|
+
artifacts_dir: summary.promptArtifactsDir,
|
|
305
274
|
knowledge_base_dir: summary.promptStoreDir,
|
|
306
275
|
uses_shared_store: summary.usesSharedStore,
|
|
307
276
|
}
|
|
@@ -312,7 +281,7 @@ export function buildProjectStorageBlock(cwd, options = {}) {
|
|
|
312
281
|
explanations.push('说明:当前宿主未提供稳定会话标识,因此使用分支默认位置。')
|
|
313
282
|
}
|
|
314
283
|
if (summary.usesSharedStore) {
|
|
315
|
-
explanations.push('
|
|
284
|
+
explanations.push('说明:状态文件与会话产物写本地激活目录;`context.md`、`guidelines.md`、`DESIGN.md`、`verify.yaml`、`modules/`、`plans/`、`archive/` 写知识库/方案目录。')
|
|
316
285
|
} else {
|
|
317
286
|
explanations.push('说明:当前使用项目本地 `.helloagents/` 作为激活目录、知识库目录和方案目录。')
|
|
318
287
|
}
|
package/scripts/ralph-loop.mjs
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
* Runs on SubagentStop (Claude Code) and Stop (Codex CLI).
|
|
5
5
|
* Auto-detects lint/test commands and blocks if they fail.
|
|
6
6
|
*/
|
|
7
|
-
import { readFileSync
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { execSync } from 'node:child_process';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { clearVerifyEvidence, detectCommands, hasUnsafeVerifyCommand, writeVerifyEvidence } from './verify-state.mjs';
|
|
12
|
+
import {
|
|
13
|
+
getRuntimeEvidencePath,
|
|
14
|
+
readRuntimeEvidence,
|
|
15
|
+
writeRuntimeEvidence,
|
|
16
|
+
} from './runtime-artifacts.mjs';
|
|
12
17
|
|
|
13
18
|
const CONFIG_FILE = join(homedir(), '.helloagents', 'helloagents.json');
|
|
14
19
|
const CMD_TIMEOUT = 60_000; // 60s
|
|
@@ -27,28 +32,23 @@ function readSettings() {
|
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
// ── Circuit Breaker (consecutive failure tracking) ───────────────────
|
|
30
|
-
const BREAKER_FILE_NAME = '
|
|
35
|
+
const BREAKER_FILE_NAME = 'loop-breaker.json';
|
|
31
36
|
|
|
32
|
-
function getBreakerPath(cwd) {
|
|
33
|
-
return
|
|
37
|
+
function getBreakerPath(cwd, options = {}) {
|
|
38
|
+
return getRuntimeEvidencePath(cwd, BREAKER_FILE_NAME, options);
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
function readBreaker(cwd) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} catch {
|
|
40
|
-
return { consecutive_failures: 0, last_failure: null };
|
|
41
|
-
}
|
|
41
|
+
function readBreaker(cwd, options = {}) {
|
|
42
|
+
return readRuntimeEvidence(cwd, BREAKER_FILE_NAME, options)
|
|
43
|
+
|| { consecutive_failures: 0, last_failure: null };
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
function writeBreaker(cwd, state) {
|
|
45
|
-
|
|
46
|
-
try { mkdirSync(dir, { recursive: true }); } catch {}
|
|
47
|
-
writeFileSync(getBreakerPath(cwd), JSON.stringify(state, null, 2));
|
|
46
|
+
function writeBreaker(cwd, state, options = {}) {
|
|
47
|
+
writeRuntimeEvidence(cwd, BREAKER_FILE_NAME, state, options);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function resetBreaker(cwd) {
|
|
51
|
-
writeBreaker(cwd, { consecutive_failures: 0, last_failure: null });
|
|
50
|
+
function resetBreaker(cwd, options = {}) {
|
|
51
|
+
writeBreaker(cwd, { consecutive_failures: 0, last_failure: null }, options);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// ── Progress Detection (git diff check) ──────────────────────────────
|
|
@@ -74,7 +74,7 @@ function runVerify(commands, cwd) {
|
|
|
74
74
|
const failures = [];
|
|
75
75
|
for (const cmd of commands) {
|
|
76
76
|
if (hasUnsafeVerifyCommand([cmd])) {
|
|
77
|
-
failures.push({ cmd, output: '
|
|
77
|
+
failures.push({ cmd, output: '已阻止:验证命令不允许使用 shell 组合操作符' });
|
|
78
78
|
continue;
|
|
79
79
|
}
|
|
80
80
|
try {
|
|
@@ -85,8 +85,8 @@ function runVerify(commands, cwd) {
|
|
|
85
85
|
continue;
|
|
86
86
|
}
|
|
87
87
|
let output = ((err.stdout || '') + (err.stderr || '')).trim();
|
|
88
|
-
if (output.length > 1000) output = output.slice(0, 1000) + '\n
|
|
89
|
-
failures.push({ cmd, output: output ||
|
|
88
|
+
if (output.length > 1000) output = output.slice(0, 1000) + '\n…已截断';
|
|
89
|
+
failures.push({ cmd, output: output || `退出码 ${err.status}` });
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
return failures;
|
|
@@ -94,13 +94,13 @@ function runVerify(commands, cwd) {
|
|
|
94
94
|
|
|
95
95
|
// ── Result Handlers ──────────────────────────────────────────────────
|
|
96
96
|
|
|
97
|
-
function handleSuccess(cwd, isSubagent) {
|
|
98
|
-
resetBreaker(cwd);
|
|
97
|
+
function handleSuccess(cwd, isSubagent, options = {}) {
|
|
98
|
+
resetBreaker(cwd, options);
|
|
99
99
|
writeVerifyEvidence(cwd, {
|
|
100
100
|
commands: detectCommands(cwd),
|
|
101
101
|
fastOnly: isSubagent,
|
|
102
102
|
source: isSubagent ? 'subagent' : 'stop',
|
|
103
|
-
});
|
|
103
|
+
}, options);
|
|
104
104
|
|
|
105
105
|
if (isSubagent) {
|
|
106
106
|
process.stdout.write(JSON.stringify({
|
|
@@ -127,12 +127,12 @@ function handleSuccess(cwd, isSubagent) {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
function handleFailure(failures, cwd) {
|
|
131
|
-
clearVerifyEvidence(cwd);
|
|
132
|
-
const breaker = readBreaker(cwd);
|
|
130
|
+
function handleFailure(failures, cwd, options = {}) {
|
|
131
|
+
clearVerifyEvidence(cwd, options);
|
|
132
|
+
const breaker = readBreaker(cwd, options);
|
|
133
133
|
breaker.consecutive_failures += 1;
|
|
134
134
|
breaker.last_failure = new Date().toISOString();
|
|
135
|
-
writeBreaker(cwd, breaker);
|
|
135
|
+
writeBreaker(cwd, breaker, options);
|
|
136
136
|
|
|
137
137
|
const breakerWarning = breaker.consecutive_failures >= 3
|
|
138
138
|
? `\n\n⚠️ [断路器] 已连续 ${breaker.consecutive_failures} 次验证失败。当前修复思路可能有误,建议:\n 1. 重新分析根因,不要继续在同一方向上硬修\n 2. 检查是否存在架构层面的问题\n 3. 考虑回退到上一个正常状态重新开始`
|
|
@@ -141,7 +141,7 @@ function handleFailure(failures, cwd) {
|
|
|
141
141
|
const details = failures.map(f => `\u2717 ${f.cmd}\n${f.output}`).join('\n\n');
|
|
142
142
|
process.stdout.write(JSON.stringify({
|
|
143
143
|
decision: 'block',
|
|
144
|
-
reason: `[Ralph Loop]
|
|
144
|
+
reason: `[Ralph Loop] 验证失败:\n\n${details}\n\n请先修复以上问题,再报告完成。${breakerWarning}`,
|
|
145
145
|
suppressOutput: true,
|
|
146
146
|
}));
|
|
147
147
|
}
|
|
@@ -175,6 +175,7 @@ async function main() {
|
|
|
175
175
|
let data = {};
|
|
176
176
|
try { data = JSON.parse(readFileSync(0, 'utf-8')); } catch {}
|
|
177
177
|
const cwd = data.cwd || process.cwd();
|
|
178
|
+
const runtimeOptions = { payload: data };
|
|
178
179
|
|
|
179
180
|
let commands = detectCommands(cwd);
|
|
180
181
|
if (!commands?.length) {
|
|
@@ -188,10 +189,14 @@ async function main() {
|
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
const failures = runVerify(commands, cwd);
|
|
191
|
-
if (failures.length === 0) handleSuccess(cwd, IS_SUBAGENT);
|
|
192
|
-
else handleFailure(failures, cwd);
|
|
192
|
+
if (failures.length === 0) handleSuccess(cwd, IS_SUBAGENT, runtimeOptions);
|
|
193
|
+
else handleFailure(failures, cwd, runtimeOptions);
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
main().catch(() => {
|
|
196
|
-
process.stdout.write(JSON.stringify({
|
|
196
|
+
main().catch((error) => {
|
|
197
|
+
process.stdout.write(JSON.stringify({
|
|
198
|
+
decision: 'block',
|
|
199
|
+
reason: `[Ralph Loop] 验证脚本执行异常,已暂停完成通知。\n原因:${error?.message || error}`,
|
|
200
|
+
suppressOutput: true,
|
|
201
|
+
}));
|
|
197
202
|
});
|