helloloop 0.8.6 → 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/.codex-plugin/plugin.json +1 -1
- package/README.md +230 -498
- 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 +51 -1
- package/src/cli_analyze_command.mjs +12 -14
- package/src/cli_args.mjs +106 -32
- package/src/cli_command_handlers.mjs +73 -25
- package/src/cli_support.mjs +2 -0
- package/src/common.mjs +11 -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 +7 -2
- package/src/engine_selection.mjs +24 -0
- package/src/engine_selection_probe.mjs +10 -6
- package/src/engine_selection_settings.mjs +53 -44
- package/src/execution_interactivity.mjs +12 -0
- package/src/host_continuation.mjs +305 -0
- package/src/install_codex.mjs +20 -30
- 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 +15 -1
- package/src/runner_execution_support.mjs +69 -3
- package/src/runner_once.mjs +5 -0
- package/src/runner_status.mjs +72 -4
- package/src/runtime_engine_support.mjs +52 -5
- 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 +49 -0
- package/src/supervisor_guardian.mjs +307 -0
- package/src/supervisor_runtime.mjs +142 -83
- package/src/supervisor_state.mjs +64 -0
- package/src/supervisor_watch.mjs +364 -0
- 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";
|
|
@@ -133,7 +135,6 @@ export function installCodexHost(bundleRoot, options = {}) {
|
|
|
133
135
|
const resolvedLocalRoot = resolveCodexLocalRoot(options.codexHome);
|
|
134
136
|
const targetPluginsRoot = path.join(resolvedLocalRoot, "plugins");
|
|
135
137
|
const targetPluginRoot = path.join(targetPluginsRoot, CODEX_PLUGIN_NAME);
|
|
136
|
-
const legacyTargetPluginRoot = path.join(resolvedCodexHome, "plugins", CODEX_PLUGIN_NAME);
|
|
137
138
|
const targetPluginCacheRoot = path.join(
|
|
138
139
|
resolvedCodexHome,
|
|
139
140
|
"plugins",
|
|
@@ -143,13 +144,11 @@ export function installCodexHost(bundleRoot, options = {}) {
|
|
|
143
144
|
);
|
|
144
145
|
const targetInstalledPluginRoot = path.join(targetPluginCacheRoot, "local");
|
|
145
146
|
const marketplaceFile = path.join(resolvedLocalRoot, ".agents", "plugins", "marketplace.json");
|
|
146
|
-
const legacyMarketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
|
|
147
147
|
const configFile = path.join(resolvedCodexHome, "config.toml");
|
|
148
148
|
const manifestFile = path.join(bundleRoot, ".codex-plugin", "plugin.json");
|
|
149
149
|
const existingMarketplace = readExistingJsonOrThrow(marketplaceFile, "Codex marketplace 配置");
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
: readExistingJsonOrThrow(legacyMarketplaceFile, "Codex legacy marketplace 配置");
|
|
150
|
+
const bundleIsPluginRoot = isSamePath(bundleRoot, targetPluginRoot);
|
|
151
|
+
const bundleIsInstalledRoot = isSamePath(bundleRoot, targetInstalledPluginRoot);
|
|
153
152
|
|
|
154
153
|
if (!fileExists(manifestFile)) {
|
|
155
154
|
throw new Error(`未找到 Codex 插件 manifest:${manifestFile}`);
|
|
@@ -157,25 +156,27 @@ export function installCodexHost(bundleRoot, options = {}) {
|
|
|
157
156
|
|
|
158
157
|
assertPathInside(resolvedLocalRoot, targetPluginRoot, "Codex 本地插件目录");
|
|
159
158
|
assertPathInside(resolvedCodexHome, targetPluginCacheRoot, "Codex 插件缓存目录");
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
if (!bundleIsPluginRoot) {
|
|
160
|
+
removeTargetIfNeeded(targetPluginRoot, options.force);
|
|
161
|
+
}
|
|
162
|
+
if (!bundleIsInstalledRoot) {
|
|
163
|
+
removeTargetIfNeeded(targetPluginCacheRoot, options.force);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
ensureDir(targetPluginsRoot);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
}
|
|
173
177
|
|
|
174
178
|
ensureDir(path.dirname(marketplaceFile));
|
|
175
179
|
updateCodexMarketplace(marketplaceFile, existingMarketplace);
|
|
176
|
-
if (legacyMarketplaceFile !== marketplaceFile) {
|
|
177
|
-
removeCodexMarketplaceEntry(legacyMarketplaceFile, existingLegacyMarketplace);
|
|
178
|
-
}
|
|
179
180
|
upsertCodexPluginConfig(configFile);
|
|
180
181
|
|
|
181
182
|
return {
|
|
@@ -192,7 +193,6 @@ export function uninstallCodexHost(options = {}) {
|
|
|
192
193
|
const resolvedCodexHome = resolveHomeDir(options.codexHome, ".codex");
|
|
193
194
|
const resolvedLocalRoot = resolveCodexLocalRoot(options.codexHome);
|
|
194
195
|
const targetPluginRoot = path.join(resolvedLocalRoot, "plugins", CODEX_PLUGIN_NAME);
|
|
195
|
-
const legacyTargetPluginRoot = path.join(resolvedCodexHome, "plugins", CODEX_PLUGIN_NAME);
|
|
196
196
|
const targetPluginCacheRoot = path.join(
|
|
197
197
|
resolvedCodexHome,
|
|
198
198
|
"plugins",
|
|
@@ -201,29 +201,19 @@ export function uninstallCodexHost(options = {}) {
|
|
|
201
201
|
CODEX_PLUGIN_NAME,
|
|
202
202
|
);
|
|
203
203
|
const marketplaceFile = path.join(resolvedLocalRoot, ".agents", "plugins", "marketplace.json");
|
|
204
|
-
const legacyMarketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
|
|
205
204
|
const configFile = path.join(resolvedCodexHome, "config.toml");
|
|
206
205
|
const existingMarketplace = readExistingJsonOrThrow(marketplaceFile, "Codex marketplace 配置");
|
|
207
|
-
const existingLegacyMarketplace = legacyMarketplaceFile === marketplaceFile
|
|
208
|
-
? existingMarketplace
|
|
209
|
-
: readExistingJsonOrThrow(legacyMarketplaceFile, "Codex legacy marketplace 配置");
|
|
210
206
|
|
|
211
207
|
const removedPlugin = removePathIfExists(targetPluginRoot);
|
|
212
|
-
const removedLegacyPlugin = legacyTargetPluginRoot === targetPluginRoot
|
|
213
|
-
? false
|
|
214
|
-
: removePathIfExists(legacyTargetPluginRoot);
|
|
215
208
|
const removedCache = removePathIfExists(targetPluginCacheRoot);
|
|
216
209
|
const removedMarketplace = removeCodexMarketplaceEntry(marketplaceFile, existingMarketplace);
|
|
217
|
-
const removedLegacyMarketplace = legacyMarketplaceFile === marketplaceFile
|
|
218
|
-
? false
|
|
219
|
-
: removeCodexMarketplaceEntry(legacyMarketplaceFile, existingLegacyMarketplace);
|
|
220
210
|
const removedConfig = removeCodexPluginConfig(configFile);
|
|
221
211
|
|
|
222
212
|
return {
|
|
223
213
|
host: "codex",
|
|
224
214
|
displayName: "Codex",
|
|
225
215
|
targetRoot: targetPluginRoot,
|
|
226
|
-
removed: removedPlugin ||
|
|
216
|
+
removed: removedPlugin || removedCache || removedMarketplace || removedConfig,
|
|
227
217
|
marketplaceFile,
|
|
228
218
|
configFile,
|
|
229
219
|
};
|
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,8 +3,9 @@ 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
|
-
import { saveBacklog } from "./config.mjs";
|
|
8
|
+
import { saveBacklog, writeStatus } from "./config.mjs";
|
|
8
9
|
import { reviewTaskCompletion } from "./completion_review.mjs";
|
|
9
10
|
import { updateTask } from "./backlog.mjs";
|
|
10
11
|
import { buildTaskPrompt } from "./prompt.mjs";
|
|
@@ -267,6 +268,19 @@ export async function executeSingleTask(context, options = {}) {
|
|
|
267
268
|
|
|
268
269
|
updateTask(execution.backlog, execution.task.id, { status: "in_progress", startedAt: nowIso() });
|
|
269
270
|
saveBacklog(context, execution.backlog);
|
|
271
|
+
writeStatus(context, {
|
|
272
|
+
ok: true,
|
|
273
|
+
sessionId: options.supervisorSessionId || "",
|
|
274
|
+
stage: "task-started",
|
|
275
|
+
taskId: execution.task.id,
|
|
276
|
+
taskTitle: execution.task.title,
|
|
277
|
+
runDir: execution.runDir,
|
|
278
|
+
summary: "",
|
|
279
|
+
message: `开始执行任务:${execution.task.title}`,
|
|
280
|
+
});
|
|
281
|
+
refreshHostContinuationArtifacts(context, {
|
|
282
|
+
sessionId: options.supervisorSessionId || "",
|
|
283
|
+
});
|
|
270
284
|
|
|
271
285
|
const state = {
|
|
272
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
|
|
|
@@ -11,6 +12,7 @@ export async function runOnce(context, options = {}) {
|
|
|
11
12
|
|
|
12
13
|
writeStatus(context, {
|
|
13
14
|
ok: result.ok,
|
|
15
|
+
sessionId: options.supervisorSessionId || "",
|
|
14
16
|
stage: result.kind,
|
|
15
17
|
taskId: result.task?.id || null,
|
|
16
18
|
taskTitle: result.task?.title || "",
|
|
@@ -24,6 +26,9 @@ export async function runOnce(context, options = {}) {
|
|
|
24
26
|
lastResult: result.ok ? "本轮成功" : (result.summary || result.kind),
|
|
25
27
|
nextTask,
|
|
26
28
|
}));
|
|
29
|
+
refreshHostContinuationArtifacts(context, {
|
|
30
|
+
sessionId: options.supervisorSessionId || "",
|
|
31
|
+
});
|
|
27
32
|
|
|
28
33
|
return result;
|
|
29
34
|
}
|