helloloop 0.9.1 → 0.10.0
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/plugin.json +1 -1
- package/README.md +230 -506
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/native/windows-hidden-shell-proxy/HelloLoopHiddenShellProxy.csproj +11 -0
- package/native/windows-hidden-shell-proxy/Program.cs +498 -0
- package/package.json +4 -2
- package/src/activity_projection.mjs +294 -0
- package/src/analyze_confirmation.mjs +3 -1
- package/src/analyzer.mjs +2 -1
- package/src/auto_execution_options.mjs +13 -0
- package/src/background_launch.mjs +73 -0
- package/src/cli.mjs +49 -1
- package/src/cli_analyze_command.mjs +9 -5
- package/src/cli_args.mjs +102 -37
- package/src/cli_command_handlers.mjs +44 -4
- package/src/cli_support.mjs +2 -0
- package/src/dashboard_command.mjs +371 -0
- package/src/dashboard_tui.mjs +289 -0
- package/src/dashboard_web.mjs +351 -0
- package/src/dashboard_web_client.mjs +167 -0
- package/src/dashboard_web_page.mjs +49 -0
- package/src/engine_event_parser_codex.mjs +167 -0
- package/src/engine_process_support.mjs +1 -0
- package/src/engine_selection.mjs +24 -0
- package/src/engine_selection_probe.mjs +10 -6
- package/src/engine_selection_settings.mjs +12 -19
- package/src/execution_interactivity.mjs +12 -0
- package/src/host_continuation.mjs +305 -0
- package/src/install_codex.mjs +20 -8
- package/src/install_shared.mjs +9 -0
- package/src/node_process_launch.mjs +28 -0
- package/src/process.mjs +2 -0
- package/src/runner_execute_task.mjs +4 -0
- package/src/runner_execution_support.mjs +69 -3
- package/src/runner_once.mjs +4 -0
- package/src/runner_status.mjs +63 -7
- package/src/runtime_engine_support.mjs +41 -4
- package/src/runtime_engine_task.mjs +7 -0
- package/src/runtime_settings.mjs +105 -0
- package/src/runtime_settings_loader.mjs +19 -0
- package/src/shell_invocation.mjs +227 -9
- package/src/supervisor_cli_support.mjs +3 -2
- package/src/supervisor_guardian.mjs +307 -0
- package/src/supervisor_runtime.mjs +138 -82
- package/src/supervisor_state.mjs +64 -0
- package/src/supervisor_watch.mjs +92 -48
- package/src/terminal_session_limits.mjs +1 -21
- package/src/windows_hidden_shell_proxy.mjs +405 -0
- package/src/workspace_registry.mjs +155 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { ensureDir, fileExists, nowIso, readJson, sleep, writeJson, writeText } from "./common.mjs";
|
|
4
|
+
import { createContext } from "./context.mjs";
|
|
5
|
+
import { collectRepoStatusSnapshot } from "./runner_status.mjs";
|
|
6
|
+
|
|
7
|
+
const RESUME_DIR_NAME = "host-resume";
|
|
8
|
+
const SNAPSHOT_FILE_NAME = "resume.json";
|
|
9
|
+
const PROMPT_FILE_NAME = "resume-prompt.md";
|
|
10
|
+
|
|
11
|
+
const ISSUE_MATCHERS = [
|
|
12
|
+
{
|
|
13
|
+
code: "rate_limit",
|
|
14
|
+
label: "429 / 限流或临时容量不足",
|
|
15
|
+
patterns: ["429", "too many requests", "rate limit", "retry limit", "capacity"],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
code: "auth",
|
|
19
|
+
label: "403 / 鉴权、订阅或权限异常",
|
|
20
|
+
patterns: ["403", "forbidden", "subscription_not_found", "subscription", "not authenticated"],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
code: "server",
|
|
24
|
+
label: "5xx / 服务端暂时异常",
|
|
25
|
+
patterns: ["500", "502", "503", "504", "server error", "service unavailable", "bad gateway"],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
code: "network",
|
|
29
|
+
label: "网络或流中断",
|
|
30
|
+
patterns: ["network", "timeout", "timed out", "connection reset", "stream closed", "socket"],
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function resumeRoot(context) {
|
|
35
|
+
return path.join(context.configRoot, RESUME_DIR_NAME);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resumeFiles(context) {
|
|
39
|
+
const root = resumeRoot(context);
|
|
40
|
+
return {
|
|
41
|
+
root,
|
|
42
|
+
snapshotFile: path.join(root, SNAPSHOT_FILE_NAME),
|
|
43
|
+
promptFile: path.join(root, PROMPT_FILE_NAME),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeText(value) {
|
|
48
|
+
return String(value || "").trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findIssue(summaryText) {
|
|
52
|
+
const normalized = normalizeText(summaryText).toLowerCase();
|
|
53
|
+
if (!normalized) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const matcher of ISSUE_MATCHERS) {
|
|
58
|
+
if (matcher.patterns.some((pattern) => normalized.includes(String(pattern).toLowerCase()))) {
|
|
59
|
+
return {
|
|
60
|
+
code: matcher.code,
|
|
61
|
+
label: matcher.label,
|
|
62
|
+
summary: normalizeText(summaryText),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
code: "unknown",
|
|
69
|
+
label: "宿主中断或未知异常",
|
|
70
|
+
summary: normalizeText(summaryText),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function shouldInferIssue(snapshot, summaryText) {
|
|
75
|
+
const normalized = normalizeText(summaryText).toLowerCase();
|
|
76
|
+
if (!normalized) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (["failed", "paused_manual", "probe_failed", "retry_waiting", "stopped_host_closed"].includes(
|
|
81
|
+
normalizeText(snapshot?.runtime?.status),
|
|
82
|
+
)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (["failed", "stopped"].includes(normalizeText(snapshot?.supervisor?.status))) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return ISSUE_MATCHERS.some((matcher) => matcher.patterns.some((pattern) => normalized.includes(String(pattern).toLowerCase())));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildCurrentAction(snapshot) {
|
|
93
|
+
return normalizeText(
|
|
94
|
+
snapshot?.activity?.current?.label
|
|
95
|
+
|| snapshot?.runtime?.failureReason
|
|
96
|
+
|| snapshot?.latestStatus?.message
|
|
97
|
+
|| snapshot?.runtime?.status
|
|
98
|
+
|| snapshot?.supervisor?.status
|
|
99
|
+
|| "等待新事件",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildRecentFiles(snapshot) {
|
|
104
|
+
const changes = Array.isArray(snapshot?.activity?.recentFileChanges)
|
|
105
|
+
? snapshot.activity.recentFileChanges
|
|
106
|
+
: [];
|
|
107
|
+
const firstGroup = changes.find((item) => Array.isArray(item?.changes) && item.changes.length);
|
|
108
|
+
if (!firstGroup) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
return firstGroup.changes.slice(0, 5).map((item) => `${item.kind}:${item.path}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildPromptLines(context, hostResume) {
|
|
115
|
+
const lines = [
|
|
116
|
+
"继续刚才被主 CLI / 主终端中断的 HelloLoop 会话,不要从头重新分析,不要忘记刚才中断前的任务。",
|
|
117
|
+
`目标仓库:${context.repoRoot}`,
|
|
118
|
+
`后台会话 ID:${hostResume.sessionId || "unknown"}`,
|
|
119
|
+
`后台命令:${hostResume.command || "unknown"}`,
|
|
120
|
+
`后台状态:${hostResume.supervisorStatus || "unknown"}`,
|
|
121
|
+
`当前任务:${hostResume.taskTitle || "无"}`,
|
|
122
|
+
`当前阶段:${hostResume.stage || "unknown"}`,
|
|
123
|
+
`当前动作:${hostResume.currentAction || "等待新事件"}`,
|
|
124
|
+
`当前运行状态:${hostResume.runtimeStatus || "idle"}`,
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
if (hostResume.issue?.label) {
|
|
128
|
+
lines.push(`最近宿主异常:${hostResume.issue.label}`);
|
|
129
|
+
}
|
|
130
|
+
if (hostResume.issue?.summary) {
|
|
131
|
+
lines.push(`异常摘要:${hostResume.issue.summary}`);
|
|
132
|
+
}
|
|
133
|
+
if (hostResume.todoLabel) {
|
|
134
|
+
lines.push(`当前待办进度:${hostResume.todoLabel}`);
|
|
135
|
+
}
|
|
136
|
+
if (hostResume.nextTaskTitle) {
|
|
137
|
+
lines.push(`下一任务:${hostResume.nextTaskTitle}`);
|
|
138
|
+
}
|
|
139
|
+
if (hostResume.recentFiles.length) {
|
|
140
|
+
lines.push(`最近文件变化:${hostResume.recentFiles.join(" | ")}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
lines.push("");
|
|
144
|
+
lines.push("接续规则:");
|
|
145
|
+
lines.push("1. 先读取 HelloLoop 当前状态;优先运行 `helloloop dashboard --json`,或至少运行 `helloloop status`。");
|
|
146
|
+
if (hostResume.supervisorActive) {
|
|
147
|
+
lines.push("2. 如果后台 supervisor 仍在运行,不要重复启动新的主线,只接续观察、汇报和必要控制。");
|
|
148
|
+
} else {
|
|
149
|
+
lines.push("2. 如果后台 supervisor 已停止,基于当前仓库和 backlog 从中断位置继续,不要重建无关任务。");
|
|
150
|
+
}
|
|
151
|
+
lines.push("3. 如果刚才是因为 429 / 403 / 网络抖动等宿主级异常中断,本轮要直接承接原任务,不要要求用户重复描述。");
|
|
152
|
+
lines.push("4. 只有发现后台已停且需要恢复时,才重新执行 `helloloop run-loop` / `helloloop run-once` / `helloloop analyze -y`。");
|
|
153
|
+
lines.push("5. 用户当前明确要求:继续刚才被中断之前的任务,并持续推进上面的开发工作。");
|
|
154
|
+
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function buildHostContinuationSnapshot(context, options = {}) {
|
|
159
|
+
const snapshot = options.snapshot || collectRepoStatusSnapshot(context, options);
|
|
160
|
+
const sessionId = normalizeText(options.sessionId || snapshot?.supervisor?.sessionId);
|
|
161
|
+
const supervisorStatus = normalizeText(snapshot?.supervisor?.status);
|
|
162
|
+
const runtimeStatus = normalizeText(snapshot?.runtime?.status || snapshot?.latestStatus?.stage || "idle");
|
|
163
|
+
const currentAction = buildCurrentAction(snapshot);
|
|
164
|
+
const todoCompleted = Number(snapshot?.activity?.todo?.completed || 0);
|
|
165
|
+
const todoTotal = Number(snapshot?.activity?.todo?.total || 0);
|
|
166
|
+
const todoLabel = todoTotal > 0 ? `${todoCompleted}/${todoTotal}` : "";
|
|
167
|
+
const issueSummary = [
|
|
168
|
+
snapshot?.runtime?.failureReason,
|
|
169
|
+
snapshot?.latestStatus?.message,
|
|
170
|
+
snapshot?.runtime?.status,
|
|
171
|
+
currentAction,
|
|
172
|
+
].filter(Boolean).join("\n");
|
|
173
|
+
const issue = shouldInferIssue(snapshot, issueSummary)
|
|
174
|
+
? findIssue(issueSummary)
|
|
175
|
+
: null;
|
|
176
|
+
const recentFiles = buildRecentFiles(snapshot);
|
|
177
|
+
|
|
178
|
+
const hostResume = {
|
|
179
|
+
schemaVersion: 1,
|
|
180
|
+
generatedAt: nowIso(),
|
|
181
|
+
repoRoot: context.repoRoot,
|
|
182
|
+
repoName: path.basename(context.repoRoot),
|
|
183
|
+
sessionId,
|
|
184
|
+
command: normalizeText(snapshot?.supervisor?.command),
|
|
185
|
+
supervisorStatus,
|
|
186
|
+
supervisorActive: ["launching", "running"].includes(supervisorStatus),
|
|
187
|
+
stage: normalizeText(snapshot?.latestStatus?.stage),
|
|
188
|
+
taskId: snapshot?.latestStatus?.taskId || "",
|
|
189
|
+
taskTitle: normalizeText(snapshot?.latestStatus?.taskTitle),
|
|
190
|
+
runtimeStatus,
|
|
191
|
+
currentAction,
|
|
192
|
+
todoLabel,
|
|
193
|
+
issue,
|
|
194
|
+
nextTaskId: snapshot?.nextTask?.id || "",
|
|
195
|
+
nextTaskTitle: normalizeText(snapshot?.nextTask?.title),
|
|
196
|
+
recentFiles,
|
|
197
|
+
summary: snapshot?.summary || null,
|
|
198
|
+
prompt: "",
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
hostResume.prompt = buildPromptLines(context, hostResume).join("\n");
|
|
202
|
+
return hostResume;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function refreshHostContinuationArtifacts(context, options = {}) {
|
|
206
|
+
const hostResume = buildHostContinuationSnapshot(context, options);
|
|
207
|
+
const files = resumeFiles(context);
|
|
208
|
+
ensureDir(files.root);
|
|
209
|
+
writeJson(files.snapshotFile, hostResume);
|
|
210
|
+
writeText(files.promptFile, `${hostResume.prompt}\n`);
|
|
211
|
+
return {
|
|
212
|
+
...hostResume,
|
|
213
|
+
snapshotFile: files.snapshotFile,
|
|
214
|
+
promptFile: files.promptFile,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function readHostContinuationSnapshot(context, options = {}) {
|
|
219
|
+
const files = resumeFiles(context);
|
|
220
|
+
if (!options.refresh && fileExists(files.snapshotFile)) {
|
|
221
|
+
try {
|
|
222
|
+
const loaded = readJson(files.snapshotFile);
|
|
223
|
+
return {
|
|
224
|
+
...loaded,
|
|
225
|
+
snapshotFile: files.snapshotFile,
|
|
226
|
+
promptFile: files.promptFile,
|
|
227
|
+
};
|
|
228
|
+
} catch {
|
|
229
|
+
// fallback to rebuilding below
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return refreshHostContinuationArtifacts(context, options);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function renderHostContinuationText(hostResume) {
|
|
236
|
+
return [
|
|
237
|
+
"HelloLoop 宿主续跑提示",
|
|
238
|
+
"======================",
|
|
239
|
+
`仓库:${hostResume.repoRoot}`,
|
|
240
|
+
`后台会话:${hostResume.sessionId || "unknown"}`,
|
|
241
|
+
`后台命令:${hostResume.command || "unknown"}`,
|
|
242
|
+
`后台状态:${hostResume.supervisorStatus || "unknown"}`,
|
|
243
|
+
`当前任务:${hostResume.taskTitle || "无"}`,
|
|
244
|
+
`当前阶段:${hostResume.stage || "unknown"}`,
|
|
245
|
+
`当前动作:${hostResume.currentAction || "等待新事件"}`,
|
|
246
|
+
...(hostResume.issue?.label ? [`最近异常:${hostResume.issue.label}`] : []),
|
|
247
|
+
...(hostResume.todoLabel ? [`当前待办:${hostResume.todoLabel}`] : []),
|
|
248
|
+
...(hostResume.nextTaskTitle ? [`下一任务:${hostResume.nextTaskTitle}`] : []),
|
|
249
|
+
"",
|
|
250
|
+
hostResume.prompt,
|
|
251
|
+
].filter(Boolean).join("\n");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildResumeSignature(hostResume) {
|
|
255
|
+
return JSON.stringify({
|
|
256
|
+
sessionId: hostResume.sessionId,
|
|
257
|
+
supervisorStatus: hostResume.supervisorStatus,
|
|
258
|
+
taskId: hostResume.taskId,
|
|
259
|
+
stage: hostResume.stage,
|
|
260
|
+
runtimeStatus: hostResume.runtimeStatus,
|
|
261
|
+
currentAction: hostResume.currentAction,
|
|
262
|
+
todoLabel: hostResume.todoLabel,
|
|
263
|
+
issueCode: hostResume.issue?.code || "",
|
|
264
|
+
nextTaskId: hostResume.nextTaskId,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function runHostContinuationCommand(context, options = {}) {
|
|
269
|
+
const pollMs = Math.max(500, Number(options.pollMs || options.watchPollMs || 2000));
|
|
270
|
+
let previousSignature = "";
|
|
271
|
+
|
|
272
|
+
while (true) {
|
|
273
|
+
const hostResume = refreshHostContinuationArtifacts(context, options);
|
|
274
|
+
const signature = buildResumeSignature(hostResume);
|
|
275
|
+
|
|
276
|
+
if (signature !== previousSignature || !options.watch) {
|
|
277
|
+
previousSignature = signature;
|
|
278
|
+
if (options.json) {
|
|
279
|
+
console.log(JSON.stringify(hostResume));
|
|
280
|
+
} else {
|
|
281
|
+
if (options.watch && process.stdout.isTTY) {
|
|
282
|
+
process.stdout.write("\x1bc");
|
|
283
|
+
}
|
|
284
|
+
console.log(renderHostContinuationText(hostResume));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!options.watch) {
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
await sleep(pollMs);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function buildDashboardHostContinuation(entry, repoSnapshot) {
|
|
297
|
+
const context = createContext({
|
|
298
|
+
repoRoot: entry.repoRoot,
|
|
299
|
+
configDirName: entry.configDirName,
|
|
300
|
+
});
|
|
301
|
+
return buildHostContinuationSnapshot(context, {
|
|
302
|
+
snapshot: repoSnapshot,
|
|
303
|
+
sessionId: entry.sessionId,
|
|
304
|
+
});
|
|
305
|
+
}
|
package/src/install_codex.mjs
CHANGED
|
@@ -5,9 +5,11 @@ import {
|
|
|
5
5
|
assertPathInside,
|
|
6
6
|
codexBundleEntries,
|
|
7
7
|
copyBundleEntries,
|
|
8
|
+
isSamePath,
|
|
8
9
|
readExistingJsonOrThrow,
|
|
9
10
|
removePathIfExists,
|
|
10
11
|
removeTargetIfNeeded,
|
|
12
|
+
runtimeBundleEntries,
|
|
11
13
|
resolveCodexLocalRoot,
|
|
12
14
|
resolveHomeDir,
|
|
13
15
|
} from "./install_shared.mjs";
|
|
@@ -145,6 +147,8 @@ export function installCodexHost(bundleRoot, options = {}) {
|
|
|
145
147
|
const configFile = path.join(resolvedCodexHome, "config.toml");
|
|
146
148
|
const manifestFile = path.join(bundleRoot, ".codex-plugin", "plugin.json");
|
|
147
149
|
const existingMarketplace = readExistingJsonOrThrow(marketplaceFile, "Codex marketplace 配置");
|
|
150
|
+
const bundleIsPluginRoot = isSamePath(bundleRoot, targetPluginRoot);
|
|
151
|
+
const bundleIsInstalledRoot = isSamePath(bundleRoot, targetInstalledPluginRoot);
|
|
148
152
|
|
|
149
153
|
if (!fileExists(manifestFile)) {
|
|
150
154
|
throw new Error(`未找到 Codex 插件 manifest:${manifestFile}`);
|
|
@@ -152,16 +156,24 @@ export function installCodexHost(bundleRoot, options = {}) {
|
|
|
152
156
|
|
|
153
157
|
assertPathInside(resolvedLocalRoot, targetPluginRoot, "Codex 本地插件目录");
|
|
154
158
|
assertPathInside(resolvedCodexHome, targetPluginCacheRoot, "Codex 插件缓存目录");
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
if (!bundleIsPluginRoot) {
|
|
160
|
+
removeTargetIfNeeded(targetPluginRoot, options.force);
|
|
161
|
+
}
|
|
162
|
+
if (!bundleIsInstalledRoot) {
|
|
163
|
+
removeTargetIfNeeded(targetPluginCacheRoot, options.force);
|
|
164
|
+
}
|
|
157
165
|
|
|
158
166
|
ensureDir(targetPluginsRoot);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
if (!bundleIsPluginRoot) {
|
|
168
|
+
ensureDir(targetPluginRoot);
|
|
169
|
+
copyBundleEntries(bundleRoot, targetPluginRoot, runtimeBundleEntries);
|
|
170
|
+
removePathIfExists(path.join(targetPluginRoot, ".git"));
|
|
171
|
+
}
|
|
172
|
+
if (!bundleIsInstalledRoot) {
|
|
173
|
+
ensureDir(targetInstalledPluginRoot);
|
|
174
|
+
copyBundleEntries(bundleRoot, targetInstalledPluginRoot, codexBundleEntries);
|
|
175
|
+
removePathIfExists(path.join(targetInstalledPluginRoot, ".git"));
|
|
176
|
+
}
|
|
165
177
|
|
|
166
178
|
ensureDir(path.dirname(marketplaceFile));
|
|
167
179
|
updateCodexMarketplace(marketplaceFile, existingMarketplace);
|
package/src/install_shared.mjs
CHANGED
|
@@ -11,6 +11,7 @@ export const runtimeBundleEntries = [
|
|
|
11
11
|
"README.md",
|
|
12
12
|
"bin",
|
|
13
13
|
"hosts",
|
|
14
|
+
"native",
|
|
14
15
|
"package.json",
|
|
15
16
|
"scripts",
|
|
16
17
|
"skills",
|
|
@@ -46,6 +47,14 @@ export function assertPathInside(parentDir, targetDir, label) {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
export function isSamePath(leftPath, rightPath) {
|
|
51
|
+
if (!leftPath || !rightPath) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const normalize = (value) => path.resolve(value).replace(/[\\\/]+$/u, "").toLowerCase();
|
|
55
|
+
return normalize(leftPath) === normalize(rightPath);
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
function sleepSync(ms) {
|
|
50
59
|
const shared = new SharedArrayBuffer(4);
|
|
51
60
|
const view = new Int32Array(shared);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
HIDDEN_PROCESS_PROXY_TARGET_ENV,
|
|
5
|
+
resolveWindowsHiddenProcessProxyExecutable,
|
|
6
|
+
} from "./windows_hidden_shell_proxy.mjs";
|
|
7
|
+
|
|
8
|
+
export function spawnNodeProcess(options = {}) {
|
|
9
|
+
const useWindowsHiddenProxy = process.platform === "win32";
|
|
10
|
+
const command = useWindowsHiddenProxy
|
|
11
|
+
? resolveWindowsHiddenProcessProxyExecutable()
|
|
12
|
+
: process.execPath;
|
|
13
|
+
|
|
14
|
+
return spawn(command, Array.isArray(options.args) ? options.args : [], {
|
|
15
|
+
cwd: options.cwd || process.cwd(),
|
|
16
|
+
detached: options.detached === true,
|
|
17
|
+
shell: false,
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
stdio: options.stdio || "pipe",
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
...(options.env || {}),
|
|
23
|
+
...(useWindowsHiddenProxy
|
|
24
|
+
? { [HIDDEN_PROCESS_PROXY_TARGET_ENV]: process.execPath }
|
|
25
|
+
: {}),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
package/src/process.mjs
CHANGED
|
@@ -19,6 +19,7 @@ export async function runCodexExec({ context, prompt, runDir, policy, hostLease
|
|
|
19
19
|
policy,
|
|
20
20
|
executionMode: "execute",
|
|
21
21
|
outputPrefix: "codex",
|
|
22
|
+
skipGitRepoCheck: true,
|
|
22
23
|
hostLease,
|
|
23
24
|
});
|
|
24
25
|
}
|
|
@@ -32,6 +33,7 @@ export async function runEngineExec({ engine, context, prompt, runDir, policy, h
|
|
|
32
33
|
policy,
|
|
33
34
|
executionMode: "execute",
|
|
34
35
|
outputPrefix: engine,
|
|
36
|
+
skipGitRepoCheck: engine === "codex",
|
|
35
37
|
hostLease,
|
|
36
38
|
});
|
|
37
39
|
}
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { rememberEngineSelection } from "./engine_selection.mjs";
|
|
4
4
|
import { getEngineDisplayName } from "./engine_metadata.mjs";
|
|
5
5
|
import { ensureDir, nowIso, writeText } from "./common.mjs";
|
|
6
|
+
import { refreshHostContinuationArtifacts } from "./host_continuation.mjs";
|
|
6
7
|
import { isHostLeaseAlive } from "./host_lease.mjs";
|
|
7
8
|
import { saveBacklog, writeStatus } from "./config.mjs";
|
|
8
9
|
import { reviewTaskCompletion } from "./completion_review.mjs";
|
|
@@ -277,6 +278,9 @@ export async function executeSingleTask(context, options = {}) {
|
|
|
277
278
|
summary: "",
|
|
278
279
|
message: `开始执行任务:${execution.task.title}`,
|
|
279
280
|
});
|
|
281
|
+
refreshHostContinuationArtifacts(context, {
|
|
282
|
+
sessionId: options.supervisorSessionId || "",
|
|
283
|
+
});
|
|
280
284
|
|
|
281
285
|
const state = {
|
|
282
286
|
engineResolution: execution.engineResolution,
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
rememberEngineSelection,
|
|
3
3
|
resolveEngineSelection,
|
|
4
4
|
} from "./engine_selection.mjs";
|
|
5
|
-
import { nowIso } from "./common.mjs";
|
|
5
|
+
import { fileExists, nowIso, readJson } from "./common.mjs";
|
|
6
6
|
import {
|
|
7
7
|
loadBacklog,
|
|
8
8
|
loadPolicy,
|
|
@@ -13,6 +13,72 @@ import {
|
|
|
13
13
|
} from "./config.mjs";
|
|
14
14
|
import { getTask, selectNextTask, unresolvedDependencies, updateTask } from "./backlog.mjs";
|
|
15
15
|
import { makeRunDir } from "./runner_status.mjs";
|
|
16
|
+
import { shouldPromptForEngineSelection } from "./execution_interactivity.mjs";
|
|
17
|
+
|
|
18
|
+
function isPidAlive(pid) {
|
|
19
|
+
const value = Number(pid || 0);
|
|
20
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
process.kill(value, 0);
|
|
25
|
+
return true;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return String(error?.code || "") === "EPERM";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasLiveSupervisor(context) {
|
|
32
|
+
if (!fileExists(context.supervisorStateFile)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const supervisor = readJson(context.supervisorStateFile);
|
|
37
|
+
const status = String(supervisor?.status || "").trim();
|
|
38
|
+
if (!["launching", "running"].includes(status)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return isPidAlive(supervisor?.pid);
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function recoverStaleInProgressTasks(context, backlog, options = {}) {
|
|
48
|
+
const staleTasks = Array.isArray(backlog?.tasks)
|
|
49
|
+
? backlog.tasks.filter((task) => task?.status === "in_progress")
|
|
50
|
+
: [];
|
|
51
|
+
if (!staleTasks.length) {
|
|
52
|
+
return backlog;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let shouldRecover = !hasLiveSupervisor(context);
|
|
56
|
+
if (!shouldRecover && fileExists(context.statusFile)) {
|
|
57
|
+
try {
|
|
58
|
+
const latestStatus = readJson(context.statusFile);
|
|
59
|
+
const currentSessionId = String(options.supervisorSessionId || "").trim();
|
|
60
|
+
const recordedSessionId = String(latestStatus?.sessionId || "").trim();
|
|
61
|
+
if (currentSessionId && recordedSessionId && currentSessionId !== recordedSessionId) {
|
|
62
|
+
shouldRecover = true;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// ignore malformed status files and keep current decision
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!shouldRecover) {
|
|
69
|
+
return backlog;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const task of staleTasks) {
|
|
73
|
+
updateTask(backlog, task.id, {
|
|
74
|
+
status: "pending",
|
|
75
|
+
startedAt: "",
|
|
76
|
+
finishedAt: "",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
saveBacklog(context, backlog);
|
|
80
|
+
return backlog;
|
|
81
|
+
}
|
|
16
82
|
|
|
17
83
|
function resolveTask(backlog, options) {
|
|
18
84
|
if (options.taskId) {
|
|
@@ -28,7 +94,7 @@ function resolveTask(backlog, options) {
|
|
|
28
94
|
export async function resolveExecutionSetup(context, options = {}) {
|
|
29
95
|
const policy = loadPolicy(context);
|
|
30
96
|
const projectConfig = loadProjectConfig(context);
|
|
31
|
-
const backlog = loadBacklog(context);
|
|
97
|
+
const backlog = recoverStaleInProgressTasks(context, loadBacklog(context), options);
|
|
32
98
|
const task = resolveTask(backlog, options);
|
|
33
99
|
if (!task) {
|
|
34
100
|
return {
|
|
@@ -52,7 +118,7 @@ export async function resolveExecutionSetup(context, options = {}) {
|
|
|
52
118
|
context,
|
|
53
119
|
policy,
|
|
54
120
|
options,
|
|
55
|
-
interactive:
|
|
121
|
+
interactive: shouldPromptForEngineSelection(options),
|
|
56
122
|
});
|
|
57
123
|
|
|
58
124
|
return {
|
package/src/runner_once.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { selectNextTask, summarizeBacklog } from "./backlog.mjs";
|
|
2
2
|
import { loadBacklog, writeStateMarkdown, writeStatus } from "./config.mjs";
|
|
3
|
+
import { refreshHostContinuationArtifacts } from "./host_continuation.mjs";
|
|
3
4
|
import { executeSingleTask } from "./runner_execute_task.mjs";
|
|
4
5
|
import { renderStatusMarkdown } from "./runner_status.mjs";
|
|
5
6
|
|
|
@@ -25,6 +26,9 @@ export async function runOnce(context, options = {}) {
|
|
|
25
26
|
lastResult: result.ok ? "本轮成功" : (result.summary || result.kind),
|
|
26
27
|
nextTask,
|
|
27
28
|
}));
|
|
29
|
+
refreshHostContinuationArtifacts(context, {
|
|
30
|
+
sessionId: options.supervisorSessionId || "",
|
|
31
|
+
});
|
|
28
32
|
|
|
29
33
|
return result;
|
|
30
34
|
}
|
package/src/runner_status.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
readJsonIfExists,
|
|
5
|
+
selectLatestActivityFile,
|
|
6
|
+
selectLatestRuntimeFile,
|
|
7
|
+
} from "./activity_projection.mjs";
|
|
3
8
|
import { fileExists, readJson, sanitizeId, tailText, timestampForFile } from "./common.mjs";
|
|
4
9
|
import { renderTaskSummary, selectNextTask, summarizeBacklog } from "./backlog.mjs";
|
|
5
10
|
import { loadBacklog } from "./config.mjs";
|
|
@@ -83,16 +88,42 @@ export function renderStatusMarkdown(context, { summary, currentTask, lastResult
|
|
|
83
88
|
].join("\n");
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
export function
|
|
91
|
+
export function collectRepoStatusSnapshot(context, options = {}) {
|
|
87
92
|
const backlog = loadBacklog(context);
|
|
88
93
|
const summary = summarizeBacklog(backlog);
|
|
89
94
|
const nextTask = selectNextTask(backlog, options);
|
|
90
|
-
const supervisor = fileExists(context.supervisorStateFile)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
const supervisor = fileExists(context.supervisorStateFile) ? readJson(context.supervisorStateFile) : null;
|
|
96
|
+
const latestStatus = fileExists(context.statusFile) ? readJson(context.statusFile) : null;
|
|
97
|
+
const runtimeFile = latestStatus?.runDir ? selectLatestRuntimeFile(latestStatus.runDir) : "";
|
|
98
|
+
const runtime = readJsonIfExists(runtimeFile);
|
|
99
|
+
const activityFile = runtime?.activityFile && fileExists(runtime.activityFile)
|
|
100
|
+
? runtime.activityFile
|
|
101
|
+
: (latestStatus?.runDir ? selectLatestActivityFile(latestStatus.runDir, runtime?.attemptPrefix || "") : "");
|
|
102
|
+
const activity = readJsonIfExists(activityFile);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
summary,
|
|
106
|
+
nextTask,
|
|
107
|
+
supervisor,
|
|
108
|
+
latestStatus,
|
|
109
|
+
runtimeFile,
|
|
110
|
+
runtime,
|
|
111
|
+
activityFile,
|
|
112
|
+
activity,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renderStatusText(context, options = {}) {
|
|
117
|
+
const snapshot = collectRepoStatusSnapshot(context, options);
|
|
118
|
+
const {
|
|
119
|
+
summary,
|
|
120
|
+
nextTask,
|
|
121
|
+
supervisor,
|
|
122
|
+
latestStatus,
|
|
123
|
+
runtime,
|
|
124
|
+
activity,
|
|
125
|
+
} = snapshot;
|
|
126
|
+
const hostResume = options.hostResume || null;
|
|
96
127
|
|
|
97
128
|
return [
|
|
98
129
|
"HelloLoop 状态",
|
|
@@ -109,6 +140,9 @@ export function renderStatusText(context, options = {}) {
|
|
|
109
140
|
`后台会话:${supervisor.status}`,
|
|
110
141
|
`后台会话 ID:${supervisor.sessionId || "unknown"}`,
|
|
111
142
|
`后台租约:${renderHostLeaseLabel(supervisor.lease)}`,
|
|
143
|
+
...(Number.isFinite(Number(supervisor.guardianRestartCount)) && Number(supervisor.guardianRestartCount) > 0
|
|
144
|
+
? [`守护重拉起次数:${supervisor.guardianRestartCount}`]
|
|
145
|
+
: []),
|
|
112
146
|
]
|
|
113
147
|
: []),
|
|
114
148
|
...(latestStatus?.taskTitle
|
|
@@ -118,10 +152,32 @@ export function renderStatusText(context, options = {}) {
|
|
|
118
152
|
`当前运行阶段:${latestStatus.stage || "unknown"}`,
|
|
119
153
|
]
|
|
120
154
|
: []),
|
|
155
|
+
...(runtime?.status
|
|
156
|
+
? [
|
|
157
|
+
`当前引擎状态:${runtime.status}`,
|
|
158
|
+
...(Number.isFinite(Number(runtime.recoveryCount))
|
|
159
|
+
? [`自动恢复次数:${runtime.recoveryCount}`]
|
|
160
|
+
: []),
|
|
161
|
+
]
|
|
162
|
+
: []),
|
|
163
|
+
...(activity?.current?.label
|
|
164
|
+
? [`当前动作:${activity.current.label}`]
|
|
165
|
+
: []),
|
|
166
|
+
...(activity?.todo?.total
|
|
167
|
+
? [`当前待办:${activity.todo.completed}/${activity.todo.total}`]
|
|
168
|
+
: []),
|
|
169
|
+
...(Array.isArray(activity?.activeCommands) && activity.activeCommands[0]?.label
|
|
170
|
+
? [`活动命令:${activity.activeCommands[0].label}`]
|
|
171
|
+
: []),
|
|
172
|
+
...(hostResume?.issue?.label
|
|
173
|
+
? [`宿主续跑:${hostResume.issue.label}`]
|
|
174
|
+
: (hostResume?.supervisorActive ? ["宿主续跑:后台仍在运行,可直接接续观察"] : [])),
|
|
121
175
|
"",
|
|
122
176
|
nextTask ? "下一任务:" : "下一任务:无",
|
|
123
177
|
nextTask ? renderTaskSummary(nextTask) : "",
|
|
124
178
|
"",
|
|
179
|
+
"聚合看板:helloloop dashboard",
|
|
180
|
+
"续跑提示:helloloop resume-host",
|
|
125
181
|
"实时观察:helloloop watch",
|
|
126
182
|
].filter(Boolean).join("\n");
|
|
127
183
|
}
|