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.
Files changed (52) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +230 -498
  4. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  5. package/hosts/gemini/extension/gemini-extension.json +1 -1
  6. package/native/windows-hidden-shell-proxy/HelloLoopHiddenShellProxy.csproj +11 -0
  7. package/native/windows-hidden-shell-proxy/Program.cs +498 -0
  8. package/package.json +4 -2
  9. package/src/activity_projection.mjs +294 -0
  10. package/src/analyze_confirmation.mjs +3 -1
  11. package/src/analyzer.mjs +2 -1
  12. package/src/auto_execution_options.mjs +13 -0
  13. package/src/background_launch.mjs +73 -0
  14. package/src/cli.mjs +51 -1
  15. package/src/cli_analyze_command.mjs +12 -14
  16. package/src/cli_args.mjs +106 -32
  17. package/src/cli_command_handlers.mjs +73 -25
  18. package/src/cli_support.mjs +2 -0
  19. package/src/common.mjs +11 -0
  20. package/src/dashboard_command.mjs +371 -0
  21. package/src/dashboard_tui.mjs +289 -0
  22. package/src/dashboard_web.mjs +351 -0
  23. package/src/dashboard_web_client.mjs +167 -0
  24. package/src/dashboard_web_page.mjs +49 -0
  25. package/src/engine_event_parser_codex.mjs +167 -0
  26. package/src/engine_process_support.mjs +7 -2
  27. package/src/engine_selection.mjs +24 -0
  28. package/src/engine_selection_probe.mjs +10 -6
  29. package/src/engine_selection_settings.mjs +53 -44
  30. package/src/execution_interactivity.mjs +12 -0
  31. package/src/host_continuation.mjs +305 -0
  32. package/src/install_codex.mjs +20 -30
  33. package/src/install_shared.mjs +9 -0
  34. package/src/node_process_launch.mjs +28 -0
  35. package/src/process.mjs +2 -0
  36. package/src/runner_execute_task.mjs +15 -1
  37. package/src/runner_execution_support.mjs +69 -3
  38. package/src/runner_once.mjs +5 -0
  39. package/src/runner_status.mjs +72 -4
  40. package/src/runtime_engine_support.mjs +52 -5
  41. package/src/runtime_engine_task.mjs +7 -0
  42. package/src/runtime_settings.mjs +105 -0
  43. package/src/runtime_settings_loader.mjs +19 -0
  44. package/src/shell_invocation.mjs +227 -9
  45. package/src/supervisor_cli_support.mjs +49 -0
  46. package/src/supervisor_guardian.mjs +307 -0
  47. package/src/supervisor_runtime.mjs +142 -83
  48. package/src/supervisor_state.mjs +64 -0
  49. package/src/supervisor_watch.mjs +364 -0
  50. package/src/terminal_session_limits.mjs +1 -21
  51. package/src/windows_hidden_shell_proxy.mjs +405 -0
  52. 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
+ }
@@ -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 existingLegacyMarketplace = legacyMarketplaceFile === marketplaceFile
151
- ? existingMarketplace
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
- removeTargetIfNeeded(targetPluginRoot, options.force);
161
- removeTargetIfNeeded(targetPluginCacheRoot, options.force);
162
- if (legacyTargetPluginRoot !== targetPluginRoot) {
163
- removePathIfExists(legacyTargetPluginRoot);
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
- ensureDir(targetPluginRoot);
168
- ensureDir(targetInstalledPluginRoot);
169
- copyBundleEntries(bundleRoot, targetPluginRoot, codexBundleEntries);
170
- copyBundleEntries(bundleRoot, targetInstalledPluginRoot, codexBundleEntries);
171
- removePathIfExists(path.join(targetPluginRoot, ".git"));
172
- removePathIfExists(path.join(targetInstalledPluginRoot, ".git"));
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 || removedLegacyPlugin || removedCache || removedMarketplace || removedLegacyMarketplace || removedConfig,
216
+ removed: removedPlugin || removedCache || removedMarketplace || removedConfig,
227
217
  marketplaceFile,
228
218
  configFile,
229
219
  };
@@ -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: !options.yes,
121
+ interactive: shouldPromptForEngineSelection(options),
56
122
  });
57
123
 
58
124
  return {
@@ -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
  }